Skip to content
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
14 changes: 14 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,19 @@ added:

Enable exposition of [EventSource Web API][] on the global scope.

### `--experimental-ext=ext`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development

Overrides the default entrypoint file extension resolution with a custom file extension.
This is particularly useful for entrypoints that don't have a file extension.
The allowed values are `js`, `cjs`, `mjs`, `ts`, `cts`, `mts`.
The `ts` values are not available with the flag `--no-experimental-strip-types`.

### `--experimental-import-meta-resolve`

<!-- YAML
Expand Down Expand Up @@ -3449,6 +3462,7 @@ one is included in the list below.
* `--experimental-addon-modules`
* `--experimental-detect-module`
* `--experimental-eventsource`
* `--experimental-ext`
* `--experimental-import-meta-resolve`
* `--experimental-json-modules`
* `--experimental-loader`
Expand Down
3 changes: 3 additions & 0 deletions doc/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@
"experimental-eventsource": {
"type": "boolean"
},
"experimental-ext": {
"type": "string"
},
"experimental-global-navigator": {
"type": "boolean"
},
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ Enable module mocking in the test runner.
.It Fl -experimental-transform-types
Enable transformation of TypeScript-only syntax into JavaScript code.
.
.It Fl -experimental-ext
Override extension format for the entrypoint.
.
.It Fl -experimental-eventsource
Enable experimental support for the EventSource Web API.
.
Expand Down
99 changes: 70 additions & 29 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,13 +578,22 @@ function tryExtensions(basePath, exts, isMain) {
/**
* Find the longest (possibly multi-dot) extension registered in `Module._extensions`.
* @param {string} filename The filename to find the longest registered extension for.
* @param {boolean} isMain Whether the module is the main module.
* @returns {string}
*/
function findLongestRegisteredExtension(filename) {
function findLongestRegisteredExtension(filename, isMain = false) {
const name = path.basename(filename);
let currentExtension;
let index;
let startIndex = 0;
// We need to check that --experimental-ext is an entry point
// And doesn't already have a loader (example: .json or .node)
// If unknown let the .js loader handle it
const experimentalExtension = getOptionValue('--experimental-ext');
if (experimentalExtension && isMain && !Module._extensions[`.${experimentalExtension}`]) {
return '.js';
}

while ((index = StringPrototypeIndexOf(name, '.', startIndex)) !== -1) {
startIndex = index + 1;
if (index === 0) { continue; } // Skip dotfiles like .gitignore
Expand Down Expand Up @@ -1474,7 +1483,7 @@ Module.prototype.load = function(filename) {
this.filename ??= filename;
this.paths ??= Module._nodeModulePaths(path.dirname(filename));

const extension = findLongestRegisteredExtension(filename);
const extension = findLongestRegisteredExtension(filename, this[kIsMainSymbol]);

Module._extensions[extension](this, filename);
this.loaded = true;
Expand Down Expand Up @@ -1849,39 +1858,71 @@ function getRequireESMError(mod, pkg, content, filename) {
return err;
}

/**
* Get the module format for a given file extension.
* @param {string} filename The name of the file.
* @param {boolean} isMain Whether the module is the main module.
* @returns {object} An object containing the format and package information.
*/
function getModuleFormatForExtension(filename, isMain = false) {
let pkg;
const extensionMap = {
'.cjs': () => 'commonjs',
'.mjs': () => 'module',
'.js': (filename) => {
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
const typeFromPjson = pkg?.data.type;
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
return typeFromPjson;
}
},
'.mts': () => (getOptionValue('--experimental-strip-types') ? 'module-typescript' : undefined),
'.cts': () => (getOptionValue('--experimental-strip-types') ? 'commonjs-typescript' : undefined),
'.ts': (filename) => {
if (!getOptionValue('--experimental-strip-types')) { return; }
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
switch (pkg?.data.type) {
case 'module':
return 'module-typescript';
case 'commonjs':
return 'commonjs-typescript';
default:
return 'typescript';
}
},
};

// Allow experimental extension via --experimental-ext
const experimentalExtension = getOptionValue('--experimental-ext');
if (experimentalExtension) {
emitExperimentalWarning('--experimental-ext');
if (isMain) {
const format = extensionMap[`.${experimentalExtension}`](filename);
return { format, pkg };
}
}

const keys = ObjectKeys(extensionMap);
for (let i = 0; i < keys.length; i++) {
const ext = keys[i];
if (StringPrototypeEndsWith(filename, ext)) {
const format = extensionMap[ext](filename);
return { format, pkg };
}
}
// Unknown extension, return undefined to let loadSource handle it.
return { pkg, format: undefined };
}

/**
* Built-in handler for `.js` files.
* @param {Module} module The module to compile
* @param {string} filename The file path of the module
*/
Module._extensions['.js'] = function(module, filename) {
let format, pkg;
const tsEnabled = getOptionValue('--experimental-strip-types');
if (StringPrototypeEndsWith(filename, '.cjs')) {
format = 'commonjs';
} else if (StringPrototypeEndsWith(filename, '.mjs')) {
format = 'module';
} else if (StringPrototypeEndsWith(filename, '.js')) {
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
const typeFromPjson = pkg?.data.type;
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
format = typeFromPjson;
}
} else if (StringPrototypeEndsWith(filename, '.mts') && tsEnabled) {
format = 'module-typescript';
} else if (StringPrototypeEndsWith(filename, '.cts') && tsEnabled) {
format = 'commonjs-typescript';
} else if (StringPrototypeEndsWith(filename, '.ts') && tsEnabled) {
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
const typeFromPjson = pkg?.data.type;
if (typeFromPjson === 'module') {
format = 'module-typescript';
} else if (typeFromPjson === 'commonjs') {
format = 'commonjs-typescript';
} else {
format = 'typescript';
}
}
// If the file is extensionless, or the extension is unknown
// it will fall into this handler.
const { format, pkg } = getModuleFormatForExtension(filename, module[kIsMainSymbol]);
const { source, format: loadedFormat } = loadSource(module, filename, format);
// Function require shouldn't be used in ES modules when require(esm) is disabled.
if ((loadedFormat === 'module' || loadedFormat === 'module-typescript') &&
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ function warnTypelessPackageJsonFile(pjsonPath, url) {
*/
function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreErrors) {
const { source } = context;
const ext = extname(url);
const experimentalExtension = getOptionValue('--experimental-ext');
const isMain = context.parentURL === undefined; // If it has a parent is not an entrypoint
const ext = experimentalExtension && isMain ? `.${experimentalExtension}` : extname(url);

if (ext === '.js') {
const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url);
Expand Down
4 changes: 3 additions & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const {
resolveForCJSWithHooks,
loadSourceForCJSWithHooks,
populateCJSExportsFromESM,
kIsMainSymbol,
} = require('internal/modules/cjs/loader');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
Expand Down Expand Up @@ -427,7 +428,8 @@ function cjsPreparseModuleExports(filename, source, format) {
}

if (format === 'commonjs' ||
(!BuiltinModule.normalizeRequirableId(resolved) && findLongestRegisteredExtension(resolved) === '.js')) {
(!BuiltinModule.normalizeRequirableId(resolved) &&
findLongestRegisteredExtension(resolved, module[kIsMainSymbol]) === '.js')) {
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, format);
for (const name of reexportNames) {
exportNames.add(name);
Expand Down
31 changes: 22 additions & 9 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { getNearestParentPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const path = require('path');
const { pathToFileURL, URL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
const { emitExperimentalWarning, kEmptyObject, getCWDURL } = require('internal/util');
const {
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
Expand Down Expand Up @@ -62,17 +62,30 @@ function shouldUseESMLoader(mainPath) {
const userImports = getOptionValue('--import');
if (userLoaders.length > 0 || userImports.length > 0) { return true; }

// Determine the module format of the entry point.
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (mainPath && StringPrototypeEndsWith(mainPath, '.wasm')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
let ext;
const experimentalExtension = getOptionValue('--experimental-ext');
if (experimentalExtension) {
emitExperimentalWarning('--experimental-ext');
ext = `.${experimentalExtension}`;
} else if (mainPath) {
const candidates = ['.mjs', '.wasm', '.cjs', '.cts', '.mts'];
for (let i = 0; i < candidates.length; i++) {
if (StringPrototypeEndsWith(mainPath, candidates[i])) {
ext = candidates[i];
break;
}
}

if (getOptionValue('--experimental-strip-types')) {
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cts')) { return false; }
// This will likely change in the future to start with commonjs loader by default
if (mainPath && StringPrototypeEndsWith(mainPath, '.mts')) { return true; }
if (ext === '.mjs' || ext === '.wasm') { return true; }
if (ext === '.cjs') { return false; }
if (getOptionValue('--experimental-strip-types')) {
// This will likely change in the future to start with commonjs loader by default
if (ext === '.mts') { return true; }
}
}

if (!mainPath || ext === '.cts') { return false; }

const type = getNearestParentPackageJSONType(mainPath);

// No package.json or no `type` field.
Expand Down
14 changes: 14 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
}
}

if (!experimental_ext.empty()) {
if (experimental_ext != "js" && experimental_ext != "mjs" &&
experimental_ext != "cjs" && experimental_ext != "ts" &&
experimental_ext != "cts" && experimental_ext != "mts") {
errors->push_back(
"--experimental-ext must be \"js\", \"mjs\", \"cjs\", \"ts\", "
"\"cts\" or \"mts\"");
}
}

if (syntax_check_only && has_eval_string) {
errors->push_back("either --check or --eval can be used, not both");
}
Expand Down Expand Up @@ -1051,6 +1061,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"ES module to preload (option can be repeated)",
&EnvironmentOptions::preload_esm_modules,
kAllowedInEnvvar);
AddOption("--experimental-ext",
"Override extension format on entrypoint",
&EnvironmentOptions::experimental_ext,
kAllowedInEnvvar);
AddOption("--experimental-strip-types",
"Experimental type-stripping for TypeScript files.",
&EnvironmentOptions::experimental_strip_types,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ class EnvironmentOptions : public Options {

bool experimental_strip_types = true;
bool experimental_transform_types = false;
std::string experimental_ext; // Value of --experimental-ext

std::vector<std::string> user_argv;

Expand Down
Loading
Loading