Skip to content

Commit

Permalink
fixes loader cyclical dependency bug by forgoing module wrapping of f…
Browse files Browse the repository at this point in the history
…ile and embedding file contents within the patch instead
  • Loading branch information
khanayan123 committed Nov 7, 2023
1 parent 5845e21 commit 647ecef
Show file tree
Hide file tree
Showing 21 changed files with 429 additions and 61 deletions.
46 changes: 41 additions & 5 deletions hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
//
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2021 Datadog, Inc.

const fs = require('fs');
const { fileURLToPath } = require('url')
const astParse = require('./lib/ast-parse.js')
const specifiers = new Map()
const isWin = process.platform === "win32"

Expand Down Expand Up @@ -106,9 +109,9 @@ function createHook (meta) {


specifiers.set(url.url, specifier)

return {
url: addIitm(url.url),
url: url.format !== 'module' ? addIitm(url.url) : url.url,
shortCircuit: true,
format: url.format
}
Expand All @@ -119,7 +122,7 @@ function createHook (meta) {
if (hasIitm(url)) {
const realUrl = deleteIitm(url)
const exportNames = await getExports(realUrl, context, parentGetSource)
return {
return {
source: `
import { register } from '${iitmURL}'
import * as namespace from ${JSON.stringify(url)}
Expand All @@ -134,6 +137,39 @@ set.${n} = (v) => {
`).join('\n')}
register(${JSON.stringify(realUrl)}, namespace, set, ${JSON.stringify(specifiers.get(realUrl))})
`
}
}
else if (context.format === 'module') {
let fileContents
try {
fileContents = fs.readFileSync(fileURLToPath(url), 'utf8')
} catch (parseError) {
console.error(`Had trouble reading file: ${fileContents}, got error: ${parseError}`);
return parentGetSource(url, context, parentGetSource)
}
try {
const outPut = astParse(fileContents)
fileContents = outPut.code
exportAlias = outPut.exportAlias
} catch (parseError) {
console.error(`Tried AST parsing ${realPath}, got error: ${parseError}`);
return parentGetSource(url, context, parentGetSource)
}
const src = `${fileContents}
import { register } from '${iitmURL}'
const set = {}
const namespace = {}
${Object.entries(exportAlias).map(([key, value]) => `
set.${key} = (v) => {
${value} = v;
return true;
};
namespace.${key} = ${value}
`).join('\n')}
register(${JSON.stringify(url)}, namespace, set, ${JSON.stringify(specifiers.get(url))})
`
return {
source: src
}
}

Expand All @@ -142,7 +178,7 @@ register(${JSON.stringify(realUrl)}, namespace, set, ${JSON.stringify(specifiers

// For Node.js 16.12.0 and higher.
async function load (url, context, parentLoad) {
if (hasIitm(url)) {
if (hasIitm(url) || context.format === 'module') {
const { source } = await getSource(url, context, parentLoad)
return {
source,
Expand All @@ -162,7 +198,7 @@ register(${JSON.stringify(realUrl)}, namespace, set, ${JSON.stringify(specifiers
resolve,
getSource,
getFormat (url, context, parentGetFormat) {
if (hasIitm(url)) {
if (hasIitm(url) || context.format === 'module') {
return {
format: 'module'
}
Expand Down
9 changes: 9 additions & 0 deletions lib/ast-parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const getEsmExports = require('./get-esm-exports.js')

function astParse(fileContents) {
return getEsmExports(fileContents, true)
}

module.exports = astParse
203 changes: 149 additions & 54 deletions lib/get-esm-exports.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const recast = require('recast');
const { Parser } = require('acorn')
const { importAssertions } = require('acorn-import-assertions');

Expand All @@ -14,82 +15,176 @@ function warn (txt) {
process.emitWarning(txt, 'get-esm-exports')
}

function getEsmExports (moduleStr) {
const exportedNames = new Set()
const tree = parser.parse(moduleStr, acornOpts)
for (const node of tree.body) {
if (!node.type.startsWith('Export')) continue
switch (node.type) {
case 'ExportNamedDeclaration':
if (node.declaration) {
parseDeclaration(node, exportedNames)
} else {
parseSpecifiers(node, exportedNames)
}
break
case 'ExportDefaultDeclaration':
exportedNames.add('default')
break
case 'ExportAllDeclaration':
if (node.exported) {
exportedNames.add(node.exported.name)
} else {
exportedNames.add('*')
}
break
default:
warn('unrecognized export type: ' + node.type)
function getEsmExports(moduleStr, generate=false) {
const exportSpecifierNames = new Set();
const exportAlias = {}
const ast = recast.parse(moduleStr, {parser: {
parse(source) {
return parser.parse(source, acornOpts);
}
}})

recast.visit(ast, {
visitNode(path) {
const node = path.node;

if (!node.type.startsWith('Export')) {
this.traverse(path);
return;
}

switch (node.type) {
case 'ExportNamedDeclaration':
if (node.declaration) {
parseDeclaration(node.declaration, exportAlias);
} else {
parseSpecifiers(node.specifiers, exportAlias, exportSpecifierNames);
}
break;
case 'ExportDefaultDeclaration':
const iitmRenamedExport = 'iitmRenamedExport';
if (node.declaration.type === 'ObjectExpression' || node.declaration.type === 'ArrayExpression' || node.declaration.type === 'Literal') {
const variableDeclaration = {
type: 'VariableDeclaration',
kind: 'let',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: iitmRenamedExport,
},
init: node.declaration,
},
],
};
path.replace(variableDeclaration);
const newExportDefaultDeclaration = {
type: 'ExportDefaultDeclaration',
declaration: {
type: 'Identifier',
name: iitmRenamedExport,
},
};
path.insertAfter(newExportDefaultDeclaration);
} else {
node.declaration.id = { type: 'Identifier', name: iitmRenamedExport };
}
exportAlias['default'] = iitmRenamedExport;
break;
case 'ExportAllDeclaration':
const exportedName = node.exported ? node.exported.name : '*';
exportAlias[exportedName] = exportedName;
break;
case 'ExportSpecifier':
exportSpecifierNames.add(node.local.name)
if (node.exported?.name) {
exportAlias[node.exported.name] = node.local.name;
} else if (node.exported?.value) {
exportAlias[node.exported.value] = node.local.name;
} else {
warn('unrecognized specifier export: ' + node.exported);
}
break;
default:
warn('unrecognized export type: ' + node.type);
}
this.traverse(path);
},
});
if (exportSpecifierNames.size !== 0) {
convertExportSpecifierToLet(exportSpecifierNames, ast)
}

if (generate) {
return {
exportAlias: exportAlias,
code: recast.print(ast).code
}
}
return Array.from(exportedNames)

return Object.keys(exportAlias)
}

function convertExportSpecifierToLet(exportSpecifierNames, ast) {
recast.visit(ast, {
visitVariableDeclaration(path) {
const declaration = path.node;
if (declaration.kind === 'const') {
for (const declarator of declaration.declarations) {
const variableName = declarator.id.name;
if (exportSpecifierNames.has(variableName)) {
declaration.kind = 'let'; // Change 'const' to 'let'
}
}
}
this.traverse(path);
},
});
}

function parseDeclaration (node, exportedNames) {
switch (node.declaration.type) {
function parseDeclaration(declaration, exportAlias) {
switch (declaration.type) {
case 'FunctionDeclaration':
exportedNames.add(node.declaration.id.name)
break
exportAlias[declaration.id.name] = declaration.id.name
break;
case 'VariableDeclaration':
for (const varDecl of node.declaration.declarations) {
parseVariableDeclaration(varDecl, exportedNames)
for (const varDecl of declaration.declarations) {
if (declaration.kind === 'const') {
declaration.kind = 'let';
}
parseVariableDeclaration(varDecl, exportAlias);
}
break
break;
case 'ClassDeclaration':
exportedNames.add(node.declaration.id.name)
break
exportAlias[declaration.id.name] = declaration.id.name
break;
default:
warn('unknown declaration type: ' + node.delcaration.type)
warn('unknown declaration type: ' + declaration.type);
}
}

function parseVariableDeclaration (node, exportedNames) {
switch (node.id.type) {
function parseVariableDeclaration(varDecl, exportAlias) {
switch (varDecl.id.type) {
case 'Identifier':
exportedNames.add(node.id.name)
break
exportAlias[varDecl.id.name] = varDecl.id.name
break;
case 'ObjectPattern':
for (const prop of node.id.properties) {
exportedNames.add(prop.value.name)
for (const prop of varDecl.id.properties) {
exportAlias[prop.value.name] = prop.value.name
}
break
break;
case 'ArrayPattern':
for (const elem of node.id.elements) {
exportedNames.add(elem.name)
for (const elem of varDecl.id.elements) {
if (elem) {
exportAlias[elem.name] = elem.name
}
}
break
break;
default:
warn('unknown variable declaration type: ' + node.id.type)
warn('unknown variable declaration type: ' + varDecl.id.type);
}
}

function parseSpecifiers (node, exportedNames) {
for (const specifier of node.specifiers) {
if (specifier.exported.type === 'Identifier') {
exportedNames.add(specifier.exported.name)
function parseSpecifiers(specifiers, exportAlias, exportSpecifierNames) {
for (const specifier of specifiers) {
if (specifier.type === 'ExportSpecifier') {
exportSpecifierNames.add(specifier.local.name)
if (specifier.exported?.name) {
exportAlias[specifier.exported.name] = specifier.local.name;
} else if (specifier.exported?.value) {
exportAlias[specifier.exported.value] = specifier.local.name;
} else {
warn('unrecognized specifier export: ' + specifier);
}
}
else if (specifier.exported.type === 'Identifier') {
exportAlias[specifier.exported.name] = specifier.exported.name
} else if (specifier.exported.type === 'Literal') {
exportedNames.add(specifier.exported.value)
} else {
warn('unrecognized specifier type: ' + specifier.exported.type)
exportAlias[specifier.exported.value] = specifier.exported.value
}
else {
warn('unrecognized specifier type: ' + specifier.exported.type);
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
"typescript": "^4.7.4"
},
"dependencies": {
"acorn": "^8.8.2",
"acorn": "^8.11.2",
"acorn-import-assertions": "^1.9.0",
"cjs-module-lexer": "^1.2.2",
"module-details-from-path": "^1.0.3"
"module-details-from-path": "^1.0.3",
"recast": "^0.23.4"
}
}
7 changes: 7 additions & 0 deletions test/fixtures/a.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { testB } from './b.mjs';

export function testA() {
console.log("testA");
}

testB();
6 changes: 6 additions & 0 deletions test/fixtures/b.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { testA } from './a.mjs';

export function testB() {
console.log("testB");
testA();
}
11 changes: 11 additions & 0 deletions test/fixtures/export-types/declarations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const o = { name5: 1, name6: 1 };
const array = [1, 1]

// Exporting declarations
export let name1 = 1, name2 = 1/*, … */; // also var
export const name3 = 1, name4 = 1/*, … */; // also var, let
export function functionName() { return 1 }
export class ClassName { getFoo() { return 1 } }
export function* generatorFunctionName() { return 1 }
export const { name5, name6: bar } = o;
export const [ name7, name8 ] = array;
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-class-anon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class { getFoo() { return 1 } }
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-class.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default class ClassName { getFoo() { return 1 } }
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-expression-array.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default [1]
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-expression-num.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 1
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-expression-string.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'dog'
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-function-anon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function () { return 1 }
1 change: 1 addition & 0 deletions test/fixtures/export-types/default-function.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function functionName() { return 1 }
Loading

0 comments on commit 647ecef

Please sign in to comment.