Skip to content
Open
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
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/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
45 changes: 2 additions & 43 deletions packages/core/src/loader/loader_fs.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,2 @@
import fs, { type Stats } from 'node:fs';

import globby from 'globby';
import { readJSONSync } from 'utility';

import utils from '../utils/index.ts';

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>;
}

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> {
return utils.loadFile(filepath);
}
}
export { RealLoaderFS } from '@eggjs/loader-fs';
export type { LoaderFS, LoaderFSGlobOptions } from '@eggjs/loader-fs';
10 changes: 2 additions & 8 deletions packages/core/test/loader/egg_loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ import assert from 'node:assert/strict';
import os from 'node:os';
import path from 'node:path';

import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';
import { getPlugins } from '@eggjs/utils';
import { mm } from 'mm';
import { describe, it, beforeAll, afterAll, afterEach } from 'vitest';

import {
ContextLoader,
EggLoader,
FileLoader,
RealLoaderFS,
type EggLoaderOptions,
type LoaderFS,
} from '../../src/index.js';
import { ContextLoader, EggLoader, FileLoader, type EggLoaderOptions } from '../../src/index.js';
import { createApp, getFilepath, type Application } from '../helper.js';

describe('test/loader/egg_loader.test.ts', () => {
Expand Down
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
8 changes: 3 additions & 5 deletions packages/core/test/loader/loader_fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';

import { RealLoaderFS as SharedRealLoaderFS } from '@eggjs/loader-fs';
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';

describe('test/loader/loader_fs.test.ts', () => {
Expand All @@ -28,9 +28,7 @@ describe('test/loader/loader_fs.test.ts', () => {

assert.deepEqual(await 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.equal(RealLoaderFS, SharedRealLoaderFS);
assert.deepEqual(await loaderFS.loadFile(path.join(baseDir, 'object.js')), { a: 1 });
});
});
9 changes: 9 additions & 0 deletions packages/loader-fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# @eggjs/loader-fs

Minimal loader-facing filesystem abstraction for Egg loaders.

`LoaderFS` intentionally covers only the file operations the loader boundary needs:
`exists`, `stat`, `realpath`, `readJSON`, `glob`, and `loadFile`.

`RealLoaderFS` is the default implementation backed by Node.js filesystem APIs,
`globby`, `utility.readJSONSync`, and `@eggjs/utils` module loading behavior.
55 changes: 55 additions & 0 deletions packages/loader-fs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "@eggjs/loader-fs",
"version": "1.0.2-beta.9",
"description": "Minimal loader-facing filesystem abstraction for Egg loaders",
"keywords": [
"egg",
"filesystem",
"loader"
],
"homepage": "https://github.com/eggjs/egg/tree/next/packages/loader-fs",
"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/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:*",
"@types/node": "catalog:",
"typescript": "catalog:"
},
"engines": {
"node": ">=22.18.0"
}
}
60 changes: 60 additions & 0 deletions packages/loader-fs/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs, { type Stats } from 'node:fs';
import path from 'node:path';
import { debuglog } from 'node:util';

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

const debug = debuglog('egg/loader-fs');
const extensionNames = Object.keys(getExtensions()).concat(['.cjs', '.mjs']);

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>;
}

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 (err) {
const message = err instanceof Error ? err.message : String(err);
const error = new Error(`[egg/loader-fs] load file: ${filepath}, error: ${message}`);
error.cause = err;
debug('[loadFile] handle %s error: %s', filepath, err);
throw error;
}
}
}
1 change: 1 addition & 0 deletions packages/loader-fs/test/fixtures/loadfile/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello loader fs
1 change: 1 addition & 0 deletions packages/loader-fs/test/fixtures/loadfile/null.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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 @@
module.exports = {
foo: 'bar',
};
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"
}
41 changes: 41 additions & 0 deletions packages/loader-fs/test/loader_fs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';

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

import { RealLoaderFS } from '../src/index.ts';

const fixtures = path.join(import.meta.dirname, 'fixtures');

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

it('should wrap exists/stat/realpath with node fs behavior', () => {
const filepath = path.join(baseDir, 'object.js');

assert.equal(loaderFS.exists(filepath), fs.existsSync(filepath));
assert.equal(loaderFS.exists(path.join(baseDir, 'not-exists.js')), false);
assert.equal(loaderFS.stat(filepath).isFile(), fs.statSync(filepath).isFile());
assert.equal(loaderFS.realpath(baseDir), fs.realpathSync(baseDir));
});

it('should wrap readJSON/glob/loadFile with current loader behavior', async () => {
const packagePath = path.join(baseDir, 'package.json');
const patterns = ['*.js', '!null.js'];

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')), { foo: 'bar' });
});

it('should return a buffer when loading a non-module file', async () => {
const filepath = path.join(baseDir, 'data.txt');
const result = await loaderFS.loadFile(filepath);

assert(result instanceof Buffer);
assert.equal(result.toString(), fs.readFileSync(filepath, 'utf8'));
});
});
4 changes: 4 additions & 0 deletions packages/loader-fs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"exclude": ["test/fixtures/**/*.ts", "*.config.ts"]
}
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 @@ -17,6 +17,9 @@
{
"path": "./packages/typings"
},
{
"path": "./packages/loader-fs"
},
{
"path": "./packages/core"
},
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`.
- [LoaderFS Package](./packages/loader-fs.md) - Shared minimal loader-facing filesystem boundary for Egg loaders and future tegg runtime reuse.
- [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
6 changes: 6 additions & 0 deletions wiki/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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

## [2026-05-10] package | extract LoaderFS into shared package

- sources touched: `packages/loader-fs/src/index.ts`, `packages/core/src/loader/loader_fs.ts`, `packages/core/src/loader/file_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: Added `@eggjs/loader-fs` as the shared loader-facing VFS boundary and switched `@eggjs/core` to consume it while preserving existing core re-exports.

## [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`
Expand Down
15 changes: 7 additions & 8 deletions wiki/packages/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ source_files:
- packages/core/src/loader/file_loader.ts
- packages/core/src/loader/context_loader.ts
- packages/core/src/loader/egg_loader.ts
updated_at: 2026-05-07
updated_at: 2026-05-10
status: active
---

Expand All @@ -20,14 +20,13 @@ 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.
`LoaderFS` now lives in `@eggjs/loader-fs`. `@eggjs/core` consumes that shared
package and keeps re-exporting `LoaderFS`, `LoaderFSGlobOptions`, and
`RealLoaderFS` for compatibility.

`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.
The boundary remains intentionally loader-facing rather than a full Node.js
`fs` polyfill. The interface covers `exists`, `stat`, `realpath`, `readJSON`,
`glob`, and `loadFile`.

`EggLoaderOptions`, `FileLoaderOptions`, and `ContextLoaderOptions` can carry a
custom `loaderFS`. `EggLoader` passes its loader FS into `loadToApp()` and
Expand Down
Loading
Loading