Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

esm: add back globalPreload tests #48779

Merged
merged 2 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/internal/main/eval_string.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ markBootstrapComplete();

const source = getOptionValue('--eval');
const print = getOptionValue('--print');
const loadESM = getOptionValue('--import').length > 0;
const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
JakobJingleheimer marked this conversation as resolved.
Show resolved Hide resolved
if (getOptionValue('--input-type') === 'module')
evalModule(source, print);
else {
Expand Down
71 changes: 52 additions & 19 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
Promise,
SafeSet,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeToUpperCase,
globalThis,
} = primordials;
Expand All @@ -30,6 +31,7 @@ const {
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_LOADER_CHAIN_INCOMPLETE,
ERR_UNKNOWN_BUILTIN_MODULE,
ERR_WORKER_UNSERIALIZABLE_ERROR,
} = require('internal/errors').codes;
const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors');
Expand Down Expand Up @@ -521,14 +523,14 @@ class HooksProxy {
this.#worker.on('exit', process.exit);
}

#waitForWorker() {
waitForWorker() {
if (!this.#isReady) {
const { kIsOnline } = require('internal/worker');
if (!this.#worker[kIsOnline]) {
debug('wait for signal from worker');
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
const response = this.#worker.receiveMessageSync();
if (response.message.status === 'exit') { return; }
if (response == null || response.message.status === 'exit') { return; }
const { preloadScripts } = this.#unwrapMessage(response);
this.#executePreloadScripts(preloadScripts);
}
Expand All @@ -538,7 +540,7 @@ class HooksProxy {
}

async makeAsyncRequest(method, ...args) {
this.#waitForWorker();
this.waitForWorker();

MessageChannel ??= require('internal/worker/io').MessageChannel;
const asyncCommChannel = new MessageChannel();
Expand Down Expand Up @@ -578,7 +580,7 @@ class HooksProxy {
}

makeSyncRequest(method, ...args) {
this.#waitForWorker();
this.waitForWorker();

// Pass work to the worker.
debug('post sync message to worker', { method, args });
Expand Down Expand Up @@ -620,35 +622,66 @@ class HooksProxy {
}
}

#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;

importMetaInitialize(meta, context, loader) {
this.#importMetaInitializer(meta, context, loader);
}

#executePreloadScripts(preloadScripts) {
for (let i = 0; i < preloadScripts.length; i++) {
const { code, port } = preloadScripts[i];
const { compileFunction } = require('vm');
const preloadInit = compileFunction(
code,
['getBuiltin', 'port'],
['getBuiltin', 'port', 'setImportMetaCallback'],
{
filename: '<preload>',
},
);
let finished = false;
let replacedImportMetaInitializer = false;
let next = this.#importMetaInitializer;
const { BuiltinModule } = require('internal/bootstrap/realm');
// Calls the compiled preload source text gotten from the hook
// Since the parameters are named we use positional parameters
// see compileFunction above to cross reference the names
FunctionPrototypeCall(
preloadInit,
globalThis,
// Param getBuiltin
(builtinName) => {
if (BuiltinModule.canBeRequiredByUsers(builtinName) &&
BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
return require(builtinName);
}
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
},
// Param port
port,
);
try {
FunctionPrototypeCall(
preloadInit,
globalThis,
// Param getBuiltin
(builtinName) => {
if (StringPrototypeStartsWith(builtinName, 'node:')) {
builtinName = StringPrototypeSlice(builtinName, 5);
} else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
aduh95 marked this conversation as resolved.
Show resolved Hide resolved
throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
}
if (BuiltinModule.canBeRequiredByUsers(builtinName)) {
return require(builtinName);
}
throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
},
// Param port
port,
// setImportMetaCallback
(fn) => {
if (finished || typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('fn', fn);
}
replacedImportMetaInitializer = true;
const parent = next;
next = (meta, context) => {
return fn(meta, context, parent);
};
},
);
} finally {
finished = true;
if (replacedImportMetaInitializer) {
this.#importMetaInitializer = next;
}
}
}
}
}
Expand Down
13 changes: 13 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ class DefaultModuleLoader {
meta = importMetaInitializer(meta, context, this);
return meta;
}

/**
* No-op when no hooks have been supplied.
*/
forceLoadHooks() {}
}
ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null);

Expand Down Expand Up @@ -349,6 +354,14 @@ class CustomizedModuleLoader extends DefaultModuleLoader {

return result;
}

importMetaInitialize(meta, context) {
hooksProxy.importMetaInitialize(meta, context, this);
}

forceLoadHooks() {
hooksProxy.waitForWorker();
}
}


Expand Down
2 changes: 2 additions & 0 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ module.exports = {
parentURL,
kEmptyObject,
));
} else {
esmLoader.forceLoadHooks();
}
await callback(esmLoader);
} catch (err) {
Expand Down
124 changes: 113 additions & 11 deletions test/es-module/test-esm-loader-hooks.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import os from 'node:os';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';

Expand Down Expand Up @@ -422,18 +423,119 @@ describe('Loader hooks', { concurrency: true }, () => {
});
});

it('should handle globalPreload returning undefined', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){}',
fixtures.path('empty.js'),
]);
describe('globalPreload', () => {
it('should handle globalPreload returning undefined', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){}',
fixtures.path('empty.js'),
]);

assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should handle loading node:test', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}',
fixtures.path('empty.js'),
]);

assert.strictEqual(stderr, '');
assert.match(stdout, /\n# pass 1\r?\n/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should handle loading node:os with node: prefix', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}',
fixtures.path('empty.js'),
]);

assert.strictEqual(stderr, '');
assert.strictEqual(stdout.trim(), os.arch());
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

// `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter).
it('should handle loading builtin module without node: prefix', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}',
fixtures.path('empty.js'),
]);

assert.strictEqual(stderr, '');
assert.strictEqual(stdout.trim(), os.arch());
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should throw when loading node:test without node: prefix', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}',
fixtures.path('empty.js'),
]);

assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});

it('should register globals set from globalPreload', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}',
'--print', 'myGlobal',
]);

assert.strictEqual(stderr, '');
assert.strictEqual(stdout.trim(), '4');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should log console.log calls returned from globalPreload', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}',
fixtures.path('empty.js'),
]);

assert.strictEqual(stderr, '');
assert.strictEqual(stdout.trim(), 'Hello from globalPreload');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});

it('should crash if globalPreload returns code that throws', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}',
fixtures.path('empty.js'),
]);

assert.match(stderr, /error from globalPreload/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

it('should be fine to call `process.removeAllListeners("beforeExit")` from the main thread', async () => {
Expand Down
45 changes: 45 additions & 0 deletions test/es-module/test-esm-loader-mock.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
import '../common/index.mjs';
import assert from 'assert/strict';

// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
import mock from 'node:mock';

mock('node:events', {
EventEmitter: 'This is mocked!'
});

// This resolves to node:events
// It is intercepted by mock-loader and doesn't return the normal value
assert.deepStrictEqual(await import('events'), Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));

const mutator = mock('node:events', {
EventEmitter: 'This is mocked v2!'
});

// It is intercepted by mock-loader and doesn't return the normal value.
// This is resolved separately from the import above since the specifiers
// are different.
const mockedV2 = await import('node:events');
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v2!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));

mutator.EventEmitter = 'This is mocked v3!';
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v3!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));
Loading