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

Commit

Permalink
esm: add utility method for detecting ES module syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
GeoffreyBooth committed May 21, 2019
1 parent 030fa2e commit 1ffb68c
Show file tree
Hide file tree
Showing 16 changed files with 170 additions and 0 deletions.
24 changes: 24 additions & 0 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,29 @@ by the [module wrapper][]. To access it, require the `Module` module:
const builtin = require('module').builtinModules;
```

### module.containsModuleSyntax(source)
<!-- YAML
added: REPLACEME
-->

* `source` {string} JavaScript source code
* Returns: {boolean}

Detect whether input JavaScript source code contains [ECMAScript Module][]
syntax, defined as `import` or `export` statements. Returns `true` as soon as
the first `import` or `export` statement is encountered, or `false` if none are
found. Note that dynamic `import()` is not an `import` statement.

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

containsModuleSyntax('import { fn } from "pkg"'); // true
containsModuleSyntax('console.log(process.version)'); // false

containsModuleSyntax('import "./file.mjs"'); // true
containsModuleSyntax('import("./file.mjs")'); // false
```

### module.createRequire(filename)
<!-- YAML
added: v12.2.0
Expand Down Expand Up @@ -957,6 +980,7 @@ requireUtil('./some-tool');
[`createRequire()`]: #modules_module_createrequire_filename
[`module` object]: #modules_the_module_object
[`path.dirname()`]: path.html#path_path_dirname_path
[ECMAScript Module]: esm.html
[ECMAScript Modules]: esm.html
[an error]: errors.html#errors_err_require_esm
[exports shortcut]: #modules_exports_shortcut
Expand Down
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 whether input source code contains at least one `import` or `export`
// statement. This can be used by dependent utilities as a way of detecting ES
// module source code from Script/CommonJS source code. 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 1ffb68c

Please sign in to comment.