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
7describe('Loader hooks', { concurrency: true }, () => {
8  it('are called with all expected arguments', async () => {
9    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
10      '--no-warnings',
11      '--experimental-loader',
12      fixtures.fileURL('es-module-loaders/hooks-input.mjs'),
13      fixtures.path('es-modules/json-modules.mjs'),
14    ]);
15
16    assert.strictEqual(stderr, '');
17    assert.strictEqual(code, 0);
18    assert.strictEqual(signal, null);
19
20    const lines = stdout.split('\n');
21    assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/);
22    assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/);
23    assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/);
24    assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/);
25    assert.strictEqual(lines[4], '');
26    assert.strictEqual(lines.length, 5);
27  });
28
29  it('are called with all expected arguments using register function', async () => {
30    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
31      '--no-warnings',
32      '--experimental-loader=data:text/javascript,',
33      '--input-type=module',
34      '--eval',
35      "import { register } from 'node:module';" +
36      `register(${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-input.mjs'))});` +
37      `await import(${JSON.stringify(fixtures.fileURL('es-modules/json-modules.mjs'))});`,
38    ]);
39
40    assert.strictEqual(stderr, '');
41    assert.strictEqual(code, 0);
42    assert.strictEqual(signal, null);
43
44    const lines = stdout.split('\n');
45    assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/);
46    assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/);
47    assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/);
48    assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/);
49    assert.strictEqual(lines[4], '');
50    assert.strictEqual(lines.length, 5);
51  });
52
53  describe('should handle never-settling hooks in ESM files', { concurrency: true }, () => {
54    it('top-level await of a never-settling resolve', async () => {
55      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
56        '--no-warnings',
57        '--experimental-loader',
58        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
59        fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.mjs'),
60      ]);
61
62      assert.strictEqual(stderr, '');
63      assert.match(stdout, /^should be output\r?\n$/);
64      assert.strictEqual(code, 13);
65      assert.strictEqual(signal, null);
66    });
67
68    it('top-level await of a never-settling load', async () => {
69      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
70        '--no-warnings',
71        '--experimental-loader',
72        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
73        fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.mjs'),
74      ]);
75
76      assert.strictEqual(stderr, '');
77      assert.match(stdout, /^should be output\r?\n$/);
78      assert.strictEqual(code, 13);
79      assert.strictEqual(signal, null);
80    });
81
82
83    it('top-level await of a race of never-settling hooks', async () => {
84      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
85        '--no-warnings',
86        '--experimental-loader',
87        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
88        fixtures.path('es-module-loaders/never-settling-resolve-step/race.mjs'),
89      ]);
90
91      assert.strictEqual(stderr, '');
92      assert.match(stdout, /^true\r?\n$/);
93      assert.strictEqual(code, 0);
94      assert.strictEqual(signal, null);
95    });
96
97    it('import.meta.resolve of a never-settling resolve', async () => {
98      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
99        '--no-warnings',
100        '--experimental-loader',
101        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
102        fixtures.path('es-module-loaders/never-settling-resolve-step/import.meta.never-resolve.mjs'),
103      ]);
104
105      assert.strictEqual(stderr, '');
106      assert.match(stdout, /^should be output\r?\n$/);
107      assert.strictEqual(code, 13);
108      assert.strictEqual(signal, null);
109    });
110  });
111
112  describe('should handle never-settling hooks in CJS files', { concurrency: true }, () => {
113    it('never-settling resolve', async () => {
114      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
115        '--no-warnings',
116        '--experimental-loader',
117        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
118        fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.cjs'),
119      ]);
120
121      assert.strictEqual(stderr, '');
122      assert.match(stdout, /^should be output\r?\n$/);
123      assert.strictEqual(code, 0);
124      assert.strictEqual(signal, null);
125    });
126
127
128    it('never-settling load', async () => {
129      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
130        '--no-warnings',
131        '--experimental-loader',
132        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
133        fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.cjs'),
134      ]);
135
136      assert.strictEqual(stderr, '');
137      assert.match(stdout, /^should be output\r?\n$/);
138      assert.strictEqual(code, 0);
139      assert.strictEqual(signal, null);
140    });
141
142    it('race of never-settling hooks', async () => {
143      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
144        '--no-warnings',
145        '--experimental-loader',
146        fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'),
147        fixtures.path('es-module-loaders/never-settling-resolve-step/race.cjs'),
148      ]);
149
150      assert.strictEqual(stderr, '');
151      assert.match(stdout, /^true\r?\n$/);
152      assert.strictEqual(code, 0);
153      assert.strictEqual(signal, null);
154    });
155  });
156
157  it('should not leak internals or expose import.meta.resolve', async () => {
158    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
159      '--no-warnings',
160      '--experimental-loader',
161      fixtures.fileURL('es-module-loaders/loader-edge-cases.mjs'),
162      fixtures.path('empty.js'),
163    ]);
164
165    assert.strictEqual(stderr, '');
166    assert.strictEqual(stdout, '');
167    assert.strictEqual(code, 0);
168    assert.strictEqual(signal, null);
169  });
170
171  it('should be fine to call `process.exit` from a custom async hook', async () => {
172    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
173      '--no-warnings',
174      '--experimental-loader',
175      'data:text/javascript,export function load(a,b,next){if(a==="data:exit")process.exit(42);return next(a,b)}',
176      '--input-type=module',
177      '--eval',
178      'import "data:exit"',
179    ]);
180
181    assert.strictEqual(stderr, '');
182    assert.strictEqual(stdout, '');
183    assert.strictEqual(code, 42);
184    assert.strictEqual(signal, null);
185  });
186
187  it('should be fine to call `process.exit` from a custom sync hook', async () => {
188    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
189      '--no-warnings',
190      '--experimental-loader',
191      'data:text/javascript,export function resolve(a,b,next){if(a==="exit:")process.exit(42);return next(a,b)}',
192      '--input-type=module',
193      '--eval',
194      'import "data:text/javascript,import.meta.resolve(%22exit:%22)"',
195    ]);
196
197    assert.strictEqual(stderr, '');
198    assert.strictEqual(stdout, '');
199    assert.strictEqual(code, 42);
200    assert.strictEqual(signal, null);
201  });
202
203  it('should be fine to call `process.exit` from the loader thread top-level', async () => {
204    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
205      '--no-warnings',
206      '--experimental-loader',
207      'data:text/javascript,process.exit(42)',
208      fixtures.path('empty.js'),
209    ]);
210
211    assert.strictEqual(stderr, '');
212    assert.strictEqual(stdout, '');
213    assert.strictEqual(code, 42);
214    assert.strictEqual(signal, null);
215  });
216
217  describe('should handle a throwing top-level body', () => {
218    it('should handle regular Error object', async () => {
219      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
220        '--no-warnings',
221        '--experimental-loader',
222        'data:text/javascript,throw new Error("error message")',
223        fixtures.path('empty.js'),
224      ]);
225
226      assert.match(stderr, /Error: error message\r?\n/);
227      assert.strictEqual(stdout, '');
228      assert.strictEqual(code, 1);
229      assert.strictEqual(signal, null);
230    });
231
232    it('should handle null', async () => {
233      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
234        '--no-warnings',
235        '--experimental-loader',
236        'data:text/javascript,throw null',
237        fixtures.path('empty.js'),
238      ]);
239
240      assert.match(stderr, /\nnull\r?\n/);
241      assert.strictEqual(stdout, '');
242      assert.strictEqual(code, 1);
243      assert.strictEqual(signal, null);
244    });
245
246    it('should handle undefined', async () => {
247      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
248        '--no-warnings',
249        '--experimental-loader',
250        'data:text/javascript,throw undefined',
251        fixtures.path('empty.js'),
252      ]);
253
254      assert.match(stderr, /\nundefined\r?\n/);
255      assert.strictEqual(stdout, '');
256      assert.strictEqual(code, 1);
257      assert.strictEqual(signal, null);
258    });
259
260    it('should handle boolean', async () => {
261      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
262        '--no-warnings',
263        '--experimental-loader',
264        'data:text/javascript,throw true',
265        fixtures.path('empty.js'),
266      ]);
267
268      assert.match(stderr, /\ntrue\r?\n/);
269      assert.strictEqual(stdout, '');
270      assert.strictEqual(code, 1);
271      assert.strictEqual(signal, null);
272    });
273
274    it('should handle empty plain object', async () => {
275      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
276        '--no-warnings',
277        '--experimental-loader',
278        'data:text/javascript,throw {}',
279        fixtures.path('empty.js'),
280      ]);
281
282      assert.match(stderr, /\n\{\}\r?\n/);
283      assert.strictEqual(stdout, '');
284      assert.strictEqual(code, 1);
285      assert.strictEqual(signal, null);
286    });
287
288    it('should handle plain object', async () => {
289      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
290        '--no-warnings',
291        '--experimental-loader',
292        'data:text/javascript,throw {fn(){},symbol:Symbol("symbol"),u:undefined}',
293        fixtures.path('empty.js'),
294      ]);
295
296      assert.match(stderr, /\n\{ fn: \[Function: fn\], symbol: Symbol\(symbol\), u: undefined \}\r?\n/);
297      assert.strictEqual(stdout, '');
298      assert.strictEqual(code, 1);
299      assert.strictEqual(signal, null);
300    });
301
302    it('should handle number', async () => {
303      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
304        '--no-warnings',
305        '--experimental-loader',
306        'data:text/javascript,throw 1',
307        fixtures.path('empty.js'),
308      ]);
309
310      assert.match(stderr, /\n1\r?\n/);
311      assert.strictEqual(stdout, '');
312      assert.strictEqual(code, 1);
313      assert.strictEqual(signal, null);
314    });
315
316    it('should handle bigint', async () => {
317      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
318        '--no-warnings',
319        '--experimental-loader',
320        'data:text/javascript,throw 1n',
321        fixtures.path('empty.js'),
322      ]);
323
324      assert.match(stderr, /\n1\r?\n/);
325      assert.strictEqual(stdout, '');
326      assert.strictEqual(code, 1);
327      assert.strictEqual(signal, null);
328    });
329
330    it('should handle string', async () => {
331      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
332        '--no-warnings',
333        '--experimental-loader',
334        'data:text/javascript,throw "literal string"',
335        fixtures.path('empty.js'),
336      ]);
337
338      assert.match(stderr, /\nliteral string\r?\n/);
339      assert.strictEqual(stdout, '');
340      assert.strictEqual(code, 1);
341      assert.strictEqual(signal, null);
342    });
343
344    it('should handle symbol', async () => {
345      const { code, signal, stdout } = await spawnPromisified(execPath, [
346        '--experimental-loader',
347        'data:text/javascript,throw Symbol("symbol descriptor")',
348        fixtures.path('empty.js'),
349      ]);
350
351      // Throwing a symbol doesn't produce any output
352      assert.strictEqual(stdout, '');
353      assert.strictEqual(code, 1);
354      assert.strictEqual(signal, null);
355    });
356
357    it('should handle function', async () => {
358      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
359        '--no-warnings',
360        '--experimental-loader',
361        'data:text/javascript,throw function fnName(){}',
362        fixtures.path('empty.js'),
363      ]);
364
365      assert.match(stderr, /\n\[Function: fnName\]\r?\n/);
366      assert.strictEqual(stdout, '');
367      assert.strictEqual(code, 1);
368      assert.strictEqual(signal, null);
369    });
370  });
371
372  describe('globalPreload', () => {
373    it('should emit warning', async () => {
374      const { stderr } = await spawnPromisified(execPath, [
375        '--experimental-loader',
376        'data:text/javascript,export function globalPreload(){}',
377        '--experimental-loader',
378        'data:text/javascript,export function globalPreload(){return""}',
379        fixtures.path('empty.js'),
380      ]);
381
382      assert.strictEqual(stderr.match(/`globalPreload` is an experimental feature/g).length, 1);
383    });
384  });
385
386  it('should be fine to call `process.removeAllListeners("beforeExit")` from the main thread', async () => {
387    const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
388      '--no-warnings',
389      '--experimental-loader',
390      'data:text/javascript,export function load(a,b,c){return new Promise(d=>setTimeout(()=>d(c(a,b)),99))}',
391      '--input-type=module',
392      '--eval',
393      'setInterval(() => process.removeAllListeners("beforeExit"),1).unref();await import("data:text/javascript,")',
394    ]);
395
396    assert.strictEqual(stderr, '');
397    assert.strictEqual(stdout, '');
398    assert.strictEqual(code, 0);
399    assert.strictEqual(signal, null);
400  });
401
402  describe('`initialize`/`register`', () => {
403    it('should invoke `initialize` correctly', async () => {
404      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
405        '--no-warnings',
406        '--experimental-loader',
407        fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'),
408        '--input-type=module',
409        '--eval',
410        'import os from "node:os";',
411      ]);
412
413      assert.strictEqual(stderr, '');
414      assert.deepStrictEqual(stdout.split('\n'), ['hooks initialize 1', '']);
415      assert.strictEqual(code, 0);
416      assert.strictEqual(signal, null);
417    });
418
419    it('should allow communicating with loader via `register` ports', async () => {
420      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
421        '--no-warnings',
422        '--input-type=module',
423        '--eval',
424        `
425        import {MessageChannel} from 'node:worker_threads';
426        import {register} from 'node:module';
427        import {once} from 'node:events';
428        const {port1, port2} = new MessageChannel();
429        port1.on('message', (msg) => {
430          console.log('message', msg);
431        });
432        const result = register(
433          ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize-port.mjs'))},
434          {data: port2, transferList: [port2]},
435        );
436        console.log('register', result);
437
438        const timeout = setTimeout(() => {}, 2**31 - 1); // to keep the process alive.
439        await Promise.all([
440          once(port1, 'message').then(() => once(port1, 'message')),
441          import('node:os'),
442        ]);
443        clearTimeout(timeout);
444        port1.close();
445        `,
446      ]);
447
448      assert.strictEqual(stderr, '');
449      assert.deepStrictEqual(stdout.split('\n'), [ 'register undefined',
450                                                   'message initialize',
451                                                   'message resolve node:os',
452                                                   '' ]);
453
454      assert.strictEqual(code, 0);
455      assert.strictEqual(signal, null);
456    });
457
458    it('should have `register` accept URL objects as `parentURL`', async () => {
459      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
460        '--no-warnings',
461        '--import',
462        `data:text/javascript,${encodeURIComponent(
463          'import{ register } from "node:module";' +
464          'import { pathToFileURL } from "node:url";' +
465          'register("./hooks-initialize.mjs", pathToFileURL("./"));'
466        )}`,
467        '--input-type=module',
468        '--eval',
469        `
470        import {register} from 'node:module';
471        register(
472          ${JSON.stringify(fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'))},
473          new URL('data:'),
474        );
475
476        import('node:os').then((result) => {
477          console.log(JSON.stringify(result));
478        });
479        `,
480      ], { cwd: fixtures.fileURL('es-module-loaders/') });
481
482      assert.strictEqual(stderr, '');
483      assert.deepStrictEqual(stdout.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}', ''].sort());
484
485      assert.strictEqual(code, 0);
486      assert.strictEqual(signal, null);
487    });
488
489    it('should have `register` work with cjs', async () => {
490      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
491        '--no-warnings',
492        '--input-type=commonjs',
493        '--eval',
494        `
495        'use strict';
496        const {register} = require('node:module');
497        register(
498          ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))},
499        );
500        register(
501          ${JSON.stringify(fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'))},
502        );
503
504        import('node:os').then((result) => {
505          console.log(JSON.stringify(result));
506        });
507        `,
508      ]);
509
510      assert.strictEqual(stderr, '');
511      assert.deepStrictEqual(stdout.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}', ''].sort());
512
513      assert.strictEqual(code, 0);
514      assert.strictEqual(signal, null);
515    });
516
517    it('`register` should work with `require`', async () => {
518      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
519        '--no-warnings',
520        '--require',
521        fixtures.path('es-module-loaders/register-loader.cjs'),
522        '--input-type=module',
523        '--eval',
524        'import "node:os";',
525      ]);
526
527      assert.strictEqual(stderr, '');
528      assert.deepStrictEqual(stdout.split('\n'), ['resolve passthru', 'resolve passthru', '']);
529      assert.strictEqual(code, 0);
530      assert.strictEqual(signal, null);
531    });
532
533    it('`register` should work with `import`', async () => {
534      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
535        '--no-warnings',
536        '--import',
537        fixtures.fileURL('es-module-loaders/register-loader.mjs'),
538        '--input-type=module',
539        '--eval',
540        'import "node:os"',
541      ]);
542
543      assert.strictEqual(stderr, '');
544      assert.deepStrictEqual(stdout.split('\n'), ['resolve passthru', '']);
545      assert.strictEqual(code, 0);
546      assert.strictEqual(signal, null);
547    });
548
549    it('should execute `initialize` in sequence', async () => {
550      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
551        '--no-warnings',
552        '--input-type=module',
553        '--eval',
554        `
555        import {register} from 'node:module';
556        console.log('result 1', register(
557          ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))}
558        ));
559        console.log('result 2', register(
560          ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))}
561        ));
562
563        await import('node:os');
564        `,
565      ]);
566
567      assert.strictEqual(stderr, '');
568      assert.deepStrictEqual(stdout.split('\n'), [ 'hooks initialize 1',
569                                                   'result 1 undefined',
570                                                   'hooks initialize 2',
571                                                   'result 2 undefined',
572                                                   '' ]);
573      assert.strictEqual(code, 0);
574      assert.strictEqual(signal, null);
575    });
576
577    it('should handle `initialize` returning never-settling promise', async () => {
578      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
579        '--no-warnings',
580        '--input-type=module',
581        '--eval',
582        `
583        import {register} from 'node:module';
584        register('data:text/javascript,export function initialize(){return new Promise(()=>{})}');
585        `,
586      ]);
587
588      assert.strictEqual(stderr, '');
589      assert.strictEqual(stdout, '');
590      assert.strictEqual(code, 13);
591      assert.strictEqual(signal, null);
592    });
593
594    it('should handle `initialize` returning rejecting promise', async () => {
595      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
596        '--no-warnings',
597        '--input-type=module',
598        '--eval',
599        `
600        import {register} from 'node:module';
601        register('data:text/javascript,export function initialize(){return Promise.reject()}');
602        `,
603      ]);
604
605      assert.match(stderr, /undefined\r?\n/);
606      assert.strictEqual(stdout, '');
607      assert.strictEqual(code, 1);
608      assert.strictEqual(signal, null);
609    });
610
611    it('should handle `initialize` throwing null', async () => {
612      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
613        '--no-warnings',
614        '--input-type=module',
615        '--eval',
616        `
617        import {register} from 'node:module';
618        register('data:text/javascript,export function initialize(){throw null}');
619        `,
620      ]);
621
622      assert.match(stderr, /null\r?\n/);
623      assert.strictEqual(stdout, '');
624      assert.strictEqual(code, 1);
625      assert.strictEqual(signal, null);
626    });
627
628    it('should be fine to call `process.exit` from a initialize hook', async () => {
629      const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
630        '--no-warnings',
631        '--input-type=module',
632        '--eval',
633        `
634        import {register} from 'node:module';
635        register('data:text/javascript,export function initialize(){process.exit(42);}');
636        `,
637      ]);
638
639      assert.strictEqual(stderr, '');
640      assert.strictEqual(stdout, '');
641      assert.strictEqual(code, 42);
642      assert.strictEqual(signal, null);
643    });
644  });
645});
646