-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: add shared egg typings package #5895
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="@eggjs/typings" /> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| { | ||
| "name": "@eggjs/typings", | ||
| "version": "1.0.0-beta.9", | ||
| "description": "Shared typings for Egg projects", | ||
| "keywords": [ | ||
| "egg", | ||
| "typescript", | ||
| "typings" | ||
| ], | ||
| "homepage": "https://github.com/eggjs/egg/tree/next/packages/typings", | ||
| "bugs": { | ||
| "url": "https://github.com/eggjs/egg/issues" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "fengmk2 <fengmk2@gmail.com>", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/eggjs/egg.git", | ||
| "directory": "packages/typings" | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "type": "module", | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": "./src/index.ts", | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "exports": { | ||
| ".": "./dist/index.js", | ||
| "./package.json": "./package.json" | ||
| } | ||
| }, | ||
| "scripts": { | ||
| "typecheck": "tsgo --noEmit" | ||
| }, | ||
| "devDependencies": { | ||
| "@eggjs/tsconfig": "workspace:*", | ||
| "typescript": "catalog:" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22.18.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Module loader for bundled egg apps. Called with the raw `importModule()` | ||
| * filepath (posix-normalized) before `importResolve`, so bundled apps can | ||
| * serve modules that no longer exist on disk. Return `undefined` to fall | ||
| * through to the standard import path. | ||
| */ | ||
| export type BundleModuleLoader = (filepath: string) => unknown; | ||
|
|
||
| declare global { | ||
| var __EGG_BUNDLE_MODULE_LOADER__: BundleModuleLoader | undefined; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "../../tsconfig.json" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import { defineConfig } from 'tsdown'; | ||
|
|
||
| export default defineConfig({ | ||
| entry: { | ||
| index: 'src/index.ts', | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
|
|
@@ -394,7 +396,32 @@ export function setSnapshotModuleLoader(loader: SnapshotModuleLoader): void { | |
| isESM = false; | ||
| } | ||
|
|
||
| export type { BundleModuleLoader } from '@eggjs/typings'; | ||
|
|
||
| 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. | ||
| */ | ||
| export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void { | ||
| globalThis.__EGG_BUNDLE_MODULE_LOADER__ = loader; | ||
| } | ||
|
|
||
| export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> { | ||
| const _bundleModuleLoader = globalThis.__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 on lines
+412
to
+423
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic for unwrapping function unwrapModule(obj: any, options?: ImportModuleOptions): 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;
}
export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> {
const _bundleModuleLoader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
if (_bundleModuleLoader) {
const hit = _bundleModuleLoader(normalizeBundleModulePath(filepath), options);
if (hit !== undefined) {
return unwrapModule(hit, options);
}
}References
|
||
|
|
||
| const moduleFilePath = importResolve(filepath, options); | ||
|
|
||
| if (_snapshotModuleLoader) { | ||
|
|
||
| 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); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,9 @@ | |
| { | ||
| "path": "./packages/core" | ||
| }, | ||
| { | ||
| "path": "./packages/typings" | ||
| }, | ||
| { | ||
| "path": "./packages/errors" | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
BundleModuleLoadercurrently only receives the rawfilepath. In multi-application environments, it is important to verify that the global bundle store'sbaseDirmatches the requestedbaseDirto prevent using an incorrect manifest. Consider passingoptions(includingbaseDir) to the loader to facilitate this check.References