-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add Node.js v20 support Since Node.js v20 moves loaders to a separate thread, we can no longer depend on loading the modules in the loader to get the exports. This is needed to add our mutable proxy. For Node.js 20, exports are now retrieved via parsing. To reduce startup overhead on older versions of Node.js, the previous method of getting exports is used, avoiding loading and using parsers. * make stacked loaders work * restrict get-esm-exports test to node 20 * remove 'unsupported' * normalize file URLs to paths (important on windows) * remove trailing \r in esm exports test
- Loading branch information
Showing
7 changed files
with
230 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
'use strict' | ||
|
||
const { Parser } = require('acorn') | ||
const { importAssertions } = require('acorn-import-assertions'); | ||
|
||
const acornOpts = { | ||
ecmaVersion: 'latest', | ||
sourceType: 'module' | ||
} | ||
|
||
const parser = Parser.extend(importAssertions) | ||
|
||
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) | ||
} | ||
} | ||
return Array.from(exportedNames) | ||
} | ||
|
||
function parseDeclaration (node, exportedNames) { | ||
switch (node.declaration.type) { | ||
case 'FunctionDeclaration': | ||
exportedNames.add(node.declaration.id.name) | ||
break | ||
case 'VariableDeclaration': | ||
for (const varDecl of node.declaration.declarations) { | ||
parseVariableDeclaration(varDecl, exportedNames) | ||
} | ||
break | ||
case 'ClassDeclaration': | ||
exportedNames.add(node.declaration.id.name) | ||
break | ||
default: | ||
warn('unknown declaration type: ' + node.delcaration.type) | ||
} | ||
} | ||
|
||
function parseVariableDeclaration (node, exportedNames) { | ||
switch (node.id.type) { | ||
case 'Identifier': | ||
exportedNames.add(node.id.name) | ||
break | ||
case 'ObjectPattern': | ||
for (const prop of node.id.properties) { | ||
exportedNames.add(prop.value.name) | ||
} | ||
break | ||
case 'ArrayPattern': | ||
for (const elem of node.id.elements) { | ||
exportedNames.add(elem.name) | ||
} | ||
break | ||
default: | ||
warn('unknown variable declaration type: ' + node.id.type) | ||
} | ||
} | ||
|
||
function parseSpecifiers (node, exportedNames) { | ||
for (const specifier of node.specifiers) { | ||
if (specifier.exported.type === 'Identifier') { | ||
exportedNames.add(specifier.exported.name) | ||
} else if (specifier.exported.type === 'Literal') { | ||
exportedNames.add(specifier.exported.value) | ||
} else { | ||
warn('unrecognized specifier type: ' + specifier.exported.type) | ||
} | ||
} | ||
} | ||
|
||
module.exports = getEsmExports |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
'use strict' | ||
|
||
const getEsmExports = require('./get-esm-exports.js') | ||
const { parse: getCjsExports } = require('cjs-module-lexer') | ||
const fs = require('fs') | ||
const { fileURLToPath } = require('url') | ||
|
||
function addDefault(arr) { | ||
return Array.from(new Set(['default', ...arr])) | ||
} | ||
|
||
async function getExports (url, context, parentLoad) { | ||
// `parentLoad` gives us the possibility of getting the source | ||
// from an upstream loader. This doesn't always work though, | ||
// so later on we fall back to reading it from disk. | ||
const parentCtx = await parentLoad(url, context) | ||
let source = parentCtx.source | ||
const format = parentCtx.format | ||
|
||
// TODO support non-node/file urls somehow? | ||
if (format === 'builtin') { | ||
// Builtins don't give us the source property, so we're stuck | ||
// just requiring it to get the exports. | ||
return addDefault(Object.keys(require(url))) | ||
} | ||
|
||
if (!source) { | ||
// Sometimes source is retrieved by parentLoad, sometimes it isn't. | ||
source = fs.readFileSync(fileURLToPath(url), 'utf8') | ||
} | ||
|
||
if (format === 'module') { | ||
return getEsmExports(source) | ||
} | ||
if (format === 'commonjs') { | ||
return addDefault(getCjsExports(source).exports) | ||
} | ||
|
||
// At this point our `format` is either undefined or not known by us. Fall | ||
// back to parsing as ESM/CJS. | ||
const esmExports = getEsmExports(source) | ||
if (!esmExports.length) { | ||
// TODO(bengl) it's might be possible to get here if somehow the format | ||
// isn't set at first and yet we have an ESM module with no exports. | ||
// I couldn't construct an example that would do this, so maybe it's | ||
// impossible? | ||
return addDefault(getCjsExports(source).exports) | ||
} | ||
} | ||
|
||
module.exports = getExports |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// Exporting declarations | ||
export let name1, name2/*, … */; // also var //| name1,name2 | ||
export const name1 = 1, name2 = 2/*, … */; // also var, let //| name1,name2 | ||
export function functionName() { /* … */ } //| functionName | ||
export class ClassName { /* … */ } //| ClassName | ||
export function* generatorFunctionName() { /* … */ } //| generatorFunctionName | ||
export const { name1, name2: bar } = o; //| name1,bar | ||
export const [ name1, name2 ] = array; //| name1,name2 | ||
|
||
// Export list | ||
let name1, nameN; export { name1, /* …, */ nameN }; //| name1,nameN | ||
let variable1, variable2, nameN; export { variable1 as name1, variable2 as name2, /* …, */ nameN }; //| name1,name2,nameN | ||
let variable1; export { variable1 as "string name" }; //| string name | ||
let name1; export { name1 as default /*, … */ }; //| default | ||
|
||
// Default exports | ||
export default expression; //| default | ||
export default function functionName() { /* … */ } //| default | ||
export default class ClassName { /* … */ } //| default | ||
export default function* generatorFunctionName() { /* … */ } //| default | ||
export default function () { /* … */ } //| default | ||
export default class { /* … */ } //| default | ||
export default function* () { /* … */ } //| default | ||
|
||
// Aggregating modules | ||
export * from "module-name"; //| * | ||
export * as name1 from "module-name"; //| name1 | ||
export { name1, /* …, */ nameN } from "module-name"; //| name1,nameN | ||
export { import1 as name1, import2 as name2, /* …, */ nameN } from "module-name"; //| name1,name2,nameN | ||
export { default, /* …, */ } from "module-name"; //| default | ||
export { default as name1 } from "module-name"; //| name1 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
'use strict' | ||
|
||
const getEsmExports = require('../../lib/get-esm-exports.js') | ||
const fs = require('fs') | ||
const assert = require('assert') | ||
const path = require('path') | ||
|
||
const fixturePath = path.join(__dirname, '../fixtures/esm-exports.txt') | ||
const fixture = fs.readFileSync(fixturePath, 'utf8') | ||
|
||
fixture.split('\n').forEach(line => { | ||
if (!line.includes(' //| ')) return | ||
const [mod, testStr] = line.split(' //| ') | ||
const expectedNames = testStr.split(',').map(x => x.trim()) | ||
if (expectedNames[0] === '') { | ||
expectedNames.length = 0 | ||
} | ||
const names = getEsmExports(mod) | ||
assert.deepEqual(expectedNames, names) | ||
console.log(`${mod}\n ✅ contains exports: ${testStr}`) | ||
}) | ||
|
||
// // Generate fixture data | ||
// fixture.split('\n').forEach(line => { | ||
// if (!line.includes('export ')) { | ||
// console.log(line) | ||
// return | ||
// } | ||
// const names = getEsmExports(line) | ||
// console.log(line, '//|', names.join(',')) | ||
// }) |