Skip to content
This repository was archived by the owner on Apr 16, 2020. It is now read-only.
Closed
20 changes: 15 additions & 5 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ conjunction with native stack and other runtime environment data.
added: v6.0.0
-->

Enable FIPS-compliant crypto at startup. (Requires Node.js to be built with
`./configure --openssl-fips`.)

### `--entry-type=type`
<!-- YAML
added: REPLACEME
Expand All @@ -139,13 +142,20 @@ added: REPLACEME
Used with `--experimental-modules`, this configures Node.js to interpret the
initial entry point as CommonJS or as an ES module.

Valid values are `"commonjs"` and `"module"`. The default is to infer from
the file extension and the `"type"` field in the nearest parent `package.json`.
Valid values are `"commonjs"`, `"module"` or `"auto"`. The default is to infer
from the file extension and the `"type"` field in the nearest parent
`package.json`.

Works for executing a file as well as `--eval`, `--print`, `STDIN`.
`--entry-type=auto` configures Node.js to interpret an _ambiguous_ initial entry
point (no explicit file extension like `.mjs` or `.cjs`, no `"type"` field in
the nearest parent `package.json`) as an ES module if Node.js finds an `import`
or `export` statement in the initial entry point's 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 initial entry point is interpreted as
CommonJS.

Enable FIPS-compliant crypto at startup. (Requires Node.js to be built with
`./configure --openssl-fips`.)
Works for executing a file as well as `--eval`, `--print`, `STDIN`.

### `--es-module-specifier-resolution=mode`
<!-- YAML
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 @@ -61,7 +61,10 @@ function checkSyntax(source, filename) {
if (experimentalModules) {
let isModule = false;
if (filename === '[stdin]' || filename === '[eval]') {
isModule = getOptionValue('--entry-type') === 'module';
const typeFlag = getOptionValue('--entry-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('--entry-type') === 'module')
const typeFlag = getOptionValue('--entry-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('--entry-type') === 'module')

const typeFlag = getOptionValue('--entry-type');
if (typeFlag === 'module' ||
(typeFlag === 'auto' &&
require('internal/modules/esm/detect_type')(source, '[eval]') === 'module'))
evalModule(source);
else
evalScript('[eval]', source, process._breakFirstLine);
8 changes: 7 additions & 1 deletion lib/internal/modules/esm/default_resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const internalFS = require('internal/fs/utils');
const { NativeModule } = require('internal/bootstrap/loaders');
const { extname } = require('path');
const { realpathSync } = require('fs');
const { readFileSync, realpathSync } = require('fs');
const { getOptionValue } = require('internal/options');

const preserveSymlinks = getOptionValue('--preserve-symlinks');
Expand Down Expand Up @@ -102,6 +102,12 @@ function resolve(specifier, parentURL) {
}
}
}
// --entry-type=auto detects an ESM .js file within a CommonJS scope.
if (isMain && format === 'commonjs' && typeFlag === 'auto') {
const filename = fileURLToPath(url);
const source = readFileSync(filename, 'utf8');
format = require('internal/modules/esm/detect_type')(source, filename);
}
if (!format) {
if (isMain && typeFlag)
format = typeFlag;
Expand Down
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/entry-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("--entry-type requires "
"--experimental-modules to be enabled");
}
if (module_type != "commonjs" && module_type != "module") {
errors->push_back("--entry-type must \"module\" or \"commonjs\"");
if (module_type != "commonjs" && module_type != "module" &&
module_type != "auto") {
errors->push_back("--entry-type must be "
"\"module\" or \"commonjs\" or \"auto\"");
}
}

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

const version = process.version;

expect('esm-with-import-statement.js', version);
expect('esm-with-export-statement.js', version);
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 = [
require.resolve(`../fixtures/es-modules/entry-type-auto-scope/${file}`)
];
const opts = {
// TODO: Remove when --experimental-modules is unflagged
env: { ...process.env,
NODE_OPTIONS: '--experimental-modules --entry-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 @@ -36,7 +36,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;