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
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/core/utils');

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(['.cjs', '.mjs']);
Comment thread
killagu marked this conversation as resolved.
Outdated

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);
throw e;
}
const err = new Error(`[egg/core] load file: ${filepath}, error: ${e.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