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