Skip to content

Commit f34d874

Browse files
Support .mts, .cts, nodenext, and node16 (#1694)
* feat: support .mts .cts * stuff * WIP with TODOs * WIP * lint-fix * WIP * WIP * WIP * fixes * flatten resolver tests by splitting into more functions * address extension-handling code * lint-fix * Fix; update resolver tests to test more things * fix * fix * test against TS rc on nodes 14, 16, 18 (good sanity-checking with these new nodenext features) * Teach ts.transpileModule to handle NodeNext correctly * tweak tests * fix test typos; update pluggable dep tests; update ignore() tests for new file extensions * fix tests * fix * fix build against stable ts, no need for 4.7.0 just yet * Gate nodenext/node16 tests behind a TS version check; fix TSCommon types * Fix nyc require.extensions issues * lint-fix * tricky types * skip nodenext tests on older TS * fix tests * fix bug and tests * fix windows * turn off another test on ancient TS versions * fix windows * fix allowing `moduleTypeOverrides` to override cts/cjs * Update moduleTypes docs * add mts and cts awareness to internal/external classifier; add .d.cts/.d.mts extensions to tests * cleanup misc markdown files * more markdown cleanup Co-authored-by: bluelovers <[email protected]>
1 parent cf93584 commit f34d874

35 files changed

+1894
-742
lines changed

.github/workflows/continuous-integration.yml

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
matrix:
5252
os: [ubuntu, windows]
5353
# Don't forget to add all new flavors to this list!
54-
flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
54+
flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
5555
include:
5656
# Node 12.15
5757
- flavor: 1
@@ -95,41 +95,64 @@ jobs:
9595
nodeFlag: 14
9696
typescript: next
9797
typescriptFlag: next
98+
- flavor: 8
99+
node: 14
100+
nodeFlag: 14
101+
typescript: rc
102+
typescriptFlag: rc
98103
# Node 16
99104
# Node 16.11.1
100105
# Earliest version that supports old ESM Loader Hooks API: https://github.com/TypeStrong/ts-node/pull/1522
101-
- flavor: 8
106+
- flavor: 9
102107
node: 16.11.1
103108
nodeFlag: 16_11_1
104109
typescript: latest
105110
typescriptFlag: latest
106-
- flavor: 9
111+
- flavor: 10
107112
node: 16
108113
nodeFlag: 16
109114
typescript: latest
110115
typescriptFlag: latest
111116
downgradeNpm: true
112-
- flavor: 10
117+
- flavor: 11
113118
node: 16
114119
nodeFlag: 16
115120
typescript: 2.7
116121
typescriptFlag: 2_7
117122
downgradeNpm: true
118-
- flavor: 11
123+
- flavor: 12
119124
node: 16
120125
nodeFlag: 16
121126
typescript: next
122127
typescriptFlag: next
123128
downgradeNpm: true
129+
- flavor: 13
130+
node: 16
131+
nodeFlag: 16
132+
typescript: rc
133+
typescriptFlag: rc
134+
downgradeNpm: true
124135
# Node 18
125-
- flavor: 12
136+
- flavor: 14
126137
node: 18
127138
nodeFlag: 18
128139
typescript: latest
129140
typescriptFlag: latest
130141
downgradeNpm: true
142+
- flavor: 15
143+
node: 18
144+
nodeFlag: 18
145+
typescript: next
146+
typescriptFlag: next
147+
downgradeNpm: true
148+
- flavor: 16
149+
node: 18
150+
nodeFlag: 18
151+
typescript: rc
152+
typescriptFlag: rc
153+
downgradeNpm: true
131154
# Node nightly
132-
- flavor: 13
155+
- flavor: 17
133156
node: nightly
134157
nodeFlag: nightly
135158
typescript: latest

.vscode/launch.json

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,33 @@
1010
"outputCapture": "std",
1111
"skipFiles": [
1212
"<node_internals>/**/*.js"
13-
],
13+
]
1414
},
1515
{
16-
"name": "Debug resolver tests (example)",
16+
"name": "Debug resolver test",
1717
"type": "pwa-node",
1818
"request": "launch",
19-
"cwd": "${workspaceFolder}/tests/tmp/resolver-0015-preferSrc-typeModule-allowJs-experimentalSpecifierResolutionNode",
19+
"cwd": "${workspaceFolder}/tests/tmp/resolver-0029-preferSrc-typeModule-allowJs-skipIgnore-experimentalSpecifierResolutionNode",
2020
"runtimeArgs": [
21-
"--loader", "../../../esm.mjs"
21+
"--loader",
22+
"../../../esm.mjs"
2223
],
23-
"program": "./src/entrypoint-0054-src-to-src.mjs"
24+
"program": "./src/entrypoint-0000-src-to-src.cjs"
2425
},
2526
{
2627
"name": "Debug Example: running a test fixture against local ts-node/esm loader",
2728
"type": "pwa-node",
2829
"request": "launch",
2930
"cwd": "${workspaceFolder}/tests/esm",
30-
"runtimeArgs": ["--loader", "../../ts-node/esm"],
31+
"runtimeArgs": [
32+
"--loader",
33+
"../../ts-node/esm"
34+
],
3135
"program": "throw error.ts",
3236
"outputCapture": "std",
3337
"skipFiles": [
3438
"<node_internals>/**/*.js"
35-
],
39+
]
3640
}
3741
]
38-
}
42+
}

development-docs/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
This directory contains a variety of documents:
2+
3+
- notes
4+
- old to-do lists
5+
- design ideas from when I implemented various features
6+
- templates for drafting release notes
7+
- etc
8+
9+
It is useful to me to keep these notes. If you find their presence
10+
confusing, you can safely ignore this directory.

development-docs/nodenextNode16.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Adding support for NodeNext, Node16, `.cts`, `.mts`, `.cjs`, `.mjs`
2+
3+
*This feature has already been implemented. Here are my notes from when
4+
I was doing the work*
5+
6+
## TODOs
7+
8+
Implement node module type classifier:
9+
- if NodeNext or Node12: ask classifier for CJS or ESM determination
10+
Add `ForceNodeNextCJSEmit`
11+
12+
Does our code check for .d.ts extensions anywhere?
13+
- if so, teach it about .d.cts and .d.mts
14+
15+
For nodenext and node12, support supplemental "flavor" information:
16+
-
17+
18+
Think about splitting out index.ts further:
19+
- register.ts - hooking stuff
20+
- types.ts
21+
- env.ts - env vars and global registration types (process.symbol)
22+
- service.ts
23+
24+
# TESTS
25+
26+
Matrix:
27+
28+
- package.json type absent, commonjs, and module
29+
- import and require
30+
- from cjs and esm
31+
- .cts, .cjs
32+
- .mts, .mjs
33+
- typechecking, transpileOnly, and swc
34+
- dynamic import
35+
- import = require
36+
- static import
37+
- allowJs on and off
38+
39+
Notes about specific matrix entries:
40+
- require mjs, mts from cjs throws error
41+
42+
Rethink:
43+
`getOutput`: null in transpile-only mode. Also may return emitskipped
44+
`getOutputTranspileOnly`: configured module option
45+
`getOutputForceCommonJS`: `commonjs` module option
46+
`getOutputForceNodeCommonJS`: `nodenext` cjs module option
47+
`getOutputForceESM`: `esnext` module option
48+
49+
Add second layer of classification to classifier:
50+
if classifier returns `auto` (no `moduleType` override)
51+
- if `getOutput` emits, done
52+
- else call `nodeModuleTypeClassifier`
53+
- delegate to appropriate `getOutput` based on its response

notes2.md renamed to development-docs/rootDirOutDirMapping.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
## Musings about resolving between rootDir and outDir
2+
13
When /dist and /src are understood to be overlaid because of src -> dist compiling
24
/dist/
35
/src/

NOTES.md renamed to development-docs/yarnPnpInterop.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
*Delete this file before merging this PR*
2-
3-
## PnP interop
1+
## Yarn PnP interop
42

53
Asked about it here:
64
https://discord.com/channels/226791405589233664/654372321225605128/957301175609344070

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

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
'use strict';
66

77
const {
8+
ArrayIsArray,
9+
ArrayPrototypeIncludes,
810
ArrayPrototypeJoin,
11+
ArrayPrototypePush,
912
JSONParse,
1013
ObjectKeys,
1114
RegExpPrototypeTest,
@@ -46,6 +49,8 @@ const {
4649

4750
const Module = require('module');
4851

52+
const isWindows = process.platform === 'win32';
53+
4954
let statCache = null;
5055

5156
function stat(filename) {
@@ -133,12 +138,13 @@ function readPackageScope(checkPath) {
133138
/**
134139
* @param {{
135140
* nodeEsmResolver: ReturnType<typeof import('./node-internal-modules-esm-resolve').createResolve>,
136-
* compiledExtensions: string[],
141+
* extensions: import('../src/file-extensions').Extensions,
137142
* preferTsExts
138143
* }} opts
139144
*/
140145
function createCjsLoader(opts) {
141-
const {nodeEsmResolver, compiledExtensions, preferTsExts} = opts;
146+
const {nodeEsmResolver, preferTsExts} = opts;
147+
const {replacementsForCjs, replacementsForJs, replacementsForMjs} = opts.extensions;
142148
const {
143149
encodedSepRegEx,
144150
packageExportsResolve,
@@ -209,47 +215,42 @@ function toRealPath(requestPath) {
209215
});
210216
}
211217

212-
/**
213-
* TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions.
214-
* IMPORTANT: preserve ordering according to preferTsExts; this affects resolution behavior!
215-
*/
216-
const extensions = Array.from(new Set([
217-
...(preferTsExts ? compiledExtensions : []),
218-
'.js', '.json', '.node', '.mjs', '.cjs',
219-
...compiledExtensions
220-
]));
221-
const replacementExtensions = {
222-
'.js': extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)),
223-
'.cjs': extensions.filter(ext => ['.cjs', '.cts'].includes(ext)),
224-
'.mjs': extensions.filter(ext => ['.mjs', '.mts'].includes(ext)),
225-
};
226-
227-
const replacableExtensionRe = /(\.(?:js|cjs|mjs))$/;
228-
229218
function statReplacementExtensions(p) {
230-
const match = p.match(replacableExtensionRe);
231-
if (match) {
232-
const replacementExts = replacementExtensions[match[1]];
233-
const pathnameWithoutExtension = p.slice(0, -match[1].length);
234-
for (let i = 0; i < replacementExts.length; i++) {
235-
const filename = pathnameWithoutExtension + replacementExts[i];
236-
const rc = stat(filename);
237-
if (rc === 0) {
238-
return [rc, filename];
219+
const lastDotIndex = p.lastIndexOf('.');
220+
if(lastDotIndex >= 0) {
221+
const ext = p.slice(lastDotIndex);
222+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
223+
const pathnameWithoutExtension = p.slice(0, lastDotIndex);
224+
const replacementExts =
225+
ext === '.js' ? replacementsForJs
226+
: ext === '.mjs' ? replacementsForMjs
227+
: replacementsForCjs;
228+
for (let i = 0; i < replacementExts.length; i++) {
229+
const filename = pathnameWithoutExtension + replacementExts[i];
230+
const rc = stat(filename);
231+
if (rc === 0) {
232+
return [rc, filename];
233+
}
239234
}
240235
}
241236
}
242237
return [stat(p), p];
243238
}
244239
function tryReplacementExtensions(p, isMain) {
245-
const match = p.match(replacableExtensionRe);
246-
if (match) {
247-
const replacementExts = replacementExtensions[match[1]];
248-
const pathnameWithoutExtension = p.slice(0, -match[1].length);
249-
for (let i = 0; i < replacementExts.length; i++) {
250-
const filename = tryFile(pathnameWithoutExtension + replacementExts[i], isMain);
251-
if (filename) {
252-
return filename;
240+
const lastDotIndex = p.lastIndexOf('.');
241+
if(lastDotIndex >= 0) {
242+
const ext = p.slice(lastDotIndex);
243+
if (ext === '.js' || ext === '.cjs' || ext === '.mjs') {
244+
const pathnameWithoutExtension = p.slice(0, lastDotIndex);
245+
const replacementExts =
246+
ext === '.js' ? replacementsForJs
247+
: ext === '.mjs' ? replacementsForMjs
248+
: replacementsForCjs;
249+
for (let i = 0; i < replacementExts.length; i++) {
250+
const filename = tryFile(pathnameWithoutExtension + replacementExts[i], isMain);
251+
if (filename) {
252+
return filename;
253+
}
253254
}
254255
}
255256
}
@@ -564,11 +565,18 @@ function assertScriptCanLoadAsCJSImpl(service, module, filename) {
564565
const pkg = readPackageScope(filename);
565566

566567
// ts-node modification: allow our configuration to override
567-
const tsNodeClassification = service.moduleTypeClassifier.classifyModule(normalizeSlashes(filename));
568+
const tsNodeClassification = service.moduleTypeClassifier.classifyModuleByModuleTypeOverrides(normalizeSlashes(filename));
568569
if(tsNodeClassification.moduleType === 'cjs') return;
569570

571+
// ignore package.json when file extension is ESM-only or CJS-only
572+
// [MUST_UPDATE_FOR_NEW_FILE_EXTENSIONS]
573+
const lastDotIndex = filename.lastIndexOf('.');
574+
const ext = lastDotIndex >= 0 ? filename.slice(lastDotIndex) : '';
575+
576+
if((ext === '.cts' || ext === '.cjs') && tsNodeClassification.moduleType === 'auto') return;
577+
570578
// Function require shouldn't be used in ES modules.
571-
if (tsNodeClassification.moduleType === 'esm' || (pkg && pkg.data && pkg.data.type === 'module')) {
579+
if (ext === '.mts' || ext === '.mjs' || tsNodeClassification.moduleType === 'esm' || (pkg && pkg.data && pkg.data.type === 'module')) {
572580
const parentPath = module.parent && module.parent.filename;
573581
const packageJsonPath = pkg ? path.resolve(pkg.path, 'package.json') : null;
574582
throw createErrRequireEsm(filename, parentPath, packageJsonPath);
@@ -578,5 +586,6 @@ function assertScriptCanLoadAsCJSImpl(service, module, filename) {
578586

579587
module.exports = {
580588
createCjsLoader,
581-
assertScriptCanLoadAsCJSImpl
589+
assertScriptCanLoadAsCJSImpl,
590+
readPackageScope
582591
};

0 commit comments

Comments
 (0)