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
4 changes: 4 additions & 0 deletions packages/core/src/egg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { ReadyFunctionArg } from 'get-ready';
import { BaseContextClass } from './base_context_class.ts';
import { Lifecycle } from './lifecycle.ts';
import { EggLoader } from './loader/egg_loader.ts';
import type { LoaderFS } from './loader/loader_fs.ts';
import { Singleton, type SingletonCreateMethod, type SingletonOptions } from './singleton.ts';
import type { EggAppConfig } from './types.ts';
import utils, { type Fun } from './utils/index.ts';
Expand All @@ -33,6 +34,8 @@ export interface EggCoreOptions {
env?: string;
/** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */
metadataOnly?: boolean;
/** Loader-facing filesystem abstraction */
loaderFS?: LoaderFS;
/**
* When true, lifecycle stops after the `configWillLoad` phase.
* `configDidLoad`, `didLoad`, `willReady`, `didReady`, and `serverDidReady`
Expand Down Expand Up @@ -230,6 +233,7 @@ export class EggCore extends KoaApplication {
env: options.env ?? '',
EggCoreClass: EggCore,
metadataOnly: options.metadataOnly,
loaderFS: options.loaderFS,
});
}

Expand Down
34 changes: 17 additions & 17 deletions packages/core/src/loader/egg_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { sequencify } from '../utils/sequencify.ts';
import { Timing } from '../utils/timing.ts';
import { type ContextLoaderOptions, ContextLoader } from './context_loader.ts';
import { type FileLoaderOptions, CaseStyle, FULLPATH, FileLoader } from './file_loader.ts';
import { ManifestLoaderFS } from './loader_fs.ts';
import { ManifestStore, type StartupManifest } from './manifest.ts';

const debug = debuglog('egg/core/loader/egg_loader');
Expand Down Expand Up @@ -96,10 +95,7 @@ export class EggLoader {
*/
constructor(options: EggLoaderOptions) {
this.options = options;
const bundleStore = ManifestStore.getBundleStore();
this.loaderFS =
this.options.loaderFS ??
(bundleStore?.baseDir === this.options.baseDir ? new ManifestLoaderFS(bundleStore) : new RealLoaderFS());
this.loaderFS = this.options.loaderFS ?? new RealLoaderFS();
assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`);
assert(this.options.app, 'options.app is required');
assert(this.options.logger, 'options.logger is required');
Expand Down Expand Up @@ -180,9 +176,6 @@ export class EggLoader {
this.manifest =
ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ??
ManifestStore.createCollector(this.options.baseDir);
if (!this.options.loaderFS && !(this.loaderFS instanceof ManifestLoaderFS)) {
this.loaderFS = new ManifestLoaderFS(this.manifest, this.loaderFS);
}
}

get app(): EggCore {
Expand Down Expand Up @@ -1802,36 +1795,43 @@ export class EggLoader {
* generation time.
*/
#collectConventionalDynamicFiles(manifest: StartupManifest): void {
const resolveCacheTargets = new Set(
Object.values(manifest.resolveCache).filter((target): target is string => typeof target === 'string'),
);
for (const unit of this.getLoadUnits()) {
this.#collectConventionFile(manifest, path.join(unit.path, 'package.json'));
this.#collectConventionFile(manifest, path.join(unit.path, 'package.json'), resolveCacheTargets);
for (const load of CONVENTIONAL_MANIFEST_LOADS) {
const target = path.join(unit.path, ...load.path);
if (load.type === 'resolve') {
this.#collectConventionResolve(manifest, target);
this.#collectConventionResolve(manifest, target, resolveCacheTargets);
} else if ('extensionlessResolve' in load && load.extensionlessResolve) {
this.#collectConventionFileResolves(manifest, target);
this.#collectConventionFileResolves(manifest, target, resolveCacheTargets);
} else {
this.#collectConventionFileDiscovery(manifest, target);
}
}
}
}

#collectConventionResolve(manifest: StartupManifest, request: string): void {
#collectConventionResolve(manifest: StartupManifest, request: string, resolveCacheTargets: Set<string>): void {
const requestKey = this.#toManifestRel(request);
if (Object.hasOwn(manifest.resolveCache, requestKey)) return;

const resolved = this.#doResolveModule(request);
manifest.resolveCache[requestKey] = resolved ? this.#toManifestRel(resolved) : null;
const resolvedKey = resolved ? this.#toManifestRel(resolved) : null;
manifest.resolveCache[requestKey] = resolvedKey;
if (resolvedKey !== null) {
resolveCacheTargets.add(resolvedKey);
}
}

#collectConventionFileResolves(manifest: StartupManifest, directory: string): void {
#collectConventionFileResolves(manifest: StartupManifest, directory: string, resolveCacheTargets: Set<string>): void {
const files = this.#collectConventionFileDiscovery(manifest, directory);
for (const file of files) {
const ext = path.extname(file);
if (!ext) continue;
const request = path.join(directory, file.slice(0, -ext.length));
this.#collectConventionResolve(manifest, request);
this.#collectConventionResolve(manifest, request, resolveCacheTargets);
}
}

Expand All @@ -1846,9 +1846,9 @@ export class EggLoader {
return manifest.fileDiscovery[dirKey];
}

#collectConventionFile(manifest: StartupManifest, filepath: string): void {
#collectConventionFile(manifest: StartupManifest, filepath: string, resolveCacheTargets: Set<string>): void {
const fileKey = this.#toManifestRel(filepath);
if (Object.values(manifest.resolveCache).includes(fileKey)) return;
if (resolveCacheTargets.has(fileKey)) return;
if (!fs.existsSync(filepath) || !fs.statSync(filepath).isFile()) return;

const dirKey = this.#toManifestRel(path.dirname(filepath));
Expand Down
100 changes: 49 additions & 51 deletions packages/core/src/loader/loader_fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,33 @@ export type { LoaderFS, LoaderFSGlobOptions };
export class ManifestLoaderFS implements LoaderFS {
readonly #manifest: ManifestStore;
readonly #fallback: LoaderFS;
readonly #manifestFiles: Set<string>;
readonly #manifestDirectories: Set<string>;
readonly #resolveCacheTargets: Set<string>;

constructor(manifest: ManifestStore, fallback: LoaderFS = new RealLoaderFS()) {
this.#manifest = manifest;
this.#fallback = fallback;
this.#resolveCacheTargets = new Set(
Object.values(manifest.data.resolveCache).filter((target): target is string => typeof target === 'string'),
);

const manifestFiles = new Set<string>();
const manifestDirectories = new Set<string>();
for (const [dir, files] of Object.entries(manifest.data.fileDiscovery)) {
addManifestDirectory(manifestDirectories, dir);
for (const file of files) {
const fullRel = path.posix.join(dir, file);
manifestFiles.add(fullRel);
addManifestDirectory(manifestDirectories, path.posix.dirname(fullRel));
}
}
for (const target of this.#resolveCacheTargets) {
manifestFiles.add(target);
addManifestDirectory(manifestDirectories, path.posix.dirname(target));
}
this.#manifestFiles = manifestFiles;
this.#manifestDirectories = manifestDirectories;
}

exists(filepath: string): boolean {
Expand Down Expand Up @@ -125,15 +148,10 @@ export class ManifestLoaderFS implements LoaderFS {
}

#resolveFromFileDiscovery(rel: string): string | undefined {
let matchedDir: string | undefined;
for (const dir of Object.keys(this.#manifest.data.fileDiscovery)) {
if ((rel === dir || rel.startsWith(dir + '/')) && (!matchedDir || dir.length > matchedDir.length)) {
matchedDir = dir;
}
}
if (!matchedDir || rel === matchedDir) return;
const matchedDir = this.#nearestManifestDiscoveryDir(rel);
if (matchedDir === undefined || rel === matchedDir) return;

const request = rel.slice(matchedDir.length + 1);
const request = matchedDir === '' ? rel : rel.slice(matchedDir.length + 1);
for (const file of this.#manifest.data.fileDiscovery[matchedDir]) {
if (file === request) {
return path.posix.join(matchedDir, file);
Expand All @@ -149,42 +167,24 @@ export class ManifestLoaderFS implements LoaderFS {
}
}

#isManifestFile(rel: string): boolean {
for (const [dir, files] of Object.entries(this.#manifest.data.fileDiscovery)) {
const file = relativeFileUnderDir(dir, rel);
if (file !== undefined && files.includes(file)) {
return true;
#nearestManifestDiscoveryDir(rel: string): string | undefined {
let current = path.posix.dirname(rel);
while (true) {
const dir = current === '.' ? '' : current;
if (Object.hasOwn(this.#manifest.data.fileDiscovery, dir)) {
return dir;
}
if (dir === '') return;
current = path.posix.dirname(dir);
}
return Object.values(this.#manifest.data.resolveCache).includes(rel);
}

#isManifestDirectory(rel: string): boolean {
if (rel === '') {
return this.#hasManifestData();
}
if (Object.hasOwn(this.#manifest.data.fileDiscovery, rel)) {
return true;
}

for (const [dir, files] of Object.entries(this.#manifest.data.fileDiscovery)) {
if (dir.startsWith(rel + '/')) {
return true;
}
for (const file of files) {
const fullRel = path.posix.join(dir, file);
if (path.posix.dirname(fullRel) === rel || fullRel.startsWith(rel + '/')) {
return true;
}
}
}
#isManifestFile(rel: string): boolean {
return this.#manifestFiles.has(rel);
}

for (const target of Object.values(this.#manifest.data.resolveCache)) {
if (target && (path.posix.dirname(target) === rel || target.startsWith(rel + '/'))) {
return true;
}
}
return false;
#isManifestDirectory(rel: string): boolean {
return this.#manifestDirectories.has(rel);
}

#listManifestFilesUnder(cwdRel: string): string[] | undefined {
Expand Down Expand Up @@ -226,13 +226,6 @@ export class ManifestLoaderFS implements LoaderFS {
return [...new Set([rel, normalizePath(abs)])];
}

#hasManifestData(): boolean {
return (
Object.keys(this.#manifest.data.fileDiscovery).length > 0 ||
Object.keys(this.#manifest.data.resolveCache).some((key) => this.#manifest.data.resolveCache[key] !== null)
);
}

#toRelative(filepath: string): string {
const rel = path.relative(this.#manifest.baseDir, path.resolve(filepath));
return normalizePath(rel);
Expand All @@ -254,17 +247,22 @@ function normalizePath(filepath: string): string {
return filepath.replaceAll(path.sep, '/');
}

function addManifestDirectory(directories: Set<string>, dir: string): void {
let current = dir === '.' ? '' : dir;
while (true) {
directories.add(current);
if (current === '') return;
const parent = path.posix.dirname(current);
current = parent === '.' ? '' : parent;
}
}

function relativePrefix(cwdRel: string, dirRel: string): string | undefined {
if (cwdRel === '') return dirRel;
if (dirRel === cwdRel) return '';
if (dirRel.startsWith(cwdRel + '/')) return dirRel.slice(cwdRel.length + 1);
}

function relativeFileUnderDir(dirRel: string, fileRel: string): string | undefined {
if (dirRel === '') return fileRel;
if (fileRel.startsWith(dirRel + '/')) return fileRel.slice(dirRel.length + 1);
}

function filterManifestGlob(files: string[], patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
const patternList = Array.isArray(patterns) ? patterns : [patterns];
if (!patternList.some((pattern) => !pattern.startsWith('!'))) return [];
Expand Down
14 changes: 13 additions & 1 deletion packages/core/test/egg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import coffee from 'coffee';
import { mm } from 'mm';
import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';

import { EggCore } from '../src/index.js';
import { EggCore, ManifestLoaderFS, RealLoaderFS } from '../src/index.js';
import { createApp, getFilepath, type Application } from './helper.js';

describe('test/egg.test.ts', () => {
Expand Down Expand Up @@ -54,6 +54,18 @@ describe('test/egg.test.ts', () => {
assert.equal(app.loader.serverScope, 'scope');
});

it('should pass options.loaderFS to EggLoader', () => {
const loaderFS = new RealLoaderFS();
app = new EggCore({ loaderFS });
assert.equal(app.loader.loaderFS, loaderFS);
});

it('should use RealLoaderFS by default for non-bundled runtime', () => {
app = new EggCore();
assert.equal(app.loader.loaderFS instanceof RealLoaderFS, true);
assert.equal(app.loader.loaderFS instanceof ManifestLoaderFS, false);
});

it('should not set value expect for application and agent', () => {
assert.throws(
() =>
Expand Down
4 changes: 3 additions & 1 deletion packages/egg/src/lib/start.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'node:path';

import { ManifestStore } from '@eggjs/core';
import { ManifestStore, type LoaderFS } from '@eggjs/core';
import { importModule } from '@eggjs/utils';
import { readJSON } from 'utility';

Expand All @@ -20,6 +20,8 @@ export interface StartEggOptions {
plugins?: EggPlugin;
/** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */
metadataOnly?: boolean;
/** Loader-facing filesystem abstraction */
loaderFS?: LoaderFS;
/**
* When true, load application metadata for V8 startup snapshot construction.
* The lifecycle stops after configWillLoad (no servers, timers, or connections)
Expand Down
8 changes: 5 additions & 3 deletions tools/egg-bundler/src/lib/EntryGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ for (const [key, spec] of __EXTERNAL_SPECS) {
/* eslint-disable */
import path from 'node:path';

import { ManifestStore } from '@eggjs/core';
import { ManifestLoaderFS, ManifestStore } from '@eggjs/core';
import type {} from '@eggjs/typings/global';
import { startEgg } from ${frameworkSpec};
import * as __frameworkModule from ${frameworkSpec};
Expand Down Expand Up @@ -315,12 +315,14 @@ for (const [appAbsRequest, targetRel] of __APP_RESOLVE_CACHE_ALIASES) {
}
}

ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir));
const __bundleManifestStore = ManifestStore.fromBundle(MANIFEST_DATA as any, __outputDir);
const __loaderFS = new ManifestLoaderFS(__bundleManifestStore);
ManifestStore.setBundleStore(__bundleManifestStore);
globalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => {
return __getBundleMap(filepath);
};

startEgg({ baseDir: __outputDir, framework: __framework, mode: 'single' }).then((app) => {
startEgg({ baseDir: __outputDir, framework: __framework, mode: 'single', loaderFS: __loaderFS }).then((app) => {
const port = process.env.PORT || app.config.cluster?.listen?.port || 7001;
app.listen(port, () => {
// eslint-disable-next-line no-console
Expand Down
Loading
Loading