-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
make experimental-specifier-resolution=node support extension-less files #41465
Comments
@lachrist, thanks for opening this issue. This was implemented before my time, but wonder if this may actually be a regression or what seeing as how one of the main reasons for having this flag in the first place was to support Node's original resolution algorithm for extensionless files in ESM contexts according to our documentation for that CLI flag.
/cc @nodejs/modules @nodejs/loaders |
The original commit that implemented this flag is b1094db, which show that this is not a regression, but has been this way the whole time. We may need to discuss whether this flag was ever intended to remain for as long as it has. Judging by the commit message, it does not seem like that was the case. I personally never use it as using a custom loader has always been my preference, but understand that there are those out there not interested in introducing the complexity necessary to achieve the same result using a custom module loader… /cc @GeoffreyBooth |
The original intention was to gather feedback, and consider making it the default implementation. However, there was never any clear rubric given for what would constitute "enough" feedback, so the likelihood the flagged behavior will ever become the default has rapidly dwindled. At this point, it's more likely to be removed entirely, I suspect. |
On the contrary, nodejs/loaders#26 is about providing support to make it easy to reimplement this flag as a custom loader. |
Thank you all for your feedback! :) Maybe I put too much emphasis on the // bin
import("./dep.mjs"); // dep.mjs
console.log("dep"); // loader.mjs
export const load = (url, context, loadDefaultAsync) => {
console.log(`loading ${url}`);
return loadDefaultAsync(url, context, loadDefaultAsync);
}; node --loader ./loader.mjs bin TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension "" for ./bin |
On a related node, it might be useful to state in the doc that the use of node/lib/internal/modules/run_main.js Line 27 in 8c3637c
|
If the issue is just for "bin" files, this is a duplicate of #41275 and #33226. For the previous issue, I did a quick workaround for this (testing with mocha actually), checking if the parent directory is "bin", and then returning Are all "bin" files commonjs? |
Not necessarily bin files, any entry point (aka main files) which does not have an extension. To answer your other question, bin files can be esm modules but it requires an extension: |
I would guess we would explicitly/automatically support "bin" files as that is a concept within Nodejs (but it has just been missed so far). For other extension-less entry point files, I think that would likely be out of scope for Nodejs core. There is discussion about exposing some of Nodejs's internal utilities related to this so users can do determine what they need as if in Nodejs core. One of those is a export async function load(url, context, defaultLoad) {
const format = getPackageFormat(url);
return defaultLoad(url, { ...context, format }); |
@JakobJingleheimer, thanks for pointing me to these issues which are indeed related. And indeed this issue seem to present itself the most with bin files. If I may, here is what I think is confusing with the current behavior of the loader. In general it is pretty easy to know which module loader will be invoked: If I were to wild guess. I would say that the best solution would be to always use the |
Duplicate of #38933. |
@lachrist a) the CJS loader isn’t legacy, and b) when type:module is used, it’s not so simple, and more cases than you might expect go to the ESM loader. |
We actually did support extension-less files in the ESM loader at one time, this was ripped out due to forwards compatibility with WASM not backwards compatibility. |
Hi @ljharb I did not want to sound like the node team did something wrong. And I understand that there might be no easy fix for this. However the issue remains that this function shouldUseESMLoader directly affects the user, and is not mentioned in the module doc. |
Yes, the decision was made to specifically not support ESM extensionless entry files because doing to would lock in their status as needing to be JavaScript, while we want to preserve the ability of Wasm extensionless entries in the future. |
FWIW I'm adding this to the documentation in #41383. |
Thank you all for the follow up. It was very instructive. If someone stumble upon this issue and is wondering how to monitor node's module loading here is my solution. ProblemAt the moment, monitoring ECMAScript modules requires SolutionAdd some logic to the loader to check for deviation: let is_main = true;
export const load = (url, context, loadNext) => {
if (is_main) {
is_main = false;
if (context.format === undefined) {
context.format = "commonjs";
}
}
return loadNext(url, context, loadNext);
}; Deprecated SolutionMonkey-patch the loading of the main file in a commonjs pre-loaded file. With something like this: const Module = require("module");
const { realpathSync } = require("fs");
Module.runMain = (main = process.argv[1]) =>
Module._load(realpathSync(main), null, true); This simple solution assumes that the main file is a commonjs module, wich is not always the case. Also, it will not apply monkey-patches previously added to const { copyFileSync, readFileSync, realpathSync, constants } = require("fs");
const { extname, dirname, basename, join } = require("path");
const Module = require("module");
const { runMain } = Module;
Module.runMain = function executeUserEntryPoint(main = process.argv[1]) {
// TODO support --preserve-symlinks-main
let path = realpathSync(main);
const ext = extname(path);
if (ext === "" || ext === ".node") {
const new_path = join(dirname(path), `${basename(path, ext)}.cjs`);
try {
copyFileSync(path, new_path, constants.COPYFILE_EXCL);
} catch (error) {
if (error.code !== "ENOTSUP") {
throw error;
}
if (readFileSync(path, "uft8") !== readFileSync(new_path, "utf8")) {
throw new Error("Cannot solve extension-less main file issue");
}
}
path = new_path;
}
return runMain(path);
}; |
Instead of rewritting main files we simply add some logic in then loader -- cf: nodejs/node#41465 A la tchusss abomination
Instead of rewritting main files we simply add some logic in then loader -- cf: nodejs/node#41465 A la tchusss abomination
Instead of rewritting main files we simply add some logic in then loader -- cf: nodejs/node#41465 A la tchusss abomination
npx tsx solves everything, a.k.a. no config ts-node. No more stupid esm to commonjs conversion, and .ts EXTENSION NOT FOUND error anymore. |
What is the problem this feature will solve?
I thought that the
--experimental-specifier-resolution=node
CLI option was made to emulate legacy cjs resolution algorithm. However the legacy cjs module loader can handle extension-less files whereas which is not the case with this CLI option:> cat yo console.log("yo"); > node yo yo > node --experimental-specifier-resolution=node yo (node:33081) ExperimentalWarning: The Node.js specifier resolution in ESM is experimental. (Use `node --trace-warnings ...` to show where the warning was created) node:internal/errors:464 ErrorCaptureStackTrace(err); ^ TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module "file:///Users/soft/Desktop/workspace/appmap-agent-js/yo" at new NodeError (node:internal/errors:371:5) at ESMLoader.load (node:internal/modules/esm/loader:380:13) at async ESMLoader.moduleProvider (node:internal/modules/esm/loader:280:47) at async link (node:internal/modules/esm/module_job:70:21) { code: 'ERR_INVALID_MODULE_SPECIFIER' } Node.js v17.3.0
These extension-less files are actually common in the
bin
directory of many popular projects -- eg: mocha. This makes it hard to use--experimental-loader
on these projects because the below command fails for the same reason as above:NODE_OPTIONS="--experimental-loader=./loader.mjs --experimental-specifier-resolution=node" npx mocha
What is the feature you are proposing to solve the problem?
Consider extension-less files as commonjs modules as the legacy cjs loader does. This is can done be done easily by modifying
legacyExtensionFormatMap
in lib/internal/modules/esm/get_format.js:Is there a good reason why this has not been considered?
What alternatives have you considered?
--experimental-loader
and do not pass it to the default loader. This is a bazooka solution because the loader API basically enables to write one's own module resolution algorithm. We don't want to emulate the entire module resolution algorithm for such a minimal change..js
extension. This must happen in a preloaded module. It is extremely hacky and does not work on windows.The text was updated successfully, but these errors were encountered: