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

Commit

Permalink
esm: utility method for detecting ES module syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
GeoffreyBooth committed May 19, 2019
1 parent 030fa2e commit 45954e1
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 0 deletions.
40 changes: 40 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,46 @@ function createRequire(filename) {

Module.createRequire = createRequire;

Module.containsModuleSyntax = (source) => {
// Detect the module type of source code: CommonJS or ES module.
// An ES module, for the purposes of this algorithm, is defined as any
// JavaScript source code containing an import or export statement.
// Since our detection is so simple, we can avoid needing to use Acorn for a
// full parse; we can detect import or export statements just from the tokens.
// Also as of this writing, Acorn doesn't support import() expressions as they
// are only Stage 3; yet Node already supports them.
const acorn = require('internal/deps/acorn/acorn/dist/acorn');
source = stripShebang(source);
source = stripBOM(source);
try {
let prevToken, prevPrevToken;
for (const { type: token } of acorn.tokenizer(source)) {
if (prevToken &&
// By definition import or export must be followed by another token.
(prevToken.keyword === 'import' || prevToken.keyword === 'export') &&
// Skip `import(`; look only for import statements, not expressions.
// import() expressions are allowed in both CommonJS and ES modules.
token.label !== '(' &&
// Also ensure that the keyword we just saw wasn't an allowed use
// of a reserved word as a property name; see
// test/fixtures/es-modules/detect/cjs-with-property-named-import.js.
!(prevPrevToken && prevPrevToken.label === '.') &&
token.label !== ':')
return true; // This source code contains ES module syntax.
prevPrevToken = prevToken;
prevToken = token;
}
} catch {
// If the tokenizer threw, there's a syntax error.
// Compile the script, this will throw with an informative error.
const vm = require('vm');
new vm.Script(source, { displayErrors: true });
}
// This source code does not contain ES module syntax.
// It may or may not be CommonJS, and it may or may not be valid syntax.
return false;
};

Module._initPaths = function() {
var homeDir;
var nodePath;
Expand Down
44 changes: 44 additions & 0 deletions test/es-module/test-esm-contains-module-syntax.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

require('../common');
const { strictEqual, fail } = require('assert');
const { readFileSync } = require('fs');

const { containsModuleSyntax } = require('module');

expect('esm-with-import-statement.js', 'module');
expect('esm-with-export-statement.js', 'module');
expect('esm-with-import-expression.js', 'module');
expect('esm-with-indented-import-statement.js', 'module');

expect('cjs-with-require.js', 'commonjs');
expect('cjs-with-import-expression.js', 'commonjs');
expect('cjs-with-property-named-import.js', 'commonjs');
expect('cjs-with-property-named-export.js', 'commonjs');
expect('cjs-with-string-containing-import.js', 'commonjs');

expect('print-version.js', 'commonjs');
expect('ambiguous-with-import-expression.js', 'commonjs');

expect('syntax-error.js', 'Invalid or unexpected token', true);

function expect(file, want, wantsError = false) {
const source = readFileSync(
require.resolve(`../fixtures/es-modules/detect/${file}`),
'utf8');
let isModule;
try {
isModule = containsModuleSyntax(source);
} catch (err) {
if (wantsError) {
return strictEqual(err.message, want);
} else {
return fail(
`Expected ${file} to throw '${want}'; received '${err.message}'`);
}
}
if (wantsError)
return fail(`Expected ${file} to throw '${want}'; no error was thrown`);
else
return strictEqual((isModule ? 'module' : 'commonjs'), want);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(async () => {
await import('./print-version.js');
})();
5 changes: 5 additions & 0 deletions test/fixtures/es-modules/detect/cjs-with-import-expression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { version } = require('process');

(async () => {
await import('./print-version.js');
})();
11 changes: 11 additions & 0 deletions test/fixtures/es-modules/detect/cjs-with-property-named-export.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// See ./cjs-with-property-named-import.js

global.export = 3;

global['export'] = 3;

const obj = {
export: 3 // Specifically at column 0, to try to trick the detector
}

console.log(require('process').version);
14 changes: 14 additions & 0 deletions test/fixtures/es-modules/detect/cjs-with-property-named-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// In JavaScript, reserved words cannot be identifiers (the `foo` in `var foo`)
// but they can be properties (`obj.foo`). This file checks that the `import`
// reserved word isn't incorrectly detected as a keyword. For more info see:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Reserved_word_usage

global.import = 3;

global['import'] = 3;

const obj = {
import: 3 // Specifically at column 0, to try to trick the detector
}

console.log(require('process').version);
3 changes: 3 additions & 0 deletions test/fixtures/es-modules/detect/cjs-with-require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { version } = require('process');

console.log(version);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { version } = require('process');

const sneakyString = `
import { version } from 'process';
`;

console.log(version);
6 changes: 6 additions & 0 deletions test/fixtures/es-modules/detect/esm-with-export-statement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const version = process.version;

export default version;

console.log(version);

5 changes: 5 additions & 0 deletions test/fixtures/es-modules/detect/esm-with-import-expression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { version } from 'process';

(async () => {
await import('./print-version.js');
})();
2 changes: 2 additions & 0 deletions test/fixtures/es-modules/detect/esm-with-import-statement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { version } from 'process';
console.log(version);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { version } from 'process';
console.log(version);
1 change: 1 addition & 0 deletions test/fixtures/es-modules/detect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions test/fixtures/es-modules/detect/print-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.version);
2 changes: 2 additions & 0 deletions test/fixtures/es-modules/detect/syntax-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const str = 'import
var foo = 3;

0 comments on commit 45954e1

Please sign in to comment.