-
-
Notifications
You must be signed in to change notification settings - Fork 534
/
resolver.spec.ts
executable file
·714 lines (669 loc) · 26.9 KB
/
resolver.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
import { context, ExecutionContext, expect, TestInterface } from './testlib';
import {
ctxTsNode,
isOneOf,
resetNodeEnvironment,
ts,
tsSupportsMtsCtsExtensions,
tsSupportsStableNodeNextNode16,
} from './helpers';
import { project as fsProject, Project as FsProject } from '@TypeStrong/fs-fixture-builder';
import { join } from 'path';
import * as semver from 'semver';
import { padStart } from 'lodash';
import _ = require('lodash');
import { pathToFileURL } from 'url';
import type { RegisterOptions } from '..';
import * as fs from 'fs';
import * as Path from 'path';
/*
* Each test case is a separate TS project, with a different permutation of
* project options. The project is written to disc, then ts-node is installed,
* then several entrypoint-* files are imported to test our resolver.
*
* High-level structure of these tests:
* package.json, tsconfig.json, src/, and out/
* entrypoint-* files are the entrypoints
* they import a bunch of target files / directories / node_modules
*
* The heart of this test is every time an entrypoint imports a target.
* We are testing if the resolver figures out the correct target file to import.
*
* To better understand the emitted projects, run the tests, then look in `tests/tmp/resolver-*`
*
* Whenever a test fails, the error will log a command you can paste into your terminal to re-run
* that project *outside* of this test suite. This may be helpful in understanding and debugging
* these tests.
*/
// Test a bunch of permutations of:
// import permutations:
// - [x] Relative import of file
// - [x] Relative import of index
// - [x] rootless library import of main
// - [x] rootless library import of index
// - [x] rootless library import of exports sub-path
// - [x] rootless self-import of main
// - [x] rootless self-import of index
// - [x] rootless self-import of exports sub-path
// - [x] Require with extension
// - [x] Require without extension
// - Require from dist to dist
// - Require from dist to src
// - Require from src to dist
// - [x] Require from src to src
// lib permutations:
// - [x] module exists in both src and dist (precompilation ran)
// - [x] module exists in only dist (came from elsewhere)
// - [x] module exists only in src (did not precompile)
// - .ts / .js extension
// - .tsx / .js extension
// - .cts / .cjs extension
// - .mts / .mjs extension
// - .js / .js extension
// - .jsx / .js extension
// - .cjs / .cjs extension
// - .mjs / .mjs extension
// Side-step compiler transformation of import() into require()
const dynamicImport = new Function('specifier', 'return import(specifier)');
// For some reason `new Function` was triggering what *might* be a node bug,
// where `context.parentURL` passed into loader `resolve()` was wrong.
// eval works for unknown reasons. This may change in future node releases.
const declareDynamicImportFunction = `const dynamicImport = eval('(specifier) => import(specifier)');`;
const test = context(ctxTsNode);
type Test = TestInterface<ctxTsNode.Ctx>;
type T = ExecutionContext<ctxTsNode.Ctx>;
const projectSeq = seqGenerator();
const entrypointSeq = seqGenerator();
const targetSeq = seqGenerator();
interface Project {
identifier: string;
allowJs: boolean;
preferSrc: boolean;
typeModule: boolean;
/** Use TS's new module: `nodenext` option */
useTsNodeNext: boolean;
experimentalSpecifierResolutionNode: boolean;
skipIgnore: boolean;
}
interface EntrypointPermutation {
entrypointExt: 'cjs' | 'mjs';
withExt: boolean;
entrypointLocation: 'src' | 'out';
entrypointTargetting: 'src' | 'out';
}
type Entrypoint = string;
interface GenerateTargetOptions {
inSrc: boolean;
inOut: boolean;
srcExt: string;
/** If true, is an index.* file within a directory */
isIndex: boolean;
targetPackageStyle: TargetPackageStyle;
packageTypeModule: boolean;
}
interface Target {
targetIdentifier: string;
outName: string;
srcName: string;
srcExt: string;
outExt: string;
inSrc: boolean;
inOut: boolean;
/** If true, is neither an index.* nor a package */
isNamedFile: boolean;
/** If true, is an index.* file within a directory */
isIndex: boolean;
/** If true, should be imported as an npm package, not relative import */
isPackage: boolean;
packageStyle: TargetPackageStyle;
typeModule: boolean;
}
/** When target is actually a mini node_modules package */
type TargetPackageStyle = typeof targetPackageStyles[number];
const targetPackageStyles = [
false,
// test that the package contains /index.*
'empty-manifest',
// "main": "src/target.<ext>"
'main-src-with-extension',
// "main": "src/target.<output ext>"
'main-src-with-out-extension',
// "main": "out/target.<output ext>"
'main-out-with-extension',
// "main": "src/target"
'main-src-extensionless',
// "main": "out/target"
'main-out-extensionless',
// "exports": {".": "src/target.<ext>"}
'exports-src-with-extension',
// "exports": {".": "src/target.<output ext>"}
'exports-src-with-out-extension',
// "exports": {".": "out/target.<output ext>"}
'exports-out-with-extension',
] as const;
test.suite('Resolver hooks', (test) => {
test.serial();
test.if(tsSupportsMtsCtsExtensions);
//
// Generate all permutations of projects
//
for (const preferSrc of [false, true]) {
for (const typeModule of [false, true]) {
for (const allowJs of [false, true]) {
for (const useTsNodeNext of [false, true]) {
// TODO test against skipIgnore: false, where imports of third-party deps in `node_modules` should not get our mapping behaviors
for (const skipIgnore of [/*false, */ true]) {
for (const experimentalSpecifierResolutionNode of [false, true]) {
let identifier = `resolver-${projectSeq()}`;
identifier += preferSrc ? '-preferSrc' : '-preferOut';
identifier += typeModule ? '-typeModule' : '-typeCjs---';
identifier += allowJs ? '-allowJs' : '--------';
identifier += useTsNodeNext ? '-useTsNodenext' : '--------------';
identifier += skipIgnore ? '-skipIgnore' : '-----------';
identifier += experimentalSpecifierResolutionNode ? '-experimentalSpecifierResolutionNode' : '';
const project: Project = {
identifier,
allowJs,
preferSrc,
typeModule,
useTsNodeNext,
experimentalSpecifierResolutionNode,
skipIgnore,
};
declareProject(test, project);
}
}
}
}
}
}
});
function declareProject(_test: Test, project: Project) {
const test = project.useTsNodeNext && !tsSupportsStableNodeNextNode16 ? _test.skip : _test;
test(`${project.identifier}`, async (t) => {
t.teardown(() => {
resetNodeEnvironment();
});
const p = fsProject(project.identifier);
p.rm();
p.addJsonFile('package.json', {
type: project.typeModule ? 'module' : undefined,
});
p.addJsonFile('tsconfig.json', {
'ts-node': {
experimentalResolver: true,
preferTsExts: project.preferSrc,
transpileOnly: true,
experimentalSpecifierResolution: project.experimentalSpecifierResolutionNode ? 'node' : undefined,
skipIgnore: project.skipIgnore,
} as RegisterOptions,
compilerOptions: {
allowJs: project.allowJs,
skipLibCheck: true,
// TODO add nodenext permutation
module: project.useTsNodeNext ? 'NodeNext' : project.typeModule ? 'esnext' : 'commonjs',
jsx: 'react',
target: 'esnext',
},
});
const targets = generateTargets(project, p);
const entrypoints = generateEntrypoints(project, p, targets);
p.write();
await execute(t, p, entrypoints);
});
}
//
// Generate all target-* files
//
function generateTargets(project: Project, p: FsProject) {
/** Array of metadata about target files to be imported */
const targets: Array<Target> = [];
// TODO does allowJs matter?
for (const inOut of [false, true]) {
for (const inSrc of [false, true]) {
for (const srcExt of ['ts', 'tsx', 'cts', 'mts', 'jsx', 'js', 'cjs', 'mjs']) {
for (const targetPackageStyle of targetPackageStyles) {
const packageTypeModulePermutations = targetPackageStyle ? [true, false] : [project.typeModule];
for (const packageTypeModule of packageTypeModulePermutations) {
const isIndexPermutations = targetPackageStyle ? [false] : [true, false];
// TODO test main pointing to a directory containing an `index.` file?
for (const isIndex of isIndexPermutations) {
//#region SKIPPING
if (!inSrc && !inOut) continue;
// Don't bother with jsx if we don't have allowJs enabled
// TODO Get rid of this? "Just work" in this case?
if (srcExt === 'jsx' && !project.allowJs) continue;
// Don't bother with src-only extensions when only emitting to `out`
if (!inSrc && ['ts', 'tsx', 'cts', 'mts', 'jsx'].includes(srcExt)) continue;
// TODO re-enable with src <-> out mapping
if (
!inOut &&
isOneOf(targetPackageStyle, [
'main-out-with-extension',
'main-out-extensionless',
'exports-out-with-extension',
])
)
continue;
if (
!inSrc &&
isOneOf(targetPackageStyle, [
'main-src-with-extension',
'main-src-extensionless',
'exports-src-with-extension',
])
)
continue;
if (
isOneOf(targetPackageStyle, [
'main-out-with-extension',
'main-out-extensionless',
'exports-out-with-extension',
])
)
continue;
//#endregion
targets.push(
generateTarget(project, p, {
inSrc,
inOut,
srcExt,
targetPackageStyle,
packageTypeModule,
isIndex,
})
);
}
}
}
}
}
}
return targets;
}
function generateTarget(project: Project, p: FsProject, options: GenerateTargetOptions) {
const { inSrc, inOut, srcExt, targetPackageStyle, packageTypeModule, isIndex } = options;
const outExt = srcExt.replace('ts', 'js').replace('x', '');
let targetIdentifier = `target-${targetSeq()}-${inOut && inSrc ? 'inboth' : inOut ? 'onlyout' : 'onlysrc'}-${srcExt}`;
if (targetPackageStyle)
targetIdentifier = `${targetIdentifier}-${targetPackageStyle}-${packageTypeModule ? 'module' : 'commonjs'}`;
let prefix = targetPackageStyle ? `node_modules/${targetIdentifier}/` : '';
let suffix = targetPackageStyle === 'empty-manifest' ? 'index' : targetPackageStyle ? 'target' : targetIdentifier;
if (isIndex) suffix += '-dir/index';
const srcDirInfix = targetPackageStyle === 'empty-manifest' ? '' : 'src/';
const outDirInfix = targetPackageStyle === 'empty-manifest' ? '' : 'out/';
const srcName = `${prefix}${srcDirInfix}${suffix}.${srcExt}`;
const srcDirOutExtName = `${prefix}${srcDirInfix}${suffix}.${outExt}`;
const outName = `${prefix}${outDirInfix}${suffix}.${outExt}`;
const selfImporterCjsName = `${prefix}self-import-cjs.cjs`;
const selfImporterMjsName = `${prefix}self-import-mjs.mjs`;
const target: Target = {
targetIdentifier,
srcName,
outName,
srcExt,
outExt,
inSrc,
inOut,
isNamedFile: !isIndex && !targetPackageStyle,
isIndex,
isPackage: !!targetPackageStyle,
packageStyle: targetPackageStyle,
typeModule: packageTypeModule,
};
const { isMjs: targetIsMjs } = fileInfo('.' + srcExt, packageTypeModule, project.allowJs);
function targetContent(loc: string) {
let content = '';
if (targetIsMjs) {
content += String.raw`
const {fileURLToPath} = await import('url');
const filenameNative = fileURLToPath(import.meta.url);
export const directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/]/, '$1');
export const filename = filenameNative.replace(/.*[\\\/]/, '');
export const targetIdentifier = '${targetIdentifier}';
export const ext = filenameNative.replace(/.*\./, '');
export const loc = '${loc}';
`;
} else {
content += String.raw`
const filenameNative = __filename;
exports.filename = filenameNative.replace(/.*[\\\/]/, '');
exports.directory = filenameNative.replace(/.*[\\\/](.*?)[\\\/].*/, '$1');
exports.targetIdentifier = '${targetIdentifier}';
exports.ext = filenameNative.replace(/.*\./, '');
exports.loc = '${loc}';
`;
}
return content;
}
if (inOut) {
p.addFile(outName, targetContent('out'));
// TODO so we can test multiple file extensions in a single directory, preferTsExt
p.addFile(srcDirOutExtName, targetContent('out'));
}
if (inSrc) {
p.addFile(srcName, targetContent('src'));
}
if (targetPackageStyle) {
const selfImporterIsCompiled = project.allowJs;
const cjsSelfImporterMustUseDynamicImportHack = !project.useTsNodeNext && selfImporterIsCompiled && targetIsMjs;
p.addFile(
selfImporterCjsName,
targetIsMjs
? cjsSelfImporterMustUseDynamicImportHack
? `${declareDynamicImportFunction}\nmodule.exports = dynamicImport('${targetIdentifier}');`
: `module.exports = import("${targetIdentifier}");`
: `module.exports = require("${targetIdentifier}");`
);
p.addFile(
selfImporterMjsName,
`
export * from "${targetIdentifier}";
`
);
function writePackageJson(obj: any) {
p.addJsonFile(`${prefix}/package.json`, {
name: targetIdentifier,
type: packageTypeModule ? 'module' : undefined,
...obj,
});
}
switch (targetPackageStyle) {
case 'empty-manifest':
writePackageJson({});
break;
case 'exports-src-with-extension':
writePackageJson({
exports: {
'.': `./src/${suffix}.${srcExt}`,
},
});
break;
case 'exports-src-with-out-extension':
writePackageJson({
exports: {
'.': `./src/${suffix}.${outExt}`,
},
});
break;
case 'exports-out-with-extension':
writePackageJson({
exports: {
'.': `./out/${suffix}.${outExt}`,
},
});
break;
case 'main-src-extensionless':
writePackageJson({
main: `src/${suffix}`,
});
break;
case 'main-out-extensionless':
writePackageJson({
main: `out/${suffix}`,
});
break;
case 'main-src-with-extension':
writePackageJson({
main: `src/${suffix}.${srcExt}`,
});
break;
case 'main-src-with-out-extension':
writePackageJson({
main: `src/${suffix}.${outExt}`,
});
break;
case 'main-out-with-extension':
writePackageJson({
main: `src/${suffix}.${outExt}`,
});
break;
default:
const _assert: never = targetPackageStyle;
}
}
return target;
}
/**
* Generate all entrypoint-* files
*/
function generateEntrypoints(project: Project, p: FsProject, targets: Target[]) {
/** Array of entrypoint files to be imported during the test */
let entrypoints: string[] = [];
for (const entrypointExt of ['cjs', 'mjs'] as const) {
// TODO consider removing this logic; deferring to conditionals in the generateEntrypoint which emit meaningful comments
const withExtPermutations =
entrypointExt == 'mjs' && project.experimentalSpecifierResolutionNode === false ? [true] : [false, true];
for (const withExt of withExtPermutations) {
// Location of the entrypoint
for (const entrypointLocation of ['src', 'out'] as const) {
// Target of the entrypoint's import statements
for (const entrypointTargetting of ['src', 'out'] as const) {
// TODO re-enable when we have out <-> src mapping
if (entrypointLocation !== 'src') continue;
if (entrypointTargetting !== 'src') continue;
entrypoints.push(
generateEntrypoint(project, p, targets, {
entrypointExt,
withExt,
entrypointLocation,
entrypointTargetting,
})
);
}
}
}
}
return entrypoints;
}
function generateEntrypoint(project: Project, p: FsProject, targets: Target[], opts: EntrypointPermutation) {
const { entrypointExt, withExt, entrypointLocation, entrypointTargetting } = opts;
const entrypointFilename = `entrypoint-${entrypointSeq()}-${entrypointLocation}-to-${entrypointTargetting}${
withExt ? '-withext' : ''
}.${entrypointExt}`;
const { isMjs: entrypointIsMjs, isCompiled: entrypointIsCompiled } = fileInfo(
entrypointFilename,
project.typeModule,
project.allowJs
);
let entrypointContent = 'let mod;\n';
entrypointContent += 'let testsRun = 0;\n';
if (entrypointIsMjs) {
entrypointContent += `import assert from 'assert';\n`;
} else {
entrypointContent += `const assert = require('assert');\n`;
entrypointContent += `${declareDynamicImportFunction}\n`;
}
entrypointContent += `async function main() {\n`;
for (const target of targets) {
// TODO re-enable these when we have outDir <-> rootDir mapping
if (target.srcName.includes('onlyout') && entrypointTargetting === 'src') continue;
if (target.srcName.includes('onlysrc') && entrypointTargetting === 'out') continue;
const {
ext: targetSrcExt,
isMjs: targetIsMjs,
isCompiled: targetIsCompiled,
} = fileInfo(target.srcName, target.typeModule, project.allowJs);
let targetExtPermutations = [''];
if (!target.isPackage) {
if (entrypointTargetting === 'out' && target.outExt !== target.srcExt) {
// TODO re-enable when we have out <-> src mapping
targetExtPermutations = [target.outExt];
} else if (target.srcExt !== target.outExt) {
targetExtPermutations = [target.srcExt, target.outExt];
} else {
targetExtPermutations = [target.srcExt];
}
}
const externalPackageSelfImportPermutations = target.isPackage ? [false, true] : [false];
for (const targetExt of targetExtPermutations) {
for (const externalPackageSelfImport of externalPackageSelfImportPermutations) {
entrypointContent += `\n// ${target.targetIdentifier}`;
if (target.isPackage) {
entrypointContent += ` node_modules package`;
if (externalPackageSelfImport) {
entrypointContent += ` self-import`;
}
} else {
entrypointContent += `.${targetExt}`;
}
entrypointContent += '\n';
// should specifier be relative or absolute?
let specifier: string;
if (externalPackageSelfImport) {
specifier = `../node_modules/${target.targetIdentifier}/self-import-${entrypointExt}.${entrypointExt}`;
} else if (target.isPackage) {
specifier = target.targetIdentifier;
} else {
if (entrypointTargetting === entrypointLocation) specifier = './';
else specifier = `../${entrypointTargetting}/`;
specifier += target.targetIdentifier;
if (target.isIndex) specifier += '-dir';
if (!target.isIndex && withExt) specifier += '.' + targetExt;
}
//#region SKIPPING
if (target.isNamedFile && !withExt) {
// Do not try to import cjs/cts without extension; node always requires these extensions
if (target.outExt === 'cjs') {
entrypointContent += `// skipping ${specifier} because we cannot omit extension from cjs / cts files; node always requires them\n`;
continue;
}
// Do not try to import mjs/mts unless experimental-specifier-resolution is turned on
if (target.outExt === 'mjs' && !project.experimentalSpecifierResolutionNode) {
entrypointContent += `// skipping ${specifier} because we cannot omit extension from mjs/mts unless experimental-specifier-resolution=node\n`;
continue;
}
// Do not try to import anything extensionless via ESM loader unless experimental-specifier-resolution is turned on
if ((targetIsMjs || entrypointIsMjs) && !project.experimentalSpecifierResolutionNode) {
entrypointContent += `// skipping ${specifier} because we cannot omit extension via esm loader unless experimental-specifier-resolution=node\n`;
continue;
}
}
if (
target.isPackage &&
isOneOf(target.packageStyle, ['empty-manifest', 'main-out-extensionless', 'main-src-extensionless']) &&
isOneOf(target.outExt, ['cjs', 'mjs'])
) {
entrypointContent += `// skipping ${specifier} because it points to a node_modules package that tries to omit file extension, and node does not allow omitting cjs/mjs extension\n`;
continue;
}
// Do not try to import a transpiled file if compiler options disagree with node's extension-based classification
if (!project.useTsNodeNext && targetIsCompiled) {
if (targetIsMjs && !project.typeModule) {
entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=commonjs\n`;
continue;
}
if (!targetIsMjs && project.typeModule) {
entrypointContent += `// skipping ${specifier} because it is compiled and compiler options disagree with node's module classification: extension=${targetSrcExt}, tsconfig module=esnext\n`;
continue;
}
}
// Do not try to import index from a directory if is forbidden by node's ESM resolver
if (target.isIndex) {
if ((targetIsMjs || entrypointIsMjs) && !project.experimentalSpecifierResolutionNode) {
entrypointContent += `// skipping ${specifier} because esm loader does not allow directory ./index imports unless experimental-specifier-resolution=node\n`;
continue;
}
if (target.outExt === 'cjs') {
entrypointContent += `// skipping ${specifier} because it relies on node automatically resolving a directory to index.cjs/cts , but node does not support those extensions for index.* files, only .js (and equivalents), .node, .json\n`;
continue;
}
}
//#endregion
// NOTE: if you try to explicitly import foo.ts, we will load foo.ts, EVEN IF you have `preferTsExts` off
const assertIsSrcOrOut = !target.inSrc
? 'out'
: !target.inOut
? 'src'
: project.preferSrc ||
(!target.isIndex && targetExt === target.srcExt && withExt) ||
target.srcExt === target.outExt || // <-- TODO re-enable when we have src <-> out mapping
(target.isPackage &&
isOneOf(target.packageStyle, ['main-src-with-extension', 'exports-src-with-extension']))
? 'src'
: 'out';
const assertHasExt = assertIsSrcOrOut === 'src' ? target.srcExt : target.outExt;
// If entrypoint is compiled as CJS, and *not* with TS's nodenext, then TS transforms `import` into `require`,
// so we must hack around the compiler to get a true `import`.
const entrypointMustUseDynamicImportHack =
!project.useTsNodeNext && entrypointIsCompiled && !entrypointIsMjs && !externalPackageSelfImport;
entrypointContent +=
entrypointExt === 'cjs' && (externalPackageSelfImport || !targetIsMjs)
? ` mod = await require('${specifier}');\n`
: entrypointMustUseDynamicImportHack
? ` mod = await dynamicImport('${specifier}');\n`
: ` mod = await import('${specifier}');\n`;
entrypointContent += ` assert.equal(mod.loc, '${assertIsSrcOrOut}');\n`;
entrypointContent += ` assert.equal(mod.targetIdentifier, '${target.targetIdentifier}');\n`;
entrypointContent += ` assert.equal(mod.ext, '${assertHasExt}');\n`;
entrypointContent += ` testsRun++;\n`;
}
}
}
entrypointContent += `}\n`;
entrypointContent += `const result = main().then(() => {return testsRun});\n`;
entrypointContent += `result.mark = 'marked';\n`;
if (entrypointIsMjs) {
entrypointContent += `export {result};\n`;
} else {
entrypointContent += `exports.result = result;\n`;
}
p.dir(entrypointLocation).addFile(entrypointFilename, entrypointContent);
return entrypointLocation + '/' + entrypointFilename;
}
/**
* Assertions happen here
*/
async function execute(t: T, p: FsProject, entrypoints: Entrypoint[]) {
//
// Install ts-node and try to import all the index-* files
//
const service = t.context.tsNodeUnderTest.register({
projectSearchDir: p.cwd,
});
process.__test_setloader__(t.context.tsNodeUnderTest.createEsmHooks(service));
for (const entrypoint of entrypoints) {
t.log(`Importing ${join(p.cwd, entrypoint)}`);
try {
const { result } = await dynamicImport(pathToFileURL(join(p.cwd, entrypoint)));
expect(result).toBeInstanceOf(Promise);
expect(result.mark).toBe('marked');
const testsRun = await result;
t.log(`Entrypoint ran ${testsRun} tests.`);
} catch (e) {
try {
const launchJsonPath = Path.resolve(__dirname, '../../.vscode/launch.json');
const launchJson = JSON.parse(fs.readFileSync(launchJsonPath, 'utf8'));
const config = launchJson.configurations.find((c: any) => c.name === 'Debug resolver test');
config.cwd = Path.join('${workspaceFolder}', Path.relative(Path.resolve(__dirname, '../..'), p.cwd));
config.program = `./${entrypoint}`;
fs.writeFileSync(launchJsonPath, JSON.stringify(launchJson, null, 2));
} catch {}
throw new Error(
[
(e as Error).message,
(e as Error).stack,
'',
'This is an error in a resolver test. It might be easier to investigate by running outside of the test suite.',
'To do that, try pasting this into your bash shell (windows invocation will be similar but maybe not identical):',
` ( cd ${p.cwd} ; node --loader ../../../esm.mjs ./${entrypoint} )`,
].join('\n')
);
}
}
}
function fileInfo(filename: string, typeModule: boolean, allowJs: boolean) {
const ext = filename.match(/\.(.*)$/)?.[1] ?? filename;
// ['ts', 'tsx', 'cts', 'mts', 'js', 'jsx', 'cjs', 'mjs']
return {
ext,
isMjs: ['mts', 'mjs'].includes(ext) ? true : ['cts', 'cjs'].includes(ext) ? false : typeModule,
isCompiled: allowJs || ['ts', 'tsx', 'jsx', 'mts', 'cts'].includes(ext),
};
}
function seqGenerator() {
let next = 0;
return function () {
return padStart('' + next++, 4, '0');
};
}