1import { spawnPromisified } from '../common/index.mjs';
2import * as fixtures from '../common/fixtures.mjs';
3import assert from 'node:assert';
4import { execPath } from 'node:process';
5import { describe, it } from 'node:test';
6
7const setupArgs = [
8  '--no-warnings',
9  '--input-type=module',
10  '--eval',
11];
12const commonInput = 'import os from "node:os"; console.log(os)';
13const commonArgs = [
14  ...setupArgs,
15  commonInput,
16];
17
18describe('ESM: loader chaining', { concurrency: true }, () => {
19  it('should load unadulterated source when there are no loaders', async () => {
20    const { code, stderr, stdout } = await spawnPromisified(
21      execPath,
22      [
23        ...setupArgs,
24        'import fs from "node:fs"; console.log(typeof fs?.constants?.F_OK )',
25      ],
26      { encoding: 'utf8' },
27    );
28
29    assert.strictEqual(stderr, '');
30    assert.match(stdout, /number/); // node:fs is an object
31    assert.strictEqual(code, 0);
32  });
33
34  it('should load properly different source when only load changes something', async () => {
35    const { code, stderr, stdout } = await spawnPromisified(
36      execPath,
37      [
38        '--loader',
39        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
40        '--loader',
41        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
42        '--loader',
43        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
44        ...commonArgs,
45      ],
46      { encoding: 'utf8' },
47    );
48
49    assert.strictEqual(stderr, '');
50    assert.match(stdout, /load passthru/);
51    assert.match(stdout, /resolve passthru/);
52    assert.match(stdout, /foo/);
53    assert.strictEqual(code, 0);
54  });
55
56  it('should result in proper output from multiple changes in resolve hooks', async () => {
57    const { code, stderr, stdout } = await spawnPromisified(
58      execPath,
59      [
60        '--loader',
61        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
62        '--loader',
63        fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
64        '--loader',
65        fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
66        '--loader',
67        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
68        ...commonArgs,
69      ],
70      { encoding: 'utf8' },
71    );
72
73    assert.strictEqual(stderr, '');
74    assert.match(stdout, /resolve 42/); // It did go thru resolve-42
75    assert.match(stdout, /foo/); // LIFO, so resolve-foo won
76    assert.strictEqual(code, 0);
77  });
78
79  it('should respect modified context within resolve chain', async () => {
80    const { code, stderr, stdout } = await spawnPromisified(
81      execPath,
82      [
83        '--loader',
84        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
85        '--loader',
86        fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
87        '--loader',
88        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
89        '--loader',
90        fixtures.fileURL('es-module-loaders', 'loader-load-receiving-modified-context.mjs'),
91        '--loader',
92        fixtures.fileURL('es-module-loaders', 'loader-load-passing-modified-context.mjs'),
93        ...commonArgs,
94      ],
95      { encoding: 'utf8' },
96    );
97
98    assert.strictEqual(stderr, '');
99    assert.match(stdout, /bar/);
100    assert.strictEqual(code, 0);
101  });
102
103  it('should accept only the correct arguments', async () => {
104    const { stdout } = await spawnPromisified(
105      execPath,
106      [
107        '--loader',
108        fixtures.fileURL('es-module-loaders', 'loader-log-args.mjs'),
109        '--loader',
110        fixtures.fileURL('es-module-loaders', 'loader-with-too-many-args.mjs'),
111        ...commonArgs,
112      ],
113      { encoding: 'utf8' },
114    );
115
116    assert.match(stdout, /^resolve arg count: 3$/m);
117    assert.match(stdout, /specifier: 'node:os'/);
118    assert.match(stdout, /next: \[AsyncFunction: nextResolve\]/);
119
120    assert.match(stdout, /^load arg count: 3$/m);
121    assert.match(stdout, /url: 'node:os'/);
122    assert.match(stdout, /next: \[AsyncFunction: nextLoad\]/);
123  });
124
125  it('should result in proper output from multiple changes in resolve hooks', async () => {
126    const { code, stderr, stdout } = await spawnPromisified(
127      execPath,
128      [
129        '--loader',
130        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
131        '--loader',
132        fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
133        '--loader',
134        fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
135        '--loader',
136        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
137        ...commonArgs,
138      ],
139      { encoding: 'utf8' },
140    );
141
142    assert.strictEqual(stderr, '');
143    assert.match(stdout, /resolve foo/); // It did go thru resolve-foo
144    assert.match(stdout, /42/); // LIFO, so resolve-42 won
145    assert.strictEqual(code, 0);
146  });
147
148  it('should provide the correct "next" fn when multiple calls to next within same loader', async () => {
149    const { code, stderr, stdout } = await spawnPromisified(
150      execPath,
151      [
152        '--loader',
153        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
154        '--loader',
155        fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
156        '--loader',
157        fixtures.fileURL('es-module-loaders', 'loader-resolve-multiple-next-calls.mjs'),
158        '--loader',
159        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
160        ...commonArgs,
161      ],
162      { encoding: 'utf8' },
163    );
164
165    const countFoos = stdout.match(/resolve foo/g)?.length;
166
167    assert.strictEqual(stderr, '');
168    assert.strictEqual(countFoos, 2);
169    assert.strictEqual(code, 0);
170  });
171
172  it('should use the correct `name` for next<HookName>\'s function', async () => {
173    const { code, stderr, stdout } = await spawnPromisified(
174      execPath,
175      [
176        '--loader',
177        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
178        '--loader',
179        fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
180        '--loader',
181        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
182        ...commonArgs,
183      ],
184      { encoding: 'utf8' },
185    );
186
187    assert.strictEqual(stderr, '');
188    assert.match(stdout, /next<HookName>: nextResolve/);
189    assert.strictEqual(code, 0);
190  });
191
192  it('should throw for incomplete resolve chain, citing errant loader & hook', async () => {
193    const { code, stderr, stdout } = await spawnPromisified(
194      execPath,
195      [
196        '--loader',
197        fixtures.fileURL('es-module-loaders', 'loader-resolve-incomplete.mjs'),
198        '--loader',
199        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
200        '--loader',
201        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
202        ...commonArgs,
203      ],
204      { encoding: 'utf8' },
205    );
206    assert.match(stdout, /resolve passthru/);
207    assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
208    assert.match(stderr, /loader-resolve-incomplete\.mjs/);
209    assert.match(stderr, /'resolve'/);
210    assert.strictEqual(code, 1);
211  });
212
213  it('should NOT throw when nested resolve hook signaled a short circuit', async () => {
214    const { code, stderr, stdout } = await spawnPromisified(
215      execPath,
216      [
217        '--loader',
218        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
219        '--loader',
220        fixtures.fileURL('es-module-loaders', 'loader-resolve-next-modified.mjs'),
221        '--loader',
222        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
223        ...commonArgs,
224      ],
225      { encoding: 'utf8' },
226    );
227
228    assert.strictEqual(stderr, '');
229    assert.strictEqual(stdout.trim(), 'foo');
230    assert.strictEqual(code, 0);
231  });
232
233  it('should NOT throw when nested load hook signaled a short circuit', async () => {
234    const { code, stderr, stdout } = await spawnPromisified(
235      execPath,
236      [
237        '--loader',
238        fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
239        '--loader',
240        fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
241        '--loader',
242        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
243        '--loader',
244        fixtures.fileURL('es-module-loaders', 'loader-load-next-modified.mjs'),
245        ...commonArgs,
246      ],
247      { encoding: 'utf8' },
248    );
249
250    assert.strictEqual(stderr, '');
251    assert.match(stdout, /421/);
252    assert.strictEqual(code, 0);
253  });
254
255  it('should allow loaders to influence subsequent loader resolutions', async () => {
256    const { code, stderr } = await spawnPromisified(
257      execPath,
258      [
259        '--loader',
260        fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
261        '--loader',
262        'xxx/loader-resolve-strip-yyy.mjs',
263        ...commonArgs,
264      ],
265      { encoding: 'utf8', cwd: fixtures.path('es-module-loaders') },
266    );
267
268    assert.strictEqual(stderr, '');
269    assert.strictEqual(code, 0);
270  });
271
272  it('should throw when the resolve chain is broken', async () => {
273    const { code, stderr, stdout } = await spawnPromisified(
274      execPath,
275      [
276        '--loader',
277        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
278        '--loader',
279        fixtures.fileURL('es-module-loaders', 'loader-resolve-incomplete.mjs'),
280        '--loader',
281        fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
282        ...commonArgs,
283      ],
284      { encoding: 'utf8' },
285    );
286
287    assert.doesNotMatch(stdout, /resolve passthru/);
288    assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
289    assert.match(stderr, /loader-resolve-incomplete\.mjs/);
290    assert.match(stderr, /'resolve'/);
291    assert.strictEqual(code, 1);
292  });
293
294  it('should throw for incomplete load chain, citing errant loader & hook', async () => {
295    const { code, stderr, stdout } = await spawnPromisified(
296      execPath,
297      [
298        '--loader',
299        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
300        '--loader',
301        fixtures.fileURL('es-module-loaders', 'loader-load-incomplete.mjs'),
302        '--loader',
303        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
304        ...commonArgs,
305      ],
306      { encoding: 'utf8' },
307    );
308
309    assert.match(stdout, /load passthru/);
310    assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
311    assert.match(stderr, /loader-load-incomplete\.mjs/);
312    assert.match(stderr, /'load'/);
313    assert.strictEqual(code, 1);
314  });
315
316  it('should throw when the load chain is broken', async () => {
317    const { code, stderr, stdout } = await spawnPromisified(
318      execPath,
319      [
320        '--loader',
321        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
322        '--loader',
323        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
324        '--loader',
325        fixtures.fileURL('es-module-loaders', 'loader-load-incomplete.mjs'),
326        ...commonArgs,
327      ],
328      { encoding: 'utf8' },
329    );
330
331    assert.doesNotMatch(stdout, /load passthru/);
332    assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
333    assert.match(stderr, /loader-load-incomplete\.mjs/);
334    assert.match(stderr, /'load'/);
335    assert.strictEqual(code, 1);
336  });
337
338  it('should throw when invalid `specifier` argument passed to `nextResolve`', async () => {
339    const { code, stderr } = await spawnPromisified(
340      execPath,
341      [
342        '--loader',
343        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
344        '--loader',
345        fixtures.fileURL('es-module-loaders', 'loader-resolve-bad-next-specifier.mjs'),
346        ...commonArgs,
347      ],
348      { encoding: 'utf8' },
349    );
350
351    assert.strictEqual(code, 1);
352    assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
353    assert.match(stderr, /loader-resolve-bad-next-specifier\.mjs/);
354    assert.match(stderr, /'resolve' hook's nextResolve\(\) specifier/);
355  });
356
357  it('should throw when resolve hook is invalid', async () => {
358    const { code, stderr } = await spawnPromisified(
359      execPath,
360      [
361        '--loader',
362        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
363        '--loader',
364        fixtures.fileURL('es-module-loaders', 'loader-resolve-null-return.mjs'),
365        ...commonArgs,
366      ],
367      { encoding: 'utf8' },
368    );
369
370    assert.strictEqual(code, 1);
371    assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
372    assert.match(stderr, /loader-resolve-null-return\.mjs/);
373    assert.match(stderr, /'resolve' hook's nextResolve\(\)/);
374    assert.match(stderr, /an object/);
375    assert.match(stderr, /got null/);
376  });
377
378  it('should throw when invalid `context` argument passed to `nextResolve`', async () => {
379    const { code, stderr } = await spawnPromisified(
380      execPath,
381      [
382        '--loader',
383        fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
384        '--loader',
385        fixtures.fileURL('es-module-loaders', 'loader-resolve-bad-next-context.mjs'),
386        ...commonArgs,
387      ],
388      { encoding: 'utf8' },
389    );
390
391    assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
392    assert.match(stderr, /loader-resolve-bad-next-context\.mjs/);
393    assert.match(stderr, /'resolve' hook's nextResolve\(\) context/);
394    assert.strictEqual(code, 1);
395  });
396
397  it('should throw when load hook is invalid', async () => {
398    const { code, stderr } = await spawnPromisified(
399      execPath,
400      [
401        '--loader',
402        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
403        '--loader',
404        fixtures.fileURL('es-module-loaders', 'loader-load-null-return.mjs'),
405        ...commonArgs,
406      ],
407      { encoding: 'utf8' },
408    );
409
410    assert.strictEqual(code, 1);
411    assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
412    assert.match(stderr, /loader-load-null-return\.mjs/);
413    assert.match(stderr, /'load' hook's nextLoad\(\)/);
414    assert.match(stderr, /an object/);
415    assert.match(stderr, /got null/);
416  });
417
418  it('should throw when invalid `url` argument passed to `nextLoad`', async () => {
419    const { code, stderr } = await spawnPromisified(
420      execPath,
421      [
422        '--loader',
423        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
424        '--loader',
425        fixtures.fileURL('es-module-loaders', 'loader-load-bad-next-url.mjs'),
426        ...commonArgs,
427      ],
428      { encoding: 'utf8' },
429    );
430
431    assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
432    assert.match(stderr, /loader-load-bad-next-url\.mjs/);
433    assert.match(stderr, /'load' hook's nextLoad\(\) url/);
434    assert.strictEqual(code, 1);
435  });
436
437  it('should throw when invalid `url` argument passed to `nextLoad`', async () => {
438    const { code, stderr } = await spawnPromisified(
439      execPath,
440      [
441        '--loader',
442        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
443        '--loader',
444        fixtures.fileURL('es-module-loaders', 'loader-load-impersonating-next-url.mjs'),
445        ...commonArgs,
446      ],
447      { encoding: 'utf8' },
448    );
449
450    assert.match(stderr, /ERR_INVALID_ARG_VALUE/);
451    assert.match(stderr, /loader-load-impersonating-next-url\.mjs/);
452    assert.match(stderr, /'load' hook's nextLoad\(\) url/);
453    assert.strictEqual(code, 1);
454  });
455
456  it('should throw when invalid `context` argument passed to `nextLoad`', async () => {
457    const { code, stderr } = await spawnPromisified(
458      execPath,
459      [
460        '--loader',
461        fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
462        '--loader',
463        fixtures.fileURL('es-module-loaders', 'loader-load-bad-next-context.mjs'),
464        ...commonArgs,
465      ],
466      { encoding: 'utf8' },
467    );
468    assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
469    assert.match(stderr, /loader-load-bad-next-context\.mjs/);
470    assert.match(stderr, /'load' hook's nextLoad\(\) context/);
471    assert.strictEqual(code, 1);
472  });
473
474  it('should allow loaders to influence subsequent loader `import()` calls in `resolve`', async () => {
475    const { code, stderr, stdout } = await spawnPromisified(
476      execPath,
477      [
478        '--loader',
479        fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
480        '--loader',
481        fixtures.fileURL('es-module-loaders', 'loader-resolve-dynamic-import.mjs'),
482        ...commonArgs,
483      ],
484      { encoding: 'utf8' },
485    );
486    assert.strictEqual(stderr, '');
487    assert.match(stdout, /resolve dynamic import/); // It did go thru resolve-dynamic
488    assert.strictEqual(code, 0);
489  });
490
491  it('should allow loaders to influence subsequent loader `import()` calls in `load`', async () => {
492    const { code, stderr, stdout } = await spawnPromisified(
493      execPath,
494      [
495        '--loader',
496        fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
497        '--loader',
498        fixtures.fileURL('es-module-loaders', 'loader-load-dynamic-import.mjs'),
499        ...commonArgs,
500      ],
501      { encoding: 'utf8' },
502    );
503    assert.strictEqual(stderr, '');
504    assert.match(stdout, /load dynamic import/); // It did go thru load-dynamic
505    assert.strictEqual(code, 0);
506  });
507});
508