Skip to content

Commit 7736f04

Browse files
authored
Implement file extension resolution for CJS, same as ESM; add exhaustive tests for resolver (#1727)
* WIP * restore .d.ts file * add notes and todos * strip down node-internal-modules-cjs-loader to match main branch; will build it back up from here * committing all local stuff * update * it kinda works! * fix CI? * fix? * fix? * fix * bump up to node 18 cuz why not * test matrix: replace node17 with node18 * fix node12 test failure * fix for prior hooks API * tweak * fix * teach node environment resetter to re-sort require.extensions * fix * fix * fix * windows fix? * Addressing TODOs * sync another raw file with upstream * Improve reuse / DI within node ESM stuff * cleanup node internalBinding stuff * Adding hint when ts-node is ignoring a file that you might want it to compile; addressing todos; adding a new one * Add tests for self-imports and empty package.json manifests importing a root index.* file * fix * fix node12
1 parent c67eb46 commit 7736f04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3718
-911
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ jobs:
5454
flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
5555
include:
5656
# Node 12.15
57-
# TODO Add comments about why we test 12.15; I think git blame says it's because of an ESM behavioral change that happened at 12.16
5857
- flavor: 1
5958
node: 12.15
6059
nodeFlag: 12_15
@@ -122,10 +121,10 @@ jobs:
122121
typescript: next
123122
typescriptFlag: next
124123
downgradeNpm: true
125-
# Node 17
124+
# Node 18
126125
- flavor: 12
127-
node: 17
128-
nodeFlag: 17
126+
node: 18
127+
nodeFlag: 18
129128
typescript: latest
130129
typescriptFlag: latest
131130
downgradeNpm: true

.gitignore

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
/node_modules/
22
/tests/node_modules/
3-
.nyc_output/
4-
coverage/
3+
/.nyc_output/
4+
/coverage/
55
.DS_Store
66
npm-debug.log
77
/dist/
8-
tsconfig.schema.json
9-
tsconfig.schemastore-schema.json
10-
.idea/
11-
/.vscode/
8+
/tsconfig.schema.json
9+
/tsconfig.schemastore-schema.json
10+
/.idea/
11+
/.vscode/*
12+
!/.vscode/launch.json
1213
/website/static/api
1314
/tsconfig.tsbuildinfo
1415
/temp

.vscode/launch.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
"<node_internals>/**/*.js"
1313
],
1414
},
15+
{
16+
"name": "Debug resolver tests (example)",
17+
"type": "pwa-node",
18+
"request": "launch",
19+
"cwd": "${workspaceFolder}/tests/tmp/resolver-0015-preferSrc-typeModule-allowJs-experimentalSpecifierResolutionNode",
20+
"runtimeArgs": [
21+
"--loader", "../../../esm.mjs"
22+
],
23+
"program": "./src/entrypoint-0054-src-to-src.mjs"
24+
},
1525
{
1626
"name": "Debug Example: running a test fixture against local ts-node/esm loader",
1727
"type": "pwa-node",
@@ -24,5 +34,5 @@
2434
"<node_internals>/**/*.js"
2535
],
2636
}
27-
],
37+
]
2838
}

NOTES.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
*Delete this file before merging this PR*
2+
3+
## PnP interop
4+
5+
Asked about it here:
6+
https://discord.com/channels/226791405589233664/654372321225605128/957301175609344070
7+
8+
PnP API checks if import specifiers are for dependencies: non-relative, non-absolute
9+
libfoo
10+
@scope/libfoo
11+
12+
When they are, it does `resolveToUnqualified` to map to an unqualified path.
13+
This path points to the module's location on disk (in a zip, perhaps) but does
14+
not handle file extension resolution or stuff like that.
15+
16+
To interop with PnP, we need PnP to *only* `resolveToUnqualified`.
17+
We do everything else.
18+
19+
```typescript
20+
import { Module } from 'module';
21+
import fs from 'fs';
22+
23+
const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:@[^/]+\/)?[^/]+)\/*(.*|)$/;
24+
25+
const originalModuleResolveFilename = Module._resolveFilename;
26+
Module._resolveFilename = function (
27+
request: string,
28+
parent: typeof Module | null | undefined,
29+
isMain: boolean,
30+
options?: { [key: string]: any }
31+
) {
32+
const dependencyNameMatch = request.match(pathRegExp);
33+
if (dependencyNameMatch !== null) {
34+
35+
const [, dependencyName, subPath] = dependencyNameMatch;
36+
37+
const unqualified = pnpapi.resolveToUnqualified(....);
38+
39+
// Do your modified resolution on the unqualified path here
40+
41+
} else {
42+
43+
// Do your modified resolution here; no need for PnP
44+
45+
}
46+
47+
};
48+
```
49+
50+
PnP can be installed at runtime.
51+
52+
To conditionally check if PnP is available at the start of *every* resolution:
53+
54+
```typescript
55+
// Get the pnpapi of either the issuer or the specifier.
56+
// The latter is required when the specifier is an absolute path to a
57+
// zip file and the issuer doesn't belong to a pnpapi
58+
const {findPnPApi} = Module;
59+
const pnpapi = findPnPApi ? (findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null)) : null;
60+
if (pnpapi) {...}
61+
```

TODO.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
*Delete this file before merging this PR*
2+
3+
## TODOs
4+
5+
Copy any relevant changes from `add-cjs-loader-resolve`
6+
7+
I forgot exactly where I was in `add-cjs-loader-resolve`
8+
Re-do the renaming and moving that I did in that branch.
9+
Then diff to see that I did it correctly.
10+
Avoid introducing any accidental behavioral changes.
11+
12+
Make list of changes from vanilla node in dist-raw/node-internal-modules-cjs-loader-old.js
13+
Apply those changes to dist-raw/node-internal-modules-cjs-loader.js
14+

ava.config.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const expect = require('expect');
2+
const semver = require('semver');
23
const { createRequire } = require('module');
34

45
module.exports = {
@@ -14,6 +15,9 @@ module.exports = {
1415
NODE_PATH: ''
1516
},
1617
require: ['./src/test/remove-env-var-force-color.js'],
18+
nodeArguments: semver.gte(process.version, '14.0.0')
19+
? ['--loader', './src/test/test-loader.mjs', '--no-warnings']
20+
: [],
1721
timeout: '300s',
1822
concurrency: 1,
1923
};

child-loader.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'url';
22
import { createRequire } from 'module';
33
const require = createRequire(fileURLToPath(import.meta.url));
44

5+
// TODO why use require() here? I think we can just `import`
56
/** @type {import('./dist/child-loader')} */
67
const childLoader = require('./dist/child/child-loader');
78
export const { resolve, load, getFormat, transformSource } = childLoader;

dist-raw/node-errors.js

Lines changed: 0 additions & 32 deletions
This file was deleted.

dist-raw/node-internal-constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copied from https://github.com/nodejs/node/blob/master/lib/internal/constants.js
2+
module.exports = {
3+
CHAR_FORWARD_SLASH: 47, /* / */
4+
};

dist-raw/node-internal-errors.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,37 @@
22

33
const path = require('path');
44

5-
module.exports = {
6-
createErrRequireEsm
7-
};
5+
exports.codes = {
6+
ERR_INPUT_TYPE_NOT_ALLOWED: createErrorCtor(joinArgs('ERR_INPUT_TYPE_NOT_ALLOWED')),
7+
ERR_INVALID_ARG_VALUE: createErrorCtor(joinArgs('ERR_INVALID_ARG_VALUE')),
8+
ERR_INVALID_MODULE_SPECIFIER: createErrorCtor(joinArgs('ERR_INVALID_MODULE_SPECIFIER')),
9+
ERR_INVALID_PACKAGE_CONFIG: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_CONFIG')),
10+
ERR_INVALID_PACKAGE_TARGET: createErrorCtor(joinArgs('ERR_INVALID_PACKAGE_TARGET')),
11+
ERR_MANIFEST_DEPENDENCY_MISSING: createErrorCtor(joinArgs('ERR_MANIFEST_DEPENDENCY_MISSING')),
12+
ERR_MODULE_NOT_FOUND: createErrorCtor((path, base, type = 'package') => {
13+
return `Cannot find ${type} '${path}' imported from ${base}`
14+
}),
15+
ERR_PACKAGE_IMPORT_NOT_DEFINED: createErrorCtor(joinArgs('ERR_PACKAGE_IMPORT_NOT_DEFINED')),
16+
ERR_PACKAGE_PATH_NOT_EXPORTED: createErrorCtor(joinArgs('ERR_PACKAGE_PATH_NOT_EXPORTED')),
17+
ERR_UNSUPPORTED_DIR_IMPORT: createErrorCtor(joinArgs('ERR_UNSUPPORTED_DIR_IMPORT')),
18+
ERR_UNSUPPORTED_ESM_URL_SCHEME: createErrorCtor(joinArgs('ERR_UNSUPPORTED_ESM_URL_SCHEME')),
19+
ERR_UNKNOWN_FILE_EXTENSION: createErrorCtor(joinArgs('ERR_UNKNOWN_FILE_EXTENSION')),
20+
}
21+
22+
function joinArgs(name) {
23+
return (...args) => {
24+
return [name, ...args].join(' ')
25+
}
26+
}
27+
28+
function createErrorCtor(errorMessageCreator) {
29+
return class CustomError extends Error {
30+
constructor(...args) {
31+
super(errorMessageCreator(...args))
32+
}
33+
}
34+
}
35+
exports.createErrRequireEsm = createErrRequireEsm;
836

937
// Native ERR_REQUIRE_ESM Error is declared here:
1038
// https://github.com/nodejs/node/blob/2d5d77306f6dff9110c1f77fefab25f973415770/lib/internal/errors.js#L1294-L1313

dist-raw/node-internal-modules-cjs-helpers.d.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist-raw/node-internal-modules-cjs-helpers.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,45 @@
1-
const {ArrayPrototypeForEach, StringPrototypeStartsWith, ObjectPrototypeHasOwnProperty, StringPrototypeIncludes, ObjectDefineProperty} = require('./node-primordials');
1+
// Copied from https://github.com/nodejs/node/blob/v17.0.1/lib/internal/modules/cjs/helpers.js
22

3-
exports.addBuiltinLibsToObject = addBuiltinLibsToObject;
3+
'use strict';
4+
5+
const {
6+
ArrayPrototypeForEach,
7+
ObjectDefineProperty,
8+
ObjectPrototypeHasOwnProperty,
9+
SafeSet,
10+
StringPrototypeIncludes,
11+
StringPrototypeStartsWith,
12+
} = require('./node-primordials');
13+
14+
const { getOptionValue } = require('./node-options');
15+
const userConditions = getOptionValue('--conditions');
16+
17+
const noAddons = getOptionValue('--no-addons');
18+
const addonConditions = noAddons ? [] : ['node-addons'];
19+
20+
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
21+
const cjsConditions = new SafeSet([
22+
'require',
23+
'node',
24+
...addonConditions,
25+
...userConditions,
26+
]);
427

5-
// Copied from https://github.com/nodejs/node/blob/21f5a56914a3b24ad77535ef369b93c6b1c11d18/lib/internal/modules/cjs/helpers.js#L133-L178
6-
function addBuiltinLibsToObject(object) {
28+
/**
29+
* @param {any} object
30+
* @param {string} [dummyModuleName]
31+
* @return {void}
32+
*/
33+
function addBuiltinLibsToObject(object, dummyModuleName) {
734
// Make built-in modules available directly (loaded lazily).
8-
const { builtinModules } = require('module').Module;
35+
const Module = require('module').Module;
36+
const { builtinModules } = Module;
37+
38+
// To require built-in modules in user-land and ignore modules whose
39+
// `canBeRequiredByUsers` is false. So we create a dummy module object and not
40+
// use `require()` directly.
41+
const dummyModule = new Module(dummyModuleName);
42+
943
ArrayPrototypeForEach(builtinModules, (name) => {
1044
// Neither add underscored modules, nor ones that contain slashes (e.g.,
1145
// 'fs/promises') or ones that are already defined.
@@ -29,7 +63,8 @@ function addBuiltinLibsToObject(object) {
2963

3064
ObjectDefineProperty(object, name, {
3165
get: () => {
32-
const lib = require(name);
66+
// Node 12 hack; remove when we drop node12 support
67+
const lib = (dummyModule.require || require)(name);
3368

3469
// Disable the current getter/setter and set up a new
3570
// non-enumerable property.
@@ -49,3 +84,6 @@ function addBuiltinLibsToObject(object) {
4984
});
5085
});
5186
}
87+
88+
exports.addBuiltinLibsToObject = addBuiltinLibsToObject;
89+
exports.cjsConditions = cjsConditions;

0 commit comments

Comments
 (0)