Skip to content

docs: module resolution pseudocode corrections #57080

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

Merged
merged 3 commits into from
Mar 22, 2025

Conversation

laverdet
Copy link
Contributor

@laverdet laverdet commented Feb 15, 2025

I found two three errors in the pseudocode for the resolution algorithm. Permalinks to the implementations which show the correct behavior:

let packageJSONUrl = new URL(`./node_modules/${packageName}/package.json`, base);

if (ObjectPrototypeHasOwnProperty(exports, packageSubpath) &&
!StringPrototypeIncludes(packageSubpath, '*') &&
!StringPrototypeEndsWith(packageSubpath, '/')) {
const target = exports[packageSubpath];
const resolveResult = resolvePackageTarget(
packageJSONUrl, target, '', packageSubpath, base, false, false, false,
conditions,
);

function isRelative(path) {
if (StringPrototypeCharCodeAt(path, 0) !== CHAR_DOT) { return false; }
return path.length === 1 || path === '..' ||
StringPrototypeStartsWith(path, './') ||
StringPrototypeStartsWith(path, '../') ||
((isWindows && StringPrototypeStartsWith(path, '.\\')) ||
StringPrototypeStartsWith(path, '..\\'));
}

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added doc Issues and PRs related to the documentations. esm Issues and PRs related to the ECMAScript Modules implementation. labels Feb 15, 2025
@laverdet
Copy link
Contributor Author

Added another correction to the CJS docs. Without this fix require(".") does not work but in reality it is equivalent to require("./index.js") || require("./index.json") || require("./index.node") (sans exceptions obviously).

See:

function isRelative(path) {
if (StringPrototypeCharCodeAt(path, 0) !== CHAR_DOT) { return false; }
return path.length === 1 || path === '..' ||
StringPrototypeStartsWith(path, './') ||
StringPrototypeStartsWith(path, '../') ||
((isWindows && StringPrototypeStartsWith(path, '.\\')) ||
StringPrototypeStartsWith(path, '..\\'));
}

@lpinca
Copy link
Member

lpinca commented Feb 18, 2025

This needs a rebase to resolve conflicts.

@aduh95 aduh95 requested a review from guybedford February 19, 2025 09:27
@laverdet
Copy link
Contributor Author

Rebased and tweaked the quad-"or" sentence a bit.

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful work here! Would be great to get these in, down to some refinement that is probably needed for trailing slash handling.

doc/api/esm.md Outdated
Comment on lines 950 to 951
> 1. If _matchKey_ is a key of _matchObj_, does not contain _"\*"_, and does not
> end with _"/"_ then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the steps below this are based on the assumption that * is contained in the matchKey, so this restriction isn't quite right.

This is due to the deprecation path of trailing slash imports, which I believe are supposed to be resolution errors now before reaching this point? Would be worth verifying and updating accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the keen eye. I think you're correct that this is not the correct place for the check. This case is handled in the ESM pseudocode here:

node/doc/api/esm.md

Lines 872 to 873 in f6ce486

> 8. If _packageSubpath_ ends in _"/"_, then
> 1. Throw an _Invalid Module Specifier_ error.

CJS doesn't describe the same precondition, though:

node/doc/api/modules.md

Lines 431 to 443 in 579fc67

LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. If `--experimental-require-module` is enabled
a. let CONDITIONS = ["node", "require", "module-sync"]
b. Else, let CONDITIONS = ["node", "require"]
6. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", CONDITIONS) <a href="esm.md#resolver-algorithm-specification">defined in the ESM resolver</a>.
7. RESOLVE_ESM_MATCH(MATCH)

So what I'm seeing is that the CJS pseudocode allows a deprecated trailing-slash require to be passed to PACKAGE_EXPORTS_RESOLVE, but the ESM pseudocode forbids the deprecated behavior before it reaches PACKAGE_EXPORTS_RESOLVE. Which behavior do you want to describe in the pseudocode? Deprecated, or only supported?

If we want the pseudocode to describe only supported non-deprecated behavior then my sense is that "If packageSubpath ends in "/", then" should be moved from PACKAGE_RESOLVE to step 1 of PACKAGE_EXPORTS_RESOLVE. That would cover both the CJS and ESM resolvers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm flip flopping on whether or not this is a good place for it. By specifying this restriction in PACKAGE_IMPORTS_EXPORTS_RESOLVE you cover the imports, exports, and CJS cases. If the endsWith check goes here then step 8 in PACKAGE_RESOLVE can be removed since it is then redundant.

Another inconsistency in the specification discovered:

node/doc/api/esm.md

Lines 932 to 936 in f6ce486

**PACKAGE\_IMPORTS\_RESOLVE**(_specifier_, _parentURL_, _conditions_)
> 1. Assert: _specifier_ begins with _"#"_.
> 2. If _specifier_ is exactly equal to _"#"_ or starts with _"#/"_, then
> 1. Throw an _Invalid Module Specifier_ error.

Implementation:

function packageImportsResolve(name, base, conditions) {
if (name === '#' || StringPrototypeStartsWith(name, '#/') ||
StringPrototypeEndsWith(name, '/')) {
const reason = 'is not a valid internal imports specifier name';
throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base));
}

As specified if you have "imports": { "#test/test/": "./index.js" } in your package.json then import("#test/test/") should work, but this specifier throws both with import & require today. With the check in PACKAGE_IMPORTS_EXPORTS_RESOLVE then the specification matches reality.

I pushed that tweak and addressed the "contains '*'" invariant you raised earlier. Let me know what you think.

Also I noticed that the double-slash restriction is not specified at all. I suppose that could also go in the same place if you wanted to specify it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like the right place for it to me, since CJS will otherwise still support trailing / resolutions fine - it's only the exports field that bans them.

Your latest change therefore looks correct to me, even though the implementation does not yet match it!

This case is handled in the ESM pseudocode here:

Given this I think I also found another implementation bug, as the following doesn't give me a deprecation warning package own-name resolution when it should:

package.json

{
  "name": "test",
  "exports": {
    "./a/*": "./a/*z.js"
  }
}

test.js

import 'test/a/b/'

I think this might be a good sign that it is time to complete the full deprecation cycle on the trailing slash imports and ensure they apply for own-name as well as normal resolutions.

The last thing I'm trying to figure out is if this new check can replace the higher up one in ESM_RESOLVE to avoid redundancy.

For packages without exports, they would only fail at resolution finalization time so it would be a different error but we did used to do this as well, and otherwise it seems like the error will still apply fine in all cases so it might even be an option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's helpful, I faithfully implemented the pseudocode (see: ESM, CJS) for some loaders I'm working on which is how I stumbled on these issues. My tests are hardly comprehensive but here's the trailing slash tests.

Anyway let me know if there's anything else I should cover for this PR or if we're good to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last thing I'm trying to figure out is if this new check can replace the higher up one in ESM_RESOLVE to avoid redundancy.

This is my last question here - if instead of checking the trailing slash in two separate places we just make this new location the only place to do that, and rely on ESM resolution always failing at resolution time for trailing slashes in the non exports case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check at the top of PACKAGE_IMPORTS_EXPORTS_RESOLVE appears to cover all the cases we want.

Semi-related, I just noticed this quirk:

marcel[10:45:40PM] [~/test] cat a.mjs 
console.log(import.meta.resolve('./does-not-exist'));

marcel[10:45:40PM] [~/test] node a.mjs 
file:///Users/marcel/test/does-not-exist

According to the pseudocode this should fail to resolve since step 7.3 in ESM_RESOLVE verifies that the file exists.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me rephrase the question: Given the new check inside of PACKAGE_IMPORTS_EXPORTS_RESOLVE, can we depreacte the existing previous check in step 8 of PACKAGE_RESOLVE? The argument being that in the non-exports case / trailers are always a not found or directory error when hitting the filesystem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import.meta.resolve implementation does not do FS existence checks on the final resolution as an exception to better align with browser implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the new check inside of PACKAGE_IMPORTS_EXPORTS_RESOLVE, can we depreacte the existing previous check in step 8 of PACKAGE_RESOLVE?

Yes, I believe so. That change was included in 4305537 attached to the PR.

@Armanidashh

This comment was marked as off-topic.

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for working through this so thoroughly.

@lpinca lpinca added commit-queue Add this label to land a pull request using GitHub Actions. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. labels Mar 22, 2025
@nodejs-github-bot nodejs-github-bot removed the commit-queue Add this label to land a pull request using GitHub Actions. label Mar 22, 2025
@nodejs-github-bot nodejs-github-bot merged commit 86d8540 into nodejs:main Mar 22, 2025
22 checks passed
@nodejs-github-bot
Copy link
Collaborator

Landed in 86d8540

aduh95 pushed a commit that referenced this pull request Mar 23, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
pandeykushagra51 pushed a commit to pandeykushagra51/node that referenced this pull request Mar 28, 2025
PR-URL: nodejs#57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit that referenced this pull request Apr 1, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit that referenced this pull request Apr 1, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit to RafaelGSS/node that referenced this pull request Apr 8, 2025
PR-URL: nodejs#57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit that referenced this pull request Apr 14, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit that referenced this pull request Apr 14, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
aduh95 pushed a commit that referenced this pull request Apr 14, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
aduh95 pushed a commit that referenced this pull request Apr 14, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
aduh95 pushed a commit that referenced this pull request Apr 15, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
RafaelGSS pushed a commit that referenced this pull request Apr 16, 2025
PR-URL: #57080
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. doc Issues and PRs related to the documentations. esm Issues and PRs related to the ECMAScript Modules implementation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants