GeezerVison is showing github.com — esm: add back `globalPreload` tests and fix failing ones · nodejs/node@a559747
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 |
|---|
@@ -24,7 +24,7 @@ markBootstrapComplete(); | 24 | 24 |
| 25 | 25 | const source = getOptionValue('--eval'); | 26 | 26 | const print = getOptionValue('--print'); | 27 | | -const loadESM = getOptionValue('--import').length > 0; | | 27 | +const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0; | 28 | 28 | if (getOptionValue('--input-type') === 'module') | 29 | 29 | evalModule(source, print); | 30 | 30 | 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 { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); | @@ -521,14 +523,14 @@ class HooksProxy { | 521 | 523 | this.#worker.on('exit', process.exit); | 522 | 524 | } | 523 | 525 |
| 524 | | - #waitForWorker() { | | 526 | + waitForWorker() { | 525 | 527 | if (!this.#isReady) { | 526 | 528 | const { kIsOnline } = require('internal/worker'); | 527 | 529 | if (!this.#worker[kIsOnline]) { | 528 | 530 | debug('wait for signal from worker'); | 529 | 531 | AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); | 530 | 532 | const response = this.#worker.receiveMessageSync(); | 531 | | - if (response.message.status === 'exit') { return; } | | 533 | + if (response == null || response.message.status === 'exit') { return; } | 532 | 534 | const { preloadScripts } = this.#unwrapMessage(response); | 533 | 535 | this.#executePreloadScripts(preloadScripts); | 534 | 536 | } | @@ -538,7 +540,7 @@ class HooksProxy { | 538 | 540 | } | 539 | 541 |
| 540 | 542 | async makeAsyncRequest(method, ...args) { | 541 | | - this.#waitForWorker(); | | 543 | + this.waitForWorker(); | 542 | 544 |
| 543 | 545 | MessageChannel ??= require('internal/worker/io').MessageChannel; | 544 | 546 | const asyncCommChannel = new MessageChannel(); | @@ -578,7 +580,7 @@ class HooksProxy { | 578 | 580 | } | 579 | 581 |
| 580 | 582 | makeSyncRequest(method, ...args) { | 581 | | - this.#waitForWorker(); | | 583 | + this.waitForWorker(); | 582 | 584 |
| 583 | 585 | // Pass work to the worker. | 584 | 586 | debug('post sync message to worker', { method, args }); | @@ -620,35 +622,66 @@ class HooksProxy { | 620 | 622 | } | 621 | 623 | } | 622 | 624 |
| | 625 | + #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; | | 626 | + | | 627 | + importMetaInitialize(meta, context, loader) { | | 628 | + this.#importMetaInitializer(meta, context, loader); | | 629 | + } | | 630 | + | 623 | 631 | #executePreloadScripts(preloadScripts) { | 624 | 632 | for (let i = 0; i < preloadScripts.length; i++) { | 625 | 633 | const { code, port } = preloadScripts[i]; | 626 | 634 | const { compileFunction } = require('vm'); | 627 | 635 | const preloadInit = compileFunction( | 628 | 636 | code, | 629 | | - ['getBuiltin', 'port'], | | 637 | + ['getBuiltin', 'port', 'setImportMetaCallback'], | 630 | 638 | { | 631 | 639 | filename: '<preload>', | 632 | 640 | }, | 633 | 641 | ); | | 642 | + let finished = false; | | 643 | + let replacedImportMetaInitializer = false; | | 644 | + let next = this.#importMetaInitializer; | 634 | 645 | const { BuiltinModule } = require('internal/bootstrap/realm'); | 635 | 646 | // Calls the compiled preload source text gotten from the hook | 636 | 647 | // Since the parameters are named we use positional parameters | 637 | 648 | // see compileFunction above to cross reference the names | 638 | | - FunctionPrototypeCall( | 639 | | - preloadInit, | 640 | | - globalThis, | 641 | | - // Param getBuiltin | 642 | | - (builtinName) => { | 643 | | - if (BuiltinModule.canBeRequiredByUsers(builtinName) && | 644 | | - BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { | 645 | | - return require(builtinName); | 646 | | - } | 647 | | - throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); | 648 | | - }, | 649 | | - // Param port | 650 | | - port, | 651 | | - ); | | 649 | + try { | | 650 | + FunctionPrototypeCall( | | 651 | + preloadInit, | | 652 | + globalThis, | | 653 | + // Param getBuiltin | | 654 | + (builtinName) => { | | 655 | + if (StringPrototypeStartsWith(builtinName, 'node:')) { | | 656 | + builtinName = StringPrototypeSlice(builtinName, 5); | | 657 | + } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { | | 658 | + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); | | 659 | + } | | 660 | + if (BuiltinModule.canBeRequiredByUsers(builtinName)) { | | 661 | + return require(builtinName); | | 662 | + } | | 663 | + throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); | | 664 | + }, | | 665 | + // Param port | | 666 | + port, | | 667 | + // setImportMetaCallback | | 668 | + (fn) => { | | 669 | + if (finished || typeof fn !== 'function') { | | 670 | + throw new ERR_INVALID_ARG_TYPE('fn', fn); | | 671 | + } | | 672 | + replacedImportMetaInitializer = true; | | 673 | + const parent = next; | | 674 | + next = (meta, context) => { | | 675 | + return fn(meta, context, parent); | | 676 | + }; | | 677 | + }, | | 678 | + ); | | 679 | + } finally { | | 680 | + finished = true; | | 681 | + if (replacedImportMetaInitializer) { | | 682 | + this.#importMetaInitializer = next; | | 683 | + } | | 684 | + } | 652 | 685 | } | 653 | 686 | } | 654 | 687 | } | |
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 |
|---|
@@ -277,6 +277,11 @@ class DefaultModuleLoader { | 277 | 277 | meta = importMetaInitializer(meta, context, this); | 278 | 278 | return meta; | 279 | 279 | } | | 280 | + | | 281 | + /** | | 282 | + * No-op when no hooks have been supplied. | | 283 | + */ | | 284 | + forceLoadHooks() {} | 280 | 285 | } | 281 | 286 | ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null); | 282 | 287 |
| @@ -349,6 +354,14 @@ class CustomizedModuleLoader extends DefaultModuleLoader { | 349 | 354 |
| 350 | 355 | return result; | 351 | 356 | } | | 357 | + | | 358 | + importMetaInitialize(meta, context) { | | 359 | + hooksProxy.importMetaInitialize(meta, context, this); | | 360 | + } | | 361 | + | | 362 | + forceLoadHooks() { | | 363 | + hooksProxy.waitForWorker(); | | 364 | + } | 352 | 365 | } | 353 | 366 |
| 354 | 367 |
| |
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 |
| @@ -422,18 +423,119 @@ describe('Loader hooks', { concurrency: true }, () => { | 422 | 423 | }); | 423 | 424 | }); | 424 | 425 |
| 425 | | - it('should handle globalPreload returning undefined', async () => { | 426 | | - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | 427 | | - '--no-warnings', | 428 | | - '--experimental-loader', | 429 | | - 'data:text/javascript,export function globalPreload(){}', | 430 | | - fixtures.path('empty.js'), | 431 | | - ]); | | 426 | + describe('globalPreload', () => { | | 427 | + it('should handle globalPreload returning undefined', async () => { | | 428 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 429 | + '--no-warnings', | | 430 | + '--experimental-loader', | | 431 | + 'data:text/javascript,export function globalPreload(){}', | | 432 | + fixtures.path('empty.js'), | | 433 | + ]); | 432 | 434 |
| 433 | | - assert.strictEqual(stderr, ''); | 434 | | - assert.strictEqual(stdout, ''); | 435 | | - assert.strictEqual(code, 0); | 436 | | - assert.strictEqual(signal, null); | | 435 | + assert.strictEqual(stderr, ''); | | 436 | + assert.strictEqual(stdout, ''); | | 437 | + assert.strictEqual(code, 0); | | 438 | + assert.strictEqual(signal, null); | | 439 | + }); | | 440 | + | | 441 | + it('should handle loading node:test', async () => { | | 442 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 443 | + '--no-warnings', | | 444 | + '--experimental-loader', | | 445 | + 'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}', | | 446 | + fixtures.path('empty.js'), | | 447 | + ]); | | 448 | + | | 449 | + assert.strictEqual(stderr, ''); | | 450 | + assert.match(stdout, /\n# pass 1\r?\n/); | | 451 | + assert.strictEqual(code, 0); | | 452 | + assert.strictEqual(signal, null); | | 453 | + }); | | 454 | + | | 455 | + it('should handle loading node:os with node: prefix', async () => { | | 456 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 457 | + '--no-warnings', | | 458 | + '--experimental-loader', | | 459 | + 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}', | | 460 | + fixtures.path('empty.js'), | | 461 | + ]); | | 462 | + | | 463 | + assert.strictEqual(stderr, ''); | | 464 | + assert.strictEqual(stdout.trim(), os.arch()); | | 465 | + assert.strictEqual(code, 0); | | 466 | + assert.strictEqual(signal, null); | | 467 | + }); | | 468 | + | | 469 | + // `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter). | | 470 | + it('should handle loading builtin module without node: prefix', async () => { | | 471 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 472 | + '--no-warnings', | | 473 | + '--experimental-loader', | | 474 | + 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}', | | 475 | + fixtures.path('empty.js'), | | 476 | + ]); | | 477 | + | | 478 | + assert.strictEqual(stderr, ''); | | 479 | + assert.strictEqual(stdout.trim(), os.arch()); | | 480 | + assert.strictEqual(code, 0); | | 481 | + assert.strictEqual(signal, null); | | 482 | + }); | | 483 | + | | 484 | + it('should throw when loading node:test without node: prefix', async () => { | | 485 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 486 | + '--no-warnings', | | 487 | + '--experimental-loader', | | 488 | + 'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}', | | 489 | + fixtures.path('empty.js'), | | 490 | + ]); | | 491 | + | | 492 | + assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/); | | 493 | + assert.strictEqual(stdout, ''); | | 494 | + assert.strictEqual(code, 1); | | 495 | + assert.strictEqual(signal, null); | | 496 | + }); | | 497 | + | | 498 | + it('should register globals set from globalPreload', async () => { | | 499 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 500 | + '--no-warnings', | | 501 | + '--experimental-loader', | | 502 | + 'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}', | | 503 | + '--print', 'myGlobal', | | 504 | + ]); | | 505 | + | | 506 | + assert.strictEqual(stderr, ''); | | 507 | + assert.strictEqual(stdout.trim(), '4'); | | 508 | + assert.strictEqual(code, 0); | | 509 | + assert.strictEqual(signal, null); | | 510 | + }); | | 511 | + | | 512 | + it('should log console.log calls returned from globalPreload', async () => { | | 513 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 514 | + '--no-warnings', | | 515 | + '--experimental-loader', | | 516 | + 'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}', | | 517 | + fixtures.path('empty.js'), | | 518 | + ]); | | 519 | + | | 520 | + assert.strictEqual(stderr, ''); | | 521 | + assert.strictEqual(stdout.trim(), 'Hello from globalPreload'); | | 522 | + assert.strictEqual(code, 0); | | 523 | + assert.strictEqual(signal, null); | | 524 | + }); | | 525 | + | | 526 | + it('should crash if globalPreload returns code that throws', async () => { | | 527 | + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ | | 528 | + '--no-warnings', | | 529 | + '--experimental-loader', | | 530 | + 'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}', | | 531 | + fixtures.path('empty.js'), | | 532 | + ]); | | 533 | + | | 534 | + assert.match(stderr, /error from globalPreload/); | | 535 | + assert.strictEqual(stdout, ''); | | 536 | + assert.strictEqual(code, 1); | | 537 | + assert.strictEqual(signal, null); | | 538 | + }); | 437 | 539 | }); | 438 | 540 |
| 439 | 541 | 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 | +})); |
|