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/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
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';
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
2 changes: 1 addition & 1 deletion packages/core/test/loader/loader_fs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';

import { RealLoaderFS } 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';

Expand Down
12 changes: 12 additions & 0 deletions packages/loader-fs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

> [!IMPORTANT]
> Moving forwards we are using the GitHub releases page at <https://github.com/eggjs/egg/releases> in combination with [release.yml](https://github.com/eggjs/egg/actions/workflows/release.yml) for publishing releases and their changelogs.

---

## 1.0.0+

### Features

- Initial shared `LoaderFS` and `RealLoaderFS` package.
14 changes: 14 additions & 0 deletions packages/loader-fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# @eggjs/loader-fs

Loader-facing filesystem abstraction for Egg loaders and bundled runtimes.

## Usage

```ts
import { RealLoaderFS, type LoaderFS } from '@eggjs/loader-fs';

const loaderFS: LoaderFS = new RealLoaderFS();
```

`LoaderFS` intentionally covers only the operations required by loader code:
`exists`, `stat`, `realpath`, `readJSON`, `glob`, and `loadFile`.
57 changes: 57 additions & 0 deletions packages/loader-fs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@eggjs/loader-fs",
"version": "1.0.0-beta.9",
"description": "Loader-facing filesystem abstraction for Egg",
"keywords": [
"egg",
"fs",
"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"
}
Comment thread
killagu marked this conversation as resolved.
},
"scripts": {
"typecheck": "tsgo --noEmit",
"test": "vitest run"
},
"dependencies": {
"@eggjs/utils": "workspace:*",
"globby": "catalog:",
"utility": "catalog:"
},
"devDependencies": {
"@eggjs/tsconfig": "workspace:*",
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "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');

type CommonJSModuleConstructor = {
_extensions?: Record<string, unknown>;
};

// Guard against poorly mocked module constructors.
const Module = typeof module !== 'undefined' && module.constructor.length > 1 ? module.constructor : BuiltinModule;

const extensions = (Module as unknown as CommonJSModuleConstructor)._extensions ?? {};
const extensionNames = Object.keys(extensions).concat(['.js', '.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 (e) {
if (!(e instanceof Error)) {
console.trace(e);
}
const message = e instanceof Error ? e.message : String(e);
const err = new Error(`[@eggjs/loader-fs] load file: ${filepath}, error: ${message}`);
err.cause = e;
debug('[loadFile] handle %s error: %s', filepath, e);
throw err;
Comment thread
killagu marked this conversation as resolved.
}
Comment thread
killagu marked this conversation as resolved.
}
}
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"
}
1 change: 1 addition & 0 deletions packages/loader-fs/test/fixtures/loadfile/plain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name: loader-fs
37 changes: 37 additions & 0 deletions packages/loader-fs/test/loader_fs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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/index.ts';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

describe('test/loader_fs.test.ts', () => {
const loaderFS = new RealLoaderFS();
const baseDir = path.join(__dirname, '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'];
const yamlPath = path.join(baseDir, 'plain.yml');

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')), { a: 1 });
assert.deepEqual(await loaderFS.loadFile(yamlPath), fs.readFileSync(yamlPath));
});
});
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 @@ -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`.
- [Loader FS Package](./packages/loader-fs.md) - Shared loader-facing filesystem boundary for Egg loaders and future bundled runtimes.
- [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 shared LoaderFS package

- sources touched: `packages/loader-fs/src/index.ts`, `packages/loader-fs/package.json`, `packages/core/src/index.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: Moved the loader-facing `LoaderFS` / `RealLoaderFS` boundary into `@eggjs/loader-fs` while keeping `@eggjs/core` as a consumer and re-exporter.

## [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
Loading
Loading