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

-m alias; determine type based for symlinks based on targets; error on mismatched type flag and package scope #45

Merged
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = {
files: [
'doc/api/esm.md',
'test/es-module/test-esm-type-flag.js',
'test/es-module/test-esm-type-flag-alias.js',
'*.mjs',
],
parserOptions: { sourceType: 'module' },
Expand Down
16 changes: 13 additions & 3 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1270,6 +1270,11 @@ An invalid or unexpected value was passed in an options object.

An invalid or unknown file encoding was passed.

<a id="ERR_INVALID_PACKAGE_CONFIG"></a>
### ERR_INVALID_PACKAGE_CONFIG

An invalid `package.json` file was found which failed parsing.

<a id="ERR_INVALID_PERFORMANCE_MARK"></a>
### ERR_INVALID_PERFORMANCE_MARK

Expand Down Expand Up @@ -2213,12 +2218,17 @@ while trying to read and parse it.

The `--type=...` flag is not compatible with the Node.js REPL.

<a id="ERR_INVALID_TYPE_EXTENSION"></a>
### ERR_INVALID_TYPE_EXTENSION
<a id="ERR_TYPE_MISMATCH"></a>
### ERR_TYPE_MISMATCH

> Stability: 1 - Experimental

Attempted to execute a `.cjs` module with the `--type=module` flag.
The `--type=commonjs` flag was used to attempt to execute an `.mjs` file or
a `.js` file where the nearest parent `package.json` contains
`"type": "module"`; or
the `--type=module` flag was used to attempt to execute a `.cjs` file or
a `.js` file where the nearest parent `package.json` either lacks a `"type"`
field or contains `"type": "commonjs"`.

<a id="ERR_INVALID_TYPE_FLAG"></a>
### ERR_INVALID_TYPE_FLAG
Expand Down
14 changes: 8 additions & 6 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ as described here, with the conditional `--type` check in **ESM_FORMAT**.
> **PACKAGE_RESOLVE**(_specifier_, _parentURL_).
> 1. If the file at _resolvedURL_ does not exist, then
> 1. Throw a _Module Not Found_ error.
> 1. Let _format_ be the result of **ESM_FORMAT**(_url_, _isMain_).
> 1. Set _resolvedURL_ to the real path of _resolvedURL_.
> 1. Let _format_ be the result of **ESM_FORMAT**(_resolvedURL_, _isMain_).
> 1. Load _resolvedURL_ as module format, _format_.

PACKAGE_RESOLVE(_packageSpecifier_, _parentURL_)
Expand Down Expand Up @@ -253,22 +254,23 @@ PACKAGE_MAIN_RESOLVE(_packageURL_, _pjson_)
> 1. Assert: _url_ corresponds to an existing file.
> 1. If _isMain_ is **true** and the `--type` flag is _"module"_, then
> 1. If _url_ ends with _".cjs"_, then
> 1. Throw an _Invalid File Extension_ error.
> 1. Throw a _Type Mismatch_ error.
> 1. Return _"module"_.
> 1. Let _pjson_ be the result of **READ_PACKAGE_BOUNDARY**(_url_).
> 1. If _pjson_ is **null** and _isMain_ is **true**, then
> 1. If _url_ ends in _".mjs"_, then
> 1. Return _"module"_.
> 1. Return _"commonjs"_.
> 1. If _pjson.type_ exists and is _"module"_, then
> 1. If _url_ ends in _".cjs"_, then
> 1. Return _"commonjs"_.
> 1. If _url_ does not end in _".js"_ or _".mjs"_, then
> 1. Throw an _Unsupported File Extension_ error.
> 1. Return _"module"_.
> 1. Otherwise,
> 1. If _url_ ends in _".mjs"_, then
> 1. Return _"module"_.
> 1. Otherwise,
> 1. Return _"commonjs"_.
> 1. If _url_ does not end in _".js"_, then
> 1. Throw an _Unsupported File Extension_ error.
> 1. Return _"commonjs"_.

READ_PACKAGE_BOUNDARY(_url_)
> 1. Let _boundaryURL_ be _url_.
Expand Down
20 changes: 17 additions & 3 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,8 @@ E('ERR_INVALID_OPT_VALUE', (name, value) =>
RangeError);
E('ERR_INVALID_OPT_VALUE_ENCODING',
'The value "%s" is invalid for option "encoding"', TypeError);
E('ERR_INVALID_PACKAGE_CONFIG',
'Invalid package config in \'%s\' imported from %s', Error);
E('ERR_INVALID_PERFORMANCE_MARK',
'The "%s" performance mark has not been set', Error);
E('ERR_INVALID_PROTOCOL',
Expand Down Expand Up @@ -813,10 +815,8 @@ E('ERR_INVALID_SYNC_FORK_INPUT',
TypeError);
E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError);
E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError);
E('ERR_INVALID_TYPE_EXTENSION', '%s extension is not supported for --type=%s',
TypeError);
E('ERR_INVALID_TYPE_FLAG',
'Type flag must be one of "esm", "commonjs". Received --type=%s',
'Type flag must be one of "module", "commonjs". Received --type=%s',
TypeError);
E('ERR_INVALID_URI', 'URI malformed', URIError);
E('ERR_INVALID_URL', 'Invalid URL: %s', TypeError);
Expand Down Expand Up @@ -956,6 +956,20 @@ E('ERR_TRANSFORM_ALREADY_TRANSFORMING',
E('ERR_TRANSFORM_WITH_LENGTH_0',
'Calling transform done when writableState.length != 0', Error);
E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError);
E('ERR_TYPE_MISMATCH', (filename, ext, typeFlag, conflict) => {
const typeString =
typeFlag === 'module' ? '--type=module or -m' : '--type=commonjs';
// --type mismatches file extension
if (conflict === 'extension')
return `Extension ${ext} is not supported for ` +
`${typeString} loading ${filename}`;
// --type mismatches package.json "type"
else if (conflict === 'scope')
return `Cannot use ${typeString} because nearest parent package.json ` +
((typeFlag === 'module') ?
'includes "type": "commonjs"' : 'includes "type": "module",') +
` which controls the type to use for ${filename}`;
}, TypeError);
E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET',
'`process.setupUncaughtExceptionCapture()` was called while a capture ' +
'callback was already active',
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/main/check_syntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function checkSyntax(source, filename) {
} else {
const resolve = require('internal/modules/esm/default_resolve');
const { format } = resolve(pathToFileURL(filename).toString());
isModule = format === 'esm';
isModule = format === 'module';
}
if (isModule) {
const { ModuleWrap } = internalBinding('module_wrap');
Expand Down
116 changes: 87 additions & 29 deletions lib/internal/modules/esm/default_resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,106 @@
const internalFS = require('internal/fs/utils');
const { NativeModule } = require('internal/bootstrap/loaders');
const { extname } = require('path');
const { realpathSync } = require('fs');
const { realpathSync, readFileSync } = require('fs');
const { getOptionValue } = require('internal/options');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
const { ERR_INVALID_PACKAGE_CONFIG,
ERR_TYPE_MISMATCH,
ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
const { pathToFileURL, fileURLToPath } = require('internal/url');
const { typeFlag } = require('internal/process/esm_loader');
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
const asyncESM = require('internal/process/esm_loader');

const realpathCache = new Map();
// TOOD(@guybedford): Shared cache with C++
const pjsonCache = new Map();

const extensionFormatMap = {
'__proto__': null,
'.cjs': 'cjs',
'.js': 'esm',
'.mjs': 'esm'
'.cjs': 'commonjs',
'.js': 'module',
'.mjs': 'module'
};

const legacyExtensionFormatMap = {
'__proto__': null,
'.cjs': 'cjs',
'.js': 'cjs',
'.json': 'cjs',
'.mjs': 'esm',
'.node': 'cjs'
'.cjs': 'commonjs',
'.js': 'commonjs',
'.json': 'commonjs',
'.mjs': 'module',
'.node': 'commonjs'
};

function readPackageConfig(path, parentURL) {
const existing = pjsonCache.get(path);
if (existing !== undefined)
return existing;
try {
return JSON.parse(readFileSync(path).toString());
} catch (e) {
if (e.code === 'ENOENT') {
pjsonCache.set(path, null);
return null;
} else if (e instanceof SyntaxError) {
throw new ERR_INVALID_PACKAGE_CONFIG(path, fileURLToPath(parentURL));
}
throw e;
}
}

function getPackageBoundaryConfig(url, parentURL) {
let pjsonURL = new URL('package.json', url);
while (true) {
const pcfg = readPackageConfig(fileURLToPath(pjsonURL), parentURL);
if (pcfg)
return pcfg;

const lastPjsonURL = pjsonURL;
pjsonURL = new URL('../package.json', pjsonURL);

// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (pjsonURL.pathname === lastPjsonURL.pathname)
return;
}
}

function getModuleFormat(url, isMain, parentURL) {
const pcfg = getPackageBoundaryConfig(url, parentURL);

const legacy = !pcfg || pcfg.type !== 'module';

const ext = extname(url.pathname);

let format = (legacy ? legacyExtensionFormatMap : extensionFormatMap)[ext];

if (!format) {
if (isMain)
format = legacy ? 'commonjs' : 'module';
else
throw new ERR_UNKNOWN_FILE_EXTENSION(fileURLToPath(url),
fileURLToPath(parentURL));
}

// Check for mismatch between --type and file extension,
// and between --type and the "type" field in package.json.
if (isMain && format !== 'module' && asyncESM.typeFlag === 'module') {
// Conflict between package scope type and --type
if (ext === '.js') {
if (pcfg && pcfg.type)
throw new ERR_TYPE_MISMATCH(
fileURLToPath(url), ext, asyncESM.typeFlag, 'scope');
// Conflict between explicit extension (.mjs, .cjs) and --type
} else {
throw new ERR_TYPE_MISMATCH(
fileURLToPath(url), ext, asyncESM.typeFlag, 'extension');
}
}

return format;
}

function resolve(specifier, parentURL) {
if (NativeModule.canBeRequiredByUsers(specifier)) {
return {
Expand All @@ -42,13 +115,7 @@ function resolve(specifier, parentURL) {
if (isMain)
parentURL = pathToFileURL(`${process.cwd()}/`).href;

const resolved = moduleWrapResolve(specifier,
parentURL,
isMain,
typeFlag === 'module');

let url = resolved.url;
const legacy = resolved.legacy;
let url = moduleWrapResolve(specifier, parentURL);

if (isMain ? !preserveSymlinksMain : !preserveSymlinks) {
const real = realpathSync(fileURLToPath(url), {
Expand All @@ -60,16 +127,7 @@ function resolve(specifier, parentURL) {
url.hash = old.hash;
}

const ext = extname(url.pathname);
let format = (legacy ? legacyExtensionFormatMap : extensionFormatMap)[ext];

if (!format) {
if (isMain)
format = legacy ? 'cjs' : 'esm';
else
throw new ERR_UNKNOWN_FILE_EXTENSION(fileURLToPath(url),
fileURLToPath(parentURL));
}
const format = getModuleFormat(url, isMain, parentURL);

return { url: `${url}`, format };
}
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function importModuleDynamically(specifier, { url }) {
}

// Strategy for loading a standard JavaScript module
translators.set('esm', async function(url) {
translators.set('module', async function(url) {
const source = `${await readFileAsync(new URL(url))}`;
debug(`Translating StandardModule ${url}`);
const module = new ModuleWrap(stripShebang(source), url);
Expand All @@ -52,7 +52,7 @@ translators.set('esm', async function(url) {
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('cjs', async function(url, isMain) {
translators.set('commonjs', async function(url, isMain) {
debug(`Translating CJSModule ${url}`);
const pathname = internalURLModule.fileURLToPath(new URL(url));
const cached = this.cjsCache.get(url);
Expand Down
Loading