Skip to content
This repository was archived by the owner on Apr 16, 2020. It is now read-only.

Commit b635988

Browse files
committed
esm: add utility method for detecting ES module syntax
1 parent 030fa2e commit b635988

15 files changed

+146
-0
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,46 @@ function createRequire(filename) {
879879

880880
Module.createRequire = createRequire;
881881

882+
Module.containsModuleSyntax = (source) => {
883+
// Detect whether input source code contains at least one `import` or `export`
884+
// statement. This can be used by dependent utilities as a way of detecting ES
885+
// module source code from Script/CommonJS source code. Since our detection is
886+
// so simple, we can avoid needing to use Acorn for a full parse; we can
887+
// detect import or export statements just from the tokens. Also as of this
888+
// writing, Acorn doesn't support import() expressions as they are only Stage
889+
// 3; yet Node already supports them.
890+
const acorn = require('internal/deps/acorn/acorn/dist/acorn');
891+
source = stripShebang(source);
892+
source = stripBOM(source);
893+
try {
894+
let prevToken, prevPrevToken;
895+
for (const { type: token } of acorn.tokenizer(source)) {
896+
if (prevToken &&
897+
// By definition import or export must be followed by another token.
898+
(prevToken.keyword === 'import' || prevToken.keyword === 'export') &&
899+
// Skip `import(`; look only for import statements, not expressions.
900+
// import() expressions are allowed in both CommonJS and ES modules.
901+
token.label !== '(' &&
902+
// Also ensure that the keyword we just saw wasn't an allowed use
903+
// of a reserved word as a property name; see
904+
// test/fixtures/es-modules/detect/cjs-with-property-named-import.js.
905+
!(prevPrevToken && prevPrevToken.label === '.') &&
906+
token.label !== ':')
907+
return true; // This source code contains ES module syntax.
908+
prevPrevToken = prevToken;
909+
prevToken = token;
910+
}
911+
} catch {
912+
// If the tokenizer threw, there's a syntax error.
913+
// Compile the script, this will throw with an informative error.
914+
const vm = require('vm');
915+
new vm.Script(source, { displayErrors: true });
916+
}
917+
// This source code does not contain ES module syntax.
918+
// It may or may not be CommonJS, and it may or may not be valid syntax.
919+
return false;
920+
};
921+
882922
Module._initPaths = function() {
883923
var homeDir;
884924
var nodePath;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
require('../common');
4+
const { strictEqual, fail } = require('assert');
5+
const { readFileSync } = require('fs');
6+
7+
const { containsModuleSyntax } = require('module');
8+
9+
expect('esm-with-import-statement.js', 'module');
10+
expect('esm-with-export-statement.js', 'module');
11+
expect('esm-with-import-expression.js', 'module');
12+
expect('esm-with-indented-import-statement.js', 'module');
13+
14+
expect('cjs-with-require.js', 'commonjs');
15+
expect('cjs-with-import-expression.js', 'commonjs');
16+
expect('cjs-with-property-named-import.js', 'commonjs');
17+
expect('cjs-with-property-named-export.js', 'commonjs');
18+
expect('cjs-with-string-containing-import.js', 'commonjs');
19+
20+
expect('print-version.js', 'commonjs');
21+
expect('ambiguous-with-import-expression.js', 'commonjs');
22+
23+
expect('syntax-error.js', 'Invalid or unexpected token', true);
24+
25+
function expect(file, want, wantsError = false) {
26+
const source = readFileSync(
27+
require.resolve(`../fixtures/es-modules/detect/${file}`),
28+
'utf8');
29+
let isModule;
30+
try {
31+
isModule = containsModuleSyntax(source);
32+
} catch (err) {
33+
if (wantsError) {
34+
return strictEqual(err.message, want);
35+
} else {
36+
return fail(
37+
`Expected ${file} to throw '${want}'; received '${err.message}'`);
38+
}
39+
}
40+
if (wantsError)
41+
return fail(`Expected ${file} to throw '${want}'; no error was thrown`);
42+
else
43+
return strictEqual((isModule ? 'module' : 'commonjs'), want);
44+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(async () => {
2+
await import('./print-version.js');
3+
})();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { version } = require('process');
2+
3+
(async () => {
4+
await import('./print-version.js');
5+
})();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// See ./cjs-with-property-named-import.js
2+
3+
global.export = 3;
4+
5+
global['export'] = 3;
6+
7+
const obj = {
8+
export: 3 // Specifically at column 0, to try to trick the detector
9+
}
10+
11+
console.log(require('process').version);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// In JavaScript, reserved words cannot be identifiers (the `foo` in `var foo`)
2+
// but they can be properties (`obj.foo`). This file checks that the `import`
3+
// reserved word isn't incorrectly detected as a keyword. For more info see:
4+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Reserved_word_usage
5+
6+
global.import = 3;
7+
8+
global['import'] = 3;
9+
10+
const obj = {
11+
import: 3 // Specifically at column 0, to try to trick the detector
12+
}
13+
14+
console.log(require('process').version);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const { version } = require('process');
2+
3+
console.log(version);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const { version } = require('process');
2+
3+
const sneakyString = `
4+
import { version } from 'process';
5+
`;
6+
7+
console.log(version);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const version = process.version;
2+
3+
export default version;
4+
5+
console.log(version);
6+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { version } from 'process';
2+
3+
(async () => {
4+
await import('./print-version.js');
5+
})();

0 commit comments

Comments
 (0)