Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@eggjs/extend2": "workspace:*",
"@eggjs/koa": "workspace:*",
"@eggjs/loader-fs": "workspace:*",
"@eggjs/path-matching": "workspace:*",
"@eggjs/router": "workspace:*",
"@eggjs/typings": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export * from './singleton.ts';
export * from './loader/egg_loader.ts';
export * from './loader/file_loader.ts';
export * from './loader/context_loader.ts';
export * from './loader/loader_fs.ts';
export * from '@eggjs/loader-fs';
export * from './loader/manifest.ts';
export * from './utils/sequencify.ts';
export * from './utils/timing.ts';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/loader/egg_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { debuglog, inspect } from 'node:util';

import { extend } from '@eggjs/extend2';
import { Request, Response, Application, Context as KoaContext } from '@eggjs/koa';
import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
import { pathMatching, type PathMatchingOptions } from '@eggjs/path-matching';
import { isESM, isSupportTypeScript } from '@eggjs/utils';
import type { Logger } from 'egg-logger';
Expand All @@ -24,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 { RealLoaderFS, type LoaderFS } from './loader_fs.ts';
import { ManifestStore, type StartupManifest } from './manifest.ts';

const debug = debuglog('egg/core/loader/egg_loader');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/loader/file_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import assert from 'node:assert';
import path from 'node:path';
import { debuglog } from 'node:util';

import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
import { isSupportTypeScript } from '@eggjs/utils';
import { isClass, isGeneratorFunction, isAsyncFunction, isPrimitive } from 'is-type-of';

import utils from '../utils/index.ts';
import { RealLoaderFS, type LoaderFS } from './loader_fs.ts';
import type { ManifestStore } from './manifest.ts';

const debug = debuglog('egg/core/file_loader');
Expand Down
43 changes: 0 additions & 43 deletions packages/core/src/loader/loader_fs.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/core/test/loader/file_loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict';
import path from 'node:path';

import { RealLoaderFS, type LoaderFSGlobOptions } from '@eggjs/loader-fs';
import { isClass } from 'is-type-of';
import yaml from 'js-yaml';
import { describe, it, expect } from 'vitest';

import { FileLoader, CaseStyle } from '../../src/loader/file_loader.ts';
import { RealLoaderFS, type LoaderFSGlobOptions } from '../../src/loader/loader_fs.ts';
import { ManifestStore } from '../../src/loader/manifest.ts';
import { getFilepath } from '../helper.ts';

Expand Down
51 changes: 51 additions & 0 deletions packages/loader-fs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@eggjs/loader-fs",
"version": "1.0.0-beta.9",
"description": "Loader-facing filesystem boundary for Egg loaders",
"keywords": [
"egg",
"filesystem",
"loader"
],
"homepage": "https://github.com/eggjs/egg/tree/next/packages/loader-fs",
"license": "MIT",
"author": "fengmk2 <fengmk2@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/eggjs/egg.git",
"directory": "packages/loader-fs"
},
"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"
},
"dependencies": {
"@eggjs/utils": "workspace:*",
"globby": "catalog:",
"utility": "catalog:"
},
"devDependencies": {
"@eggjs/tsconfig": "workspace:*",
"typescript": "catalog:"
},
"engines": {
"node": ">=22.18.0"
}
}
73 changes: 73 additions & 0 deletions packages/loader-fs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs, { type Stats } from 'node:fs';
import BuiltinModule from 'node:module';
import path from 'node:path';
import { debuglog } from 'node:util';

import { importModule } from '@eggjs/utils';
import globby from 'globby';
import { readJSONSync } from 'utility';

const debug = debuglog('egg/loader-fs');

export type LoaderFSGlobOptions = globby.GlobbyOptions;

export interface LoaderFS {
exists(filepath: string): boolean;
stat(filepath: string): Stats;
realpath(filepath: string): string;
readJSON<T = unknown>(filepath: string): T;
glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[];
loadFile(filepath: string): Promise<unknown>;
}

interface ModuleConstructorWithExtensions {
_extensions: Record<string, unknown>;
}

const ModuleConstructor =
typeof module !== 'undefined' && module.constructor.length > 1
? (module.constructor as unknown as ModuleConstructorWithExtensions)
: (BuiltinModule as unknown as ModuleConstructorWithExtensions);
const extensionNames = Object.keys(ModuleConstructor._extensions).concat(['.cjs', '.mjs']);

export class RealLoaderFS implements LoaderFS {
exists(filepath: string): boolean {
return fs.existsSync(filepath);
}

stat(filepath: string): Stats {
return fs.statSync(filepath);
}

realpath(filepath: string): string {
return fs.realpathSync(filepath);
}

readJSON<T = unknown>(filepath: string): T {
return readJSONSync(filepath) as T;
}

glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[] {
return globby.sync(patterns, options);
}

async loadFile(filepath: string): Promise<unknown> {
debug('[loadFile:start] filepath: %s', filepath);
try {
const extname = path.extname(filepath);
if (extname && !extensionNames.includes(extname) && extname !== '.ts') {
return fs.readFileSync(filepath);
}
return await importModule(filepath, { importDefaultOnly: true });
} catch (e) {
if (!(e instanceof Error)) {
console.trace(e);
throw e;
}
const err = new Error(`[egg/core] load file: ${filepath}, error: ${e.message}`);
Comment thread
killagu marked this conversation as resolved.
err.cause = e;
debug('[loadFile] handle %s error: %s', filepath, e);
throw err;
}
}
Comment thread
killagu marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions packages/loader-fs/test/fixtures/loadfile/no-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo: bar
3 changes: 3 additions & 0 deletions packages/loader-fs/test/fixtures/loadfile/null.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = null;
3 changes: 3 additions & 0 deletions packages/loader-fs/test/fixtures/loadfile/object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = { a: 1 };
3 changes: 3 additions & 0 deletions packages/loader-fs/test/fixtures/loadfile/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "commonjs"
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

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

import { RealLoaderFS } from '../../src/loader/loader_fs.ts';
import utils from '../../src/utils/index.ts';
import { getFilepath } from '../helper.ts';
import { RealLoaderFS } from '../src/index.ts';

describe('test/loader/loader_fs.test.ts', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));

describe('test/index.test.ts', () => {
const loaderFS = new RealLoaderFS();
const baseDir = getFilepath('loadfile');
const baseDir = path.join(__dirname, 'fixtures/loadfile');

it('should wrap exists/stat/realpath with node fs behavior', () => {
const filepath = path.join(baseDir, 'object.js');
Expand All @@ -26,11 +27,9 @@
const packagePath = path.join(baseDir, 'package.json');
const patterns = ['*.js', '!null.js'];

assert.deepEqual(await loaderFS.readJSON(packagePath), JSON.parse(fs.readFileSync(packagePath, 'utf8')));
assert.deepEqual(loaderFS.readJSON(packagePath), JSON.parse(fs.readFileSync(packagePath, 'utf8')));
assert.deepEqual(loaderFS.glob(patterns, { cwd: baseDir }).sort(), globby.sync(patterns, { cwd: baseDir }).sort());
assert.deepEqual(
await loaderFS.loadFile(path.join(baseDir, 'object.js')),
await utils.loadFile(path.join(baseDir, 'object.js')),
);
assert.deepEqual(await loaderFS.loadFile(path.join(baseDir, 'object.js')), { a: 1 });
assert.deepEqual(await loaderFS.loadFile(path.join(baseDir, 'no-js.yml')), Buffer.from('foo: bar\n'));

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 22)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 22)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 22)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 24)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 24)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12

Check failure on line 33 in packages/loader-fs/test/index.test.ts

View workflow job for this annotation

GitHub Actions / Test (windows-latest, 24)

[@eggjs/loader-fs] test/index.test.ts > test/index.test.ts > should wrap readJSON/glob/loadFile with current loader behavior

AssertionError: Expected values to be strictly deep-equal: + actual - expected ... Skipped lines + Buffer(10) [Uint8Array] [ - Buffer(9) [Uint8Array] [ 102, 111, 111, 58, 32, ... 114, + 13, 10 ] - Expected + Received @@ -6,9 +6,10 @@ 58, 32, 98, 97, 114, + 13, 10, ], "type": "Buffer", } ❯ test/index.test.ts:33:12
});
});
3 changes: 3 additions & 0 deletions packages/loader-fs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
7 changes: 7 additions & 0 deletions packages/loader-fs/tsdown.config.ts
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',
},
});
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
{
"path": "./packages/core"
},
{
"path": "./packages/loader-fs"
},
{
"path": "./packages/errors"
},
Expand Down
1 change: 1 addition & 0 deletions wiki/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Read this file before exploring raw sources.

- [Core Package](./packages/core.md) - Loader, lifecycle, and application core primitives used by Egg runtime packages.
- [Egg Bundler](./packages/egg-bundler.md) - Tooling package that bundles Egg applications and backs `egg-bin bundle`.
- [Loader FS Package](./packages/loader-fs.md) - Shared loader-facing filesystem boundary and default real filesystem implementation.
- [Onerror Plugin](./packages/onerror.md) - Default Egg error-handling plugin and configurable response negotiation layer.
- [Typings Package](./packages/typings.md) - Shared TypeScript type surface for cross-package Egg typings.
- [Utils Package](./packages/utils.md) - Shared utility package for module loading and bundled module-loader integration.
Expand Down
8 changes: 7 additions & 1 deletion wiki/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

Dates use the workspace-local Asia/Shanghai calendar date.

## [2026-05-10] package | extract LoaderFS package boundary

- sources touched: `packages/loader-fs/src/index.ts`, `packages/core/src/index.ts`, `packages/core/src/loader/file_loader.ts`, `packages/core/src/loader/context_loader.ts`, `packages/core/src/loader/egg_loader.ts`
- pages updated: `wiki/index.md`, `wiki/log.md`, `wiki/packages/core.md`, `wiki/packages/loader-fs.md`
- note: Moved `LoaderFS` and `RealLoaderFS` into `@eggjs/loader-fs` so core, tegg, and bundled runtimes can share the loader-facing boundary without a core dependency.

## [2026-05-07] package | document core LoaderFS boundary

- sources touched: `packages/core/src/index.ts`, `packages/core/src/loader/loader_fs.ts`, `packages/core/src/loader/file_loader.ts`, `packages/core/src/loader/context_loader.ts`, `packages/core/src/loader/egg_loader.ts`
- sources touched: `packages/core/src/index.ts`, `packages/core/src/loader/file_loader.ts`, `packages/core/src/loader/context_loader.ts`, `packages/core/src/loader/egg_loader.ts`
- pages updated: `wiki/index.md`, `wiki/log.md`, `wiki/packages/core.md`
- note: Recorded `LoaderFS` as the minimal loader filesystem boundary and `RealLoaderFS` as the default implementation for existing non-bundled behavior.

Expand Down
14 changes: 5 additions & 9 deletions wiki/packages/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ type: package
summary: Loader, lifecycle, and application core primitives used by Egg runtime packages.
source_files:
- packages/core/src/index.ts
- packages/core/src/loader/loader_fs.ts
- packages/core/src/loader/file_loader.ts
- packages/core/src/loader/context_loader.ts
- packages/core/src/loader/egg_loader.ts
Expand All @@ -20,14 +19,11 @@ support, lifecycle, and base context classes.

## LoaderFS

`LoaderFS` is the minimal filesystem boundary for loader-facing file access. It
covers `exists`, `stat`, `realpath`, `readJSON`, `glob`, and `loadFile` without
trying to polyfill the full Node.js `fs` module.

`RealLoaderFS` is the default implementation. It preserves normal non-bundled
runtime behavior by delegating to `fs.existsSync`, `fs.statSync`,
`fs.realpathSync`, `utility.readJSONSync`, `globby.sync`, and the existing
`utils.loadFile()` helper.
`@eggjs/core` depends on `@eggjs/loader-fs` for the minimal loader-facing
filesystem boundary. It re-exports `LoaderFS`, `LoaderFSGlobOptions`, and
`RealLoaderFS` for core loader consumers, but the implementation lives in the
standalone package so tegg and bundled runtimes can share the same small
boundary without depending on core.

`EggLoaderOptions`, `FileLoaderOptions`, and `ContextLoaderOptions` can carry a
custom `loaderFS`. `EggLoader` passes its loader FS into `loadToApp()` and
Expand Down
27 changes: 27 additions & 0 deletions wiki/packages/loader-fs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
title: Loader FS Package
type: package
summary: Shared loader-facing filesystem boundary and default real filesystem implementation.
source_files:
- packages/loader-fs/src/index.ts
- packages/loader-fs/test/index.test.ts
updated_at: 2026-05-10
status: active
---

# Loader FS Package

`@eggjs/loader-fs` owns the small filesystem boundary shared by Egg loader
runtimes. It is not a full Node.js `fs` polyfill; it only covers the loader
operations needed by Egg-style file discovery and loading: `exists`, `stat`,
`realpath`, `readJSON`, `glob`, and `loadFile`.

`RealLoaderFS` is the default implementation. It preserves normal non-bundled
runtime behavior by delegating to `fs.existsSync`, `fs.statSync`,
`fs.realpathSync`, `utility.readJSONSync`, `globby.sync`, and Egg's standard
module loading semantics through `@eggjs/utils` `importModule()`.

`@eggjs/core` consumes and re-exports this package for `EggLoader`,
`FileLoader`, and `ContextLoader` options. Future manifest-backed or bundled
loaders can provide another `LoaderFS` implementation without making tegg or
runtime packages depend on `@eggjs/core`.
Loading