Skip to content
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
37 changes: 16 additions & 21 deletions packages/egg/test/cluster1/app_worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('test/cluster1/app_worker.test.ts', () => {
beforeAll(async () => {
app = cluster('apps/app-server');
await app.ready();
});
}, 60000);
afterAll(() => app.close());

// FIXME: unsable
Expand Down Expand Up @@ -148,33 +148,28 @@ function rawRequest(port: number, path: string) {
socket.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: close\r\n\r\n`);
});

function resolveOnce() {
if (!settled) {
settled = true;
resolve(response);
}
}

function rejectOnce(err: Error) {
if (!settled) {
settled = true;
reject(err);
}
}
const settle = (callback: () => void) => {
if (settled) return;
settled = true;
socket.setTimeout(0);
callback();
};

socket.setTimeout(5000, () =>
settle(() => {
socket.destroy();
reject(new Error(`rawRequest timeout after 5s, partial response: ${response}`));
}),
);
socket.setEncoding('utf8');
socket.setTimeout(5000);
socket.on('data', (chunk) => {
response += chunk;
});
socket.on('timeout', () => {
socket.destroy(new Error('Timed out waiting for raw HTTP response'));
});
socket.on('end', resolveOnce);
socket.on('error', rejectOnce);
socket.on('error', (err) => settle(() => reject(err)));
socket.on('end', () => settle(() => resolve(response)));
socket.on('close', (hadError) => {
if (!hadError) {
resolveOnce();
settle(() => resolve(response));
}
});
});
Expand Down
4 changes: 3 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
"scripts": {
"typecheck": "tsgo --noEmit"
},
"dependencies": {},
"dependencies": {
"@eggjs/typings": "workspace:*"
},
"devDependencies": {
"coffee": "catalog:",
"mm": "catalog:",
Expand Down
39 changes: 39 additions & 0 deletions packages/utils/src/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from 'node:path';
import { pathToFileURL, fileURLToPath } from 'node:url';
import { debuglog } from 'node:util';

import type { BundleModuleLoader } from '@eggjs/typings';

import { ImportResolveError } from './error/index.ts';

const debug = debuglog('egg/utils/import');
Expand Down Expand Up @@ -394,7 +396,44 @@ export function setSnapshotModuleLoader(loader: SnapshotModuleLoader): void {
isESM = false;
}

export type { BundleModuleLoader } from '@eggjs/typings';

type BundleModuleGlobalThis = typeof globalThis & {
__EGG_BUNDLE_MODULE_LOADER__: BundleModuleLoader | undefined;
};

const bundleModuleGlobalThis = globalThis as BundleModuleGlobalThis;

function normalizeBundleModulePath(filepath: string): string {
return filepath.split(path.win32.sep).join(path.posix.sep);
}

/**
* Register a bundle module loader. Uses globalThis so that bundled and
* external copies of @eggjs/utils share the same loader.
*
* The loader receives a POSIX-normalized filepath or virtual specifier before
* normal resolution runs. Return `undefined` to fall through to the default
* import path. Non-undefined hits use the same default unwrapping semantics as
* normal imports, including `importDefaultOnly` and double-default `__esModule`
* compatibility.
*/
export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void {
bundleModuleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader;
}
Comment thread
killagu marked this conversation as resolved.
Comment thread
killagu marked this conversation as resolved.
Comment thread
killagu marked this conversation as resolved.

export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
const _bundleModuleLoader = bundleModuleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__;
if (_bundleModuleLoader) {
const hit = _bundleModuleLoader(normalizeBundleModulePath(filepath));
if (hit !== undefined) {
let obj = hit as any;
if (obj?.default?.__esModule === true && 'default' in obj.default) obj = obj.default;
if (options?.importDefaultOnly && obj && typeof obj === 'object' && 'default' in obj) obj = obj.default;
return obj;
Comment thread
killagu marked this conversation as resolved.
}
}

const moduleFilePath = importResolve(filepath, options);

if (_snapshotModuleLoader) {
Expand Down
1 change: 1 addition & 0 deletions packages/utils/test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ exports[`test/index.test.ts > export all > should keep checking 1`] = `
"importResolve",
"isESM",
"isSupportTypeScript",
"setBundleModuleLoader",
"setSnapshotModuleLoader",
]
`;
90 changes: 90 additions & 0 deletions packages/utils/test/bundle-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { strict as assert } from 'node:assert';
import path from 'node:path';

import { afterEach, describe, it } from 'vitest';

import { importModule, setBundleModuleLoader } from '../src/import.ts';
import { getFilepath } from './helper.ts';

describe('test/bundle-import.test.ts', () => {
afterEach(() => {
setBundleModuleLoader(undefined);
});

it('returns the real module when no bundle loader is registered', async () => {
const result = await importModule(getFilepath('esm'));
assert.ok(result);
assert.equal(typeof result, 'object');
});

it('intercepts importModule with the registered loader', async () => {
const seen: string[] = [];
const fakeModule = { default: { hello: 'bundle' }, other: 'stuff' };
setBundleModuleLoader((p) => {
seen.push(p);
if (p.endsWith('/fixtures/esm')) return fakeModule;
});

const result = await importModule(getFilepath('esm'));
assert.deepEqual(result, fakeModule);
assert.ok(seen.some((p) => p.endsWith('/fixtures/esm')));
});

it('honors importDefaultOnly when the bundle hit has a default key', async () => {
setBundleModuleLoader(() => ({ default: { greet: 'hi' }, other: 'x' }));

const result = await importModule(getFilepath('esm'), { importDefaultOnly: true });
assert.deepEqual(result, { greet: 'hi' });
});

it('keeps non-default bundle hits when importDefaultOnly is enabled', async () => {
const fakeModule = { named: 'bundle' };
setBundleModuleLoader(() => fakeModule);

const result = await importModule(getFilepath('esm'), { importDefaultOnly: true });
assert.deepEqual(result, fakeModule);
});

it('keeps null bundle hits when importDefaultOnly is enabled', async () => {
setBundleModuleLoader(() => null);

const result = await importModule(getFilepath('esm'), { importDefaultOnly: true });
assert.equal(result, null);
});

it('unwraps __esModule double-default shape', async () => {
setBundleModuleLoader(() => ({
default: { __esModule: true, default: { fn: 'bundled' } },
}));

const result = await importModule(getFilepath('esm'));
assert.equal(result.__esModule, true);
assert.deepEqual(result.default, { fn: 'bundled' });
});

it('falls through to normal import when loader returns undefined', async () => {
setBundleModuleLoader(() => undefined);

const result = await importModule(getFilepath('esm'));
assert.ok(result);
assert.equal(result.default.foo, 'bar');
});

it('serves virtual specifiers from the loader without requiring them on disk', async () => {
const fakeModule = { virtual: true };
setBundleModuleLoader((p) => (p === 'virtual/not-on-disk' ? fakeModule : undefined));

const result = await importModule('virtual/not-on-disk');
assert.deepEqual(result, fakeModule);
});

it('normalizes Windows-style bundle paths before loader lookup', async () => {
const fakeModule = { windows: true };
const filepath = getFilepath('esm').split(path.posix.sep).join(path.win32.sep);

setBundleModuleLoader((p) => (p.endsWith('/fixtures/esm') ? fakeModule : undefined));

const result = await importModule(filepath);
assert.deepEqual(result, fakeModule);
});
});
7 changes: 7 additions & 0 deletions packages/utils/test/fixtures/esm/es-module-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
__esModule: true,
default: {
foo: 'bar',
one: 1,
},
};
9 changes: 9 additions & 0 deletions packages/utils/test/import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ describe('test/import.test.ts', () => {
assert.deepEqual(Object.keys(obj), ['foo', 'one']);
assert.equal(obj.foo, 'bar');
assert.equal(obj.one, 1);

obj = await importModule(getFilepath('esm/es-module-default.js'));
assert.deepEqual(Object.keys(obj), ['__esModule', 'default']);
assert.deepEqual(obj.default, { foo: 'bar', one: 1 });

obj = await importModule(getFilepath('esm/es-module-default.js'), {
importDefaultOnly: true,
});
assert.deepEqual(obj, { foo: 'bar', one: 1 });
});

it('should work on tshy without dist', async () => {
Expand Down
Loading