GeezerVison is showing github.com — esm: add back `globalPreload` tests and fix failing ones · nodejs/node@63aa5d7
File treeExpand file treeCollapse file tree Open diff view settings fixtures/es-module-loaders
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/main/eval_string.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -22,7 +22,7 @@ markBootstrapComplete(); | 22 | 22 |
| 23 | 23 | const source = getOptionValue('--eval'); | 24 | 24 | const print = getOptionValue('--print'); | 25 | | -const loadESM = getOptionValue('--import').length > 0; | | 25 | +const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; | 26 | 26 | if (getOptionValue('--input-type') === 'module') | 27 | 27 | evalModule(source, print); | 28 | 28 | else | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/hooks.js +52-19Lines changed: 52 additions & 19 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,6 +10,7 @@ const { | 10 | 10 | Promise, | 11 | 11 | SafeSet, | 12 | 12 | StringPrototypeSlice, | | 13 | + StringPrototypeStartsWith, | 13 | 14 | StringPrototypeToUpperCase, | 14 | 15 | globalThis, | 15 | 16 | } = primordials; | @@ -30,6 +31,7 @@ const { | 30 | 31 | ERR_INVALID_RETURN_PROPERTY_VALUE, | 31 | 32 | ERR_INVALID_RETURN_VALUE, | 32 | 33 | ERR_LOADER_CHAIN_INCOMPLETE, | | 34 | + ERR_UNKNOWN_BUILTIN_MODULE, | 33 | 35 | ERR_WORKER_UNSERIALIZABLE_ERROR, | 34 | 36 | } = require('internal/errors').codes; | 35 | 37 | const { URL } = require('internal/url'); | @@ -520,14 +522,14 @@ class HooksProxy { | 520 | 522 | this.#worker.on('exit', process.exit); | 521 | 523 | } | 522 | 524 |
| 523 | | - #waitForWorker() { | | 525 | + waitForWorker() { | 524 | 526 | if (!this.#isReady) { | 525 | 527 | const { kIsOnline } = require('internal/worker'); | 526 | 528 | if (!this.#worker[kIsOnline]) { | 527 | 529 | debug('wait for signal from worker'); | 528 | 530 | AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); | 529 | 531 | const response = this.#worker.receiveMessageSync(); | 530 | | - if (response.message.status === 'exit') { return; } | | 532 | + if (response == null || response.message.status === 'exit') { return; } | 531 | 533 | const { preloadScripts } = this.#unwrapMessage(response); | 532 | 534 | this.#executePreloadScripts(preloadScripts); | 533 | 535 | } | @@ -537,7 +539,7 @@ class HooksProxy { | 537 | 539 | } | 538 | 540 |
| 539 | 541 | async makeAsyncRequest(method, ...args) { | 540 | | - this.#waitForWorker(); | | 542 | + this.waitForWorker(); | 541 | 543 |
| 542 | 544 | MessageChannel ??= require('internal/worker/io').MessageChannel; | 543 | 545 | const asyncCommChannel = new MessageChannel(); | @@ -577,7 +579,7 @@ class HooksProxy { | 577 | 579 | } | 578 | 580 |
| 579 | 581 | makeSyncRequest(method, ...args) { | 580 | | - this.#waitForWorker(); | | 582 | + this.waitForWorker(); | 581 | 583 |
| 582 | 584 | // Pass work to the worker. | 583 | 585 | debug('post sync message to worker', { method, args }); | @@ -619,35 +621,66 @@ class HooksProxy { | 619 | 621 | } | 620 | 622 | } | 621 | 623 |
| | 624 | + #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; | | 625 | + | | 626 | + importMetaInitialize(meta, context, loader) { | | 627 | + this.#importMetaInitializer(meta, context, loader); | | 628 | + } | | 629 | + | 622 | 630 | #executePreloadScripts(preloadScripts) { | 623 | 631 | for (let i = 0; i < preloadScripts.length; i++) { | 624 | 632 | const { code, port } = preloadScripts[i]; | 625 | 633 | const { compileFunction } = require('vm'); | 626 | 634 | const preloadInit = compileFunction( | 627 | 635 | code, | 628 | | - ['getBuiltin', 'port'], | | 636 | + ['getBuiltin', 'port', 'setImportMetaCallback'], | 629 | 637 | { | 630 | 638 | filename: '<preload>', | 631 | 639 | }, | 632 | 640 | ); | | 641 | + let finished = false; | | 642 | + let replacedImportMetaInitializer = false; | | 643 | + let next = this.#importMetaInitializer; | 633 | 644 | const { BuiltinModule } = require('internal/bootstrap/realm'); | 634 | 645 | // Calls the compiled preload source text gotten from the hook | 635 | 646 | // Since the parameters are named we use positional parameters | 636 | 647 | // see compileFunction above to cross reference the names | 637 | | - FunctionPrototypeCall( | 638 | | - preloadInit, | 639 | | - globalThis, | 640 | | - // Param getBuiltin | 641 | | - (builtinName) => { | 642 | | - if (BuiltinModule.canBeRequiredByUsers(builtinName) && | 643 | | - BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { | 644 | | - return require(builtinName); | 645 | | - } | 646 | | - throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); | 647 | | - }, | 648 | | - // Param port | 649 | | - port, | 650 | | - ); | | 648 | + try { | | 649 | + FunctionPrototypeCall( | | 650 | + preloadInit, | | 651 | + globalThis, | | 652 | + // Param getBuiltin | | 653 | + (builtinName) => { | | 654 | + if (StringPrototypeStartsWith(builtinName, 'node:')) { | | 655 | + builtinName = StringPrototypeSlice(builtinName, 5); | | 656 | + } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { | | 657 | + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); | | 658 | + } | | 659 | + if (BuiltinModule.canBeRequiredByUsers(builtinName)) { | | 660 | + return require(builtinName); | | 661 | + } | | 662 | + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); | | 663 | + }, | | 664 | + // Param port | | 665 | + port, | | 666 | + // setImportMetaCallback | | 667 | + (fn) => { | | 668 | + if (finished || typeof fn !== 'function') { | | 669 | + throw new ERR_INVALID_ARG_TYPE('fn', fn); | | 670 | + } | | 671 | + replacedImportMetaInitializer = true; | | 672 | + const parent = next; | | 673 | + next = (meta, context) => { | | 674 | + return fn(meta, context, parent); | | 675 | + }; | | 676 | + }, | | 677 | + ); | | 678 | + } finally { | | 679 | + finished = true; | | 680 | + if (replacedImportMetaInitializer) { | | 681 | + this.#importMetaInitializer = next; | | 682 | + } | | 683 | + } | 651 | 684 | } | 652 | 685 | } | 653 | 686 | } | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -287,6 +287,11 @@ class DefaultModuleLoader { | 287 | 287 | meta = importMetaInitializer(meta, context, this); | 288 | 288 | return meta; | 289 | 289 | } | | 290 | + | | 291 | + /** | | 292 | + * No-op when no hooks have been supplied. | | 293 | + */ | | 294 | + forceLoadHooks() {} | 290 | 295 | } | 291 | 296 | ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); | 292 | 297 |
| @@ -359,6 +364,14 @@ class CustomizedModuleLoader extends DefaultModuleLoader { | 359 | 364 |
| 360 | 365 | return result; | 361 | 366 | } | | 367 | + | | 368 | + importMetaInitialize(meta, context) { | | 369 | + hooksProxy.importMetaInitialize(meta, context, this); | | 370 | + } | | 371 | + | | 372 | + forceLoadHooks() { | | 373 | + hooksProxy.waitForWorker(); | | 374 | + } | 362 | 375 | } | 363 | 376 |
| 364 | 377 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/esm_loader.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -36,6 +36,8 @@ module.exports = { | 36 | 36 | parentURL, | 37 | 37 | kEmptyObject, | 38 | 38 | )); | | 39 | + } else { | | 40 | + esmLoader.forceLoadHooks(); | 39 | 41 | } | 40 | 42 | await callback(esmLoader); | 41 | 43 | } catch (err) { | |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-esm-loader-hooks.mjs +113-11Lines changed: 113 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | import { spawnPromisified } from '../common/index.mjs'; | 2 | 2 | import * as fixtures from '../common/fixtures.mjs'; | 3 | 3 | import assert from 'node:assert'; | | 4 | +import os from 'node:os'; | 4 | 5 | import { execPath } from 'node:process'; | 5 | 6 | import { describe, it } from 'node:test'; | 6 | 7 |
| @@ -370,18 +371,119 @@ describe('Loader hooks', { concurrency: true }, () => { | 370 | 371 | }); | 371 | 372 | }); | 372 | 373 |
| 373 | | - it('should handle globalPreload returning undefined', async () => { | 374 | | - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | 375 | | - '--no-warnings', | 376 | | - '--experimental-loader', | 377 | | - 'data:text/javascript,export function globalPreload(){}', | 378 | | - fixtures.path('empty.js'), | 379 | | - ]); | | 374 | + describe('globalPreload', () => { | | 375 | + it('should handle globalPreload returning undefined', async () => { | | 376 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 377 | + '--no-warnings', | | 378 | + '--experimental-loader', | | 379 | + 'data:text/javascript,export function globalPreload(){}', | | 380 | + fixtures.path('empty.js'), | | 381 | + ]); | 380 | 382 |
| 381 | | - assert.strictEqual(stderr, ''); | 382 | | - assert.strictEqual(stdout, ''); | 383 | | - assert.strictEqual(code, 0); | 384 | | - assert.strictEqual(signal, null); | | 383 | + assert.strictEqual(stderr, ''); | | 384 | + assert.strictEqual(stdout, ''); | | 385 | + assert.strictEqual(code, 0); | | 386 | + assert.strictEqual(signal, null); | | 387 | + }); | | 388 | + | | 389 | + it('should handle loading node:test', async () => { | | 390 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 391 | + '--no-warnings', | | 392 | + '--experimental-loader', | | 393 | + 'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}', | | 394 | + fixtures.path('empty.js'), | | 395 | + ]); | | 396 | + | | 397 | + assert.strictEqual(stderr, ''); | | 398 | + assert.match(stdout, /\n# pass 1\r?\n/); | | 399 | + assert.strictEqual(code, 0); | | 400 | + assert.strictEqual(signal, null); | | 401 | + }); | | 402 | + | | 403 | + it('should handle loading node:os with node: prefix', async () => { | | 404 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 405 | + '--no-warnings', | | 406 | + '--experimental-loader', | | 407 | + 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}', | | 408 | + fixtures.path('empty.js'), | | 409 | + ]); | | 410 | + | | 411 | + assert.strictEqual(stderr, ''); | | 412 | + assert.strictEqual(stdout.trim(), os.arch()); | | 413 | + assert.strictEqual(code, 0); | | 414 | + assert.strictEqual(signal, null); | | 415 | + }); | | 416 | + | | 417 | + // `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter). | | 418 | + it('should handle loading builtin module without node: prefix', async () => { | | 419 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 420 | + '--no-warnings', | | 421 | + '--experimental-loader', | | 422 | + 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}', | | 423 | + fixtures.path('empty.js'), | | 424 | + ]); | | 425 | + | | 426 | + assert.strictEqual(stderr, ''); | | 427 | + assert.strictEqual(stdout.trim(), os.arch()); | | 428 | + assert.strictEqual(code, 0); | | 429 | + assert.strictEqual(signal, null); | | 430 | + }); | | 431 | + | | 432 | + it('should throw when loading node:test without node: prefix', async () => { | | 433 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 434 | + '--no-warnings', | | 435 | + '--experimental-loader', | | 436 | + 'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}', | | 437 | + fixtures.path('empty.js'), | | 438 | + ]); | | 439 | + | | 440 | + assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/); | | 441 | + assert.strictEqual(stdout, ''); | | 442 | + assert.strictEqual(code, 1); | | 443 | + assert.strictEqual(signal, null); | | 444 | + }); | | 445 | + | | 446 | + it('should register globals set from globalPreload', async () => { | | 447 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 448 | + '--no-warnings', | | 449 | + '--experimental-loader', | | 450 | + 'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}', | | 451 | + '--print', 'myGlobal', | | 452 | + ]); | | 453 | + | | 454 | + assert.strictEqual(stderr, ''); | | 455 | + assert.strictEqual(stdout.trim(), '4'); | | 456 | + assert.strictEqual(code, 0); | | 457 | + assert.strictEqual(signal, null); | | 458 | + }); | | 459 | + | | 460 | + it('should log console.log calls returned from globalPreload', async () => { | | 461 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 462 | + '--no-warnings', | | 463 | + '--experimental-loader', | | 464 | + 'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}', | | 465 | + fixtures.path('empty.js'), | | 466 | + ]); | | 467 | + | | 468 | + assert.strictEqual(stderr, ''); | | 469 | + assert.strictEqual(stdout.trim(), 'Hello from globalPreload'); | | 470 | + assert.strictEqual(code, 0); | | 471 | + assert.strictEqual(signal, null); | | 472 | + }); | | 473 | + | | 474 | + it('should crash if globalPreload returns code that throws', async () => { | | 475 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 476 | + '--no-warnings', | | 477 | + '--experimental-loader', | | 478 | + 'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}', | | 479 | + fixtures.path('empty.js'), | | 480 | + ]); | | 481 | + | | 482 | + assert.match(stderr, /error from globalPreload/); | | 483 | + assert.strictEqual(stdout, ''); | | 484 | + assert.strictEqual(code, 1); | | 485 | + assert.strictEqual(signal, null); | | 486 | + }); | 385 | 487 | }); | 386 | 488 |
| 387 | 489 | it('should be fine to call `process.removeAllListeners("beforeExit")` from the main thread', async () => { | |
Collapse file Copy file name to clipboard +45Lines changed: 45 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs | | 2 | +import '../common/index.mjs'; | | 3 | +import assert from 'assert/strict'; | | 4 | + | | 5 | +// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs | | 6 | +import mock from 'node:mock'; | | 7 | + | | 8 | +mock('node:events', { | | 9 | + EventEmitter: 'This is mocked!' | | 10 | +}); | | 11 | + | | 12 | +// This resolves to node:events | | 13 | +// It is intercepted by mock-loader and doesn't return the normal value | | 14 | +assert.deepStrictEqual(await import('events'), Object.defineProperty({ | | 15 | + __proto__: null, | | 16 | + EventEmitter: 'This is mocked!' | | 17 | +}, Symbol.toStringTag, { | | 18 | + enumerable: false, | | 19 | + value: 'Module' | | 20 | +})); | | 21 | + | | 22 | +const mutator = mock('node:events', { | | 23 | + EventEmitter: 'This is mocked v2!' | | 24 | +}); | | 25 | + | | 26 | +// It is intercepted by mock-loader and doesn't return the normal value. | | 27 | +// This is resolved separately from the import above since the specifiers | | 28 | +// are different. | | 29 | +const mockedV2 = await import('node:events'); | | 30 | +assert.deepStrictEqual(mockedV2, Object.defineProperty({ | | 31 | + __proto__: null, | | 32 | + EventEmitter: 'This is mocked v2!' | | 33 | +}, Symbol.toStringTag, { | | 34 | + enumerable: false, | | 35 | + value: 'Module' | | 36 | +})); | | 37 | + | | 38 | +mutator.EventEmitter = 'This is mocked v3!'; | | 39 | +assert.deepStrictEqual(mockedV2, Object.defineProperty({ | | 40 | + __proto__: null, | | 41 | + EventEmitter: 'This is mocked v3!' | | 42 | +}, Symbol.toStringTag, { | | 43 | + enumerable: false, | | 44 | + value: 'Module' | | 45 | +})); |
|