Skip to content
This repository was archived by the owner on Apr 16, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ For completeness there is also `--input-type=commonjs`, for explicitly running
string input as CommonJS. This is the default behavior if `--input-type` is
unspecified.

There is also `--input-type=auto`, which configures Node.js to interpret string
input as an ES module if Node.js finds an `import` or `export` statement in the
source code. (Note that dynamic `import()` expressions are different from
`import` statements; `import()` is allowed in both CommonJS and ES modules.) If
no `import` or `export` statements are found, the string is interpreted as
CommonJS.

## Package Entry Points

The `package.json` `"main"` field defines the entry point for a package,
Expand Down
5 changes: 4 additions & 1 deletion lib/internal/main/check_syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ function checkSyntax(source, filename) {
if (experimentalModules) {
let isModule = false;
if (filename === '[stdin]' || filename === '[eval]') {
isModule = getOptionValue('--input-type') === 'module';
const typeFlag = getOptionValue('--input-type');
isModule = typeFlag === 'module' || (typeFlag === 'auto' &&
require('internal/modules/esm/detect_type')(source, filename) ===
'module');
} else {
const resolve = require('internal/modules/esm/default_resolve');
const { format } = resolve(pathToFileURL(filename).toString());
Expand Down
7 changes: 6 additions & 1 deletion lib/internal/main/eval_stdin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const {
prepareMainThreadExecution
} = require('internal/bootstrap/pre_execution');

const { getOptionValue } = require('internal/options');

const {
evalModule,
evalScript,
Expand All @@ -17,7 +19,10 @@ markBootstrapComplete();

readStdin((code) => {
process._eval = code;
if (require('internal/options').getOptionValue('--input-type') === 'module')
const typeFlag = getOptionValue('--input-type');
if (typeFlag === 'module' ||
(typeFlag === 'auto' &&
require('internal/modules/esm/detect_type')(code, '[stdin]') === 'module'))
evalModule(process._eval);
else
evalScript('[stdin]', process._eval, process._breakFirstLine);
Expand Down
6 changes: 5 additions & 1 deletion lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ const source = getOptionValue('--eval');
prepareMainThreadExecution();
addBuiltinLibsToObject(global);
markBootstrapComplete();
if (getOptionValue('--input-type') === 'module')

const typeFlag = getOptionValue('--input-type');
if (typeFlag === 'module' ||
(typeFlag === 'auto' &&
require('internal/modules/esm/detect_type')(source, '[eval]') === 'module'))
evalModule(source);
else
evalScript('[eval]', source, process._breakFirstLine);
47 changes: 47 additions & 0 deletions lib/internal/modules/esm/detect_type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

const acorn = require('internal/deps/acorn/acorn/dist/acorn');
const {
stripShebang,
stripBOM,
} = require('internal/modules/cjs/helpers');

// Detect the module type of a file: CommonJS or ES module.
// An ES module, for the purposes of this algorithm, is defined as any
// JavaScript file 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.
function detectType(source, filename) {
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/input-type-auto-scope/
// cjs-with-property-named-import.js
!(prevPrevToken && prevPrevToken.label === '.') &&
token.label !== ':')
return 'module';
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, filename });
}
return 'commonjs';
}

module.exports = detectType;
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
'lib/internal/modules/esm/loader.js',
'lib/internal/modules/esm/create_dynamic_module.js',
'lib/internal/modules/esm/default_resolve.js',
'lib/internal/modules/esm/detect_type.js',
'lib/internal/modules/esm/module_job.js',
'lib/internal/modules/esm/module_map.js',
'lib/internal/modules/esm/translators.js',
Expand Down
6 changes: 4 additions & 2 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
errors->push_back("--input-type requires "
"--experimental-modules to be enabled");
}
if (module_type != "commonjs" && module_type != "module") {
errors->push_back("--input-type must be \"module\" or \"commonjs\"");
if (module_type != "commonjs" && module_type != "module" &&
module_type != "auto") {
errors->push_back("--input-type must be "
"\"module\" or \"commonjs\" or \"auto\"");
}
}

Expand Down
52 changes: 52 additions & 0 deletions test/es-module/test-esm-input-type-auto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const exec = require('child_process').execFile;
const { readFileSync } = require('fs');

const version = process.version;

expect('esm-with-import-statement.js', version);
expect('esm-with-export-statement.js', version);
// TODO: Enable import expression tests once import() is supported
// in string input.
// expect('esm-with-import-expression.js', version);
expect('esm-with-indented-import-statement.js', version);

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

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

expect('syntax-error-1.js', 'SyntaxError', true);
expect('syntax-error-2.js', 'SyntaxError', true);

function expect(file, want, wantsError = false) {
const argv = [
'--eval',
readFileSync(
require.resolve(`../fixtures/es-modules/input-type-auto-scope/${file}`),
'utf8')
];
const opts = {
// TODO: Remove when --experimental-modules is unflagged
env: { ...process.env,
NODE_OPTIONS: '--experimental-modules --input-type=auto' },
maxBuffer: 1e6,
};
exec(process.execPath, argv, opts,
common.mustCall((err, stdout, stderr) => {
if (wantsError) {
stdout = stderr;
} else {
assert.ifError(err);
}
if (stdout.includes(want)) return;
assert.fail(
`For ${file}, failed to find ${want} in: <\n${stdout}\n>`);
}));
}
2 changes: 1 addition & 1 deletion test/es-module/test-esm-type-flag-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function expect(opt = '', inputFile, want, wantsError = false) {
opt = `--experimental-modules ${opt}`;
const argv = [inputFile];
const opts = {
env: Object.assign({}, process.env, { NODE_OPTIONS: opt }),
env: { ...process.env, NODE_OPTIONS: opt },
maxBuffer: 1e6,
};
exec(process.execPath, argv, opts, common.mustCall((err, stdout, stderr) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(async () => {
await import('./print-version.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');
})();
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);
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);
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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const version = process.version;

export default version;

console.log(version);

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { version } from 'process';

(async () => {
await import('./print-version.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);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log(process.version);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const str = 'import
var foo = 3;