@eggjs/loader-fs 1.0.0-beta.25 → 1.0.0-beta.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,4 +11,7 @@ const loaderFS: LoaderFS = new RealLoaderFS();
11
11
  ```
12
12
 
13
13
  `LoaderFS` intentionally covers only the operations required by loader code:
14
- `exists`, `stat`, `realpath`, `readJSON`, `glob`, and `loadFile`.
14
+ `exists`, `stat`, `realpath`, `readJSON`, `glob`, and `loadFile`. A source with
15
+ an authoritative, precomputed directory view may also implement
16
+ `getKnownFiles`; `undefined` means discovery should fall back to `glob`, while
17
+ an empty array means the directory is authoritatively empty.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { LoaderFSManifest, LoaderFSManifestData, ManifestLoaderFS } from "./manifest_loader_fs.js";
1
2
  import { Stats } from "node:fs";
2
3
  import globby from "globby";
3
4
 
@@ -8,6 +9,12 @@ interface LoaderFS {
8
9
  stat(filepath: string): Stats;
9
10
  realpath(filepath: string): string;
10
11
  readJSON<T = unknown>(filepath: string): T;
12
+ /**
13
+ * Return an authoritative, precomputed file list for a directory when one is
14
+ * available. `undefined` means the source has no precomputed view and callers
15
+ * should fall back to normal discovery; an empty array is authoritative.
16
+ */
17
+ getKnownFiles?(directory: string): readonly string[] | undefined;
11
18
  glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[];
12
19
  loadFile(filepath: string): Promise<unknown>;
13
20
  }
@@ -20,4 +27,4 @@ declare class RealLoaderFS implements LoaderFS {
20
27
  loadFile(filepath: string): Promise<unknown>;
21
28
  }
22
29
  //#endregion
23
- export { LoaderFS, LoaderFSGlobOptions, RealLoaderFS };
30
+ export { LoaderFS, LoaderFSGlobOptions, type LoaderFSManifest, type LoaderFSManifestData, ManifestLoaderFS, RealLoaderFS };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ManifestLoaderFS } from "./manifest_loader_fs.js";
1
2
  import BuiltinModule from "node:module";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -63,4 +64,4 @@ var RealLoaderFS = class {
63
64
  };
64
65
 
65
66
  //#endregion
66
- export { RealLoaderFS };
67
+ export { ManifestLoaderFS, RealLoaderFS };
@@ -0,0 +1,28 @@
1
+ import { LoaderFS, LoaderFSGlobOptions } from "./index.js";
2
+ import { Stats } from "node:fs";
3
+
4
+ //#region src/manifest_loader_fs.d.ts
5
+ interface LoaderFSManifestData {
6
+ /** resolveModule cache: relative filepath -> resolved relative path | null */
7
+ resolveCache: Record<string, string | null>;
8
+ /** relative directory path -> file paths relative to that directory */
9
+ fileDiscovery: Record<string, string[]>;
10
+ }
11
+ /** Host-neutral manifest view consumed by ManifestLoaderFS. */
12
+ interface LoaderFSManifest {
13
+ readonly baseDir: string;
14
+ readonly data: LoaderFSManifestData;
15
+ }
16
+ declare class ManifestLoaderFS implements LoaderFS {
17
+ #private;
18
+ constructor(manifest: LoaderFSManifest, fallback?: LoaderFS);
19
+ exists(filepath: string): boolean;
20
+ stat(filepath: string): Stats;
21
+ realpath(filepath: string): string;
22
+ readJSON<T = unknown>(filepath: string): T;
23
+ getKnownFiles(directory: string): readonly string[] | undefined;
24
+ glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[];
25
+ loadFile(filepath: string): Promise<unknown>;
26
+ }
27
+ //#endregion
28
+ export { LoaderFSManifest, LoaderFSManifestData, ManifestLoaderFS };
@@ -0,0 +1,276 @@
1
+ import { RealLoaderFS } from "./index.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import multimatch from "multimatch";
5
+
6
+ //#region src/manifest_loader_fs.ts
7
+ var ManifestLoaderFS = class {
8
+ #manifest;
9
+ #fallback;
10
+ #manifestFiles;
11
+ #manifestDirectories;
12
+ #resolveCacheTargets;
13
+ constructor(manifest, fallback = new RealLoaderFS()) {
14
+ this.#manifest = manifest;
15
+ this.#fallback = fallback;
16
+ this.#resolveCacheTargets = new Set(Object.values(manifest.data.resolveCache).filter((target) => typeof target === "string"));
17
+ const manifestFiles = /* @__PURE__ */ new Set();
18
+ const manifestDirectories = /* @__PURE__ */ new Set();
19
+ for (const [dir, files] of Object.entries(manifest.data.fileDiscovery)) {
20
+ addManifestDirectory(manifestDirectories, dir);
21
+ for (const file of files) {
22
+ const fullRel = path.posix.join(dir, file);
23
+ manifestFiles.add(fullRel);
24
+ addManifestDirectory(manifestDirectories, path.posix.dirname(fullRel));
25
+ }
26
+ }
27
+ for (const target of this.#resolveCacheTargets) {
28
+ manifestFiles.add(target);
29
+ addManifestDirectory(manifestDirectories, path.posix.dirname(target));
30
+ }
31
+ this.#manifestFiles = manifestFiles;
32
+ this.#manifestDirectories = manifestDirectories;
33
+ }
34
+ exists(filepath) {
35
+ const entry = this.#getManifestEntry(filepath);
36
+ if (entry) return entry.type !== "missing";
37
+ return this.#fallback.exists(filepath);
38
+ }
39
+ stat(filepath) {
40
+ const entry = this.#getManifestEntry(filepath);
41
+ if (entry?.type === "file" || entry?.type === "directory") return createVirtualStats(entry.type);
42
+ if (entry?.type === "missing") throw createNotFoundError("stat", filepath);
43
+ return this.#fallback.stat(filepath);
44
+ }
45
+ realpath(filepath) {
46
+ const entry = this.#getManifestEntry(filepath);
47
+ if (entry?.type === "file" || entry?.type === "directory") return this.#toAbsolute(entry.rel);
48
+ if (entry?.type === "missing") throw createNotFoundError("realpath", filepath);
49
+ return this.#fallback.realpath(filepath);
50
+ }
51
+ readJSON(filepath) {
52
+ const entry = this.#getManifestEntry(filepath);
53
+ if (entry?.type === "file") {
54
+ const bundled = this.#loadBundledModule(entry.rel);
55
+ if (bundled !== void 0) return unwrapDefaultExport(bundled);
56
+ }
57
+ if (entry?.type === "missing") throw createNotFoundError("open", filepath);
58
+ return this.#fallback.readJSON(filepath);
59
+ }
60
+ getKnownFiles(directory) {
61
+ const relativeDirectory = this.#toRelative(directory);
62
+ if (Object.hasOwn(this.#manifest.data.fileDiscovery, relativeDirectory)) return [...this.#manifest.data.fileDiscovery[relativeDirectory]].sort();
63
+ return this.#fallback.getKnownFiles?.(directory);
64
+ }
65
+ glob(patterns, options) {
66
+ const cwd = options?.cwd === void 0 ? process.cwd() : String(options.cwd);
67
+ const absoluteCwd = path.resolve(cwd);
68
+ const cwdRel = this.#toRelative(absoluteCwd);
69
+ const manifestFiles = this.#listManifestFilesUnder(cwdRel);
70
+ if (manifestFiles === void 0) return this.#fallback.glob(patterns, options);
71
+ const matched = filterManifestGlob(manifestFiles, patterns, options);
72
+ if (options?.absolute) return matched.map((file) => path.join(absoluteCwd, file));
73
+ return matched;
74
+ }
75
+ async loadFile(filepath) {
76
+ const entry = this.#getManifestEntry(filepath);
77
+ if (entry?.type === "file") {
78
+ const bundled = this.#loadBundledModule(entry.rel);
79
+ if (bundled !== void 0) return unwrapDefaultExport(bundled);
80
+ }
81
+ if (entry?.type === "missing") throw createNotFoundError("open", filepath);
82
+ return this.#fallback.loadFile(filepath);
83
+ }
84
+ #getManifestEntry(filepath) {
85
+ const rel = this.#toRelative(filepath);
86
+ const resolved = this.#resolveManifestFile(rel);
87
+ if (resolved?.type === "file") return {
88
+ type: "file",
89
+ rel: resolved.rel
90
+ };
91
+ if (resolved?.type === "missing") return {
92
+ type: "missing",
93
+ rel
94
+ };
95
+ if (this.#isManifestDirectory(rel)) return {
96
+ type: "directory",
97
+ rel
98
+ };
99
+ }
100
+ #resolveManifestFile(rel) {
101
+ const cache = this.#manifest.data.resolveCache;
102
+ if (Object.hasOwn(cache, rel)) {
103
+ const cached = cache[rel];
104
+ return cached === null ? { type: "missing" } : {
105
+ type: "file",
106
+ rel: cached
107
+ };
108
+ }
109
+ if (this.#isManifestFile(rel)) return {
110
+ type: "file",
111
+ rel
112
+ };
113
+ const discovered = this.#resolveFromFileDiscovery(rel);
114
+ if (discovered) return {
115
+ type: "file",
116
+ rel: discovered
117
+ };
118
+ }
119
+ #resolveFromFileDiscovery(rel) {
120
+ const matchedDir = this.#nearestManifestDiscoveryDir(rel);
121
+ if (matchedDir === void 0 || rel === matchedDir) return;
122
+ const request = matchedDir === "" ? rel : rel.slice(matchedDir.length + 1);
123
+ for (const file of this.#manifest.data.fileDiscovery[matchedDir]) {
124
+ if (file === request) return path.posix.join(matchedDir, file);
125
+ const ext = path.posix.extname(file);
126
+ if (!ext || ext === ".map") continue;
127
+ const extensionlessFile = file.slice(0, -ext.length);
128
+ if (extensionlessFile === request || extensionlessFile === `${request}/index`) return path.posix.join(matchedDir, file);
129
+ }
130
+ }
131
+ #nearestManifestDiscoveryDir(rel) {
132
+ let current = path.posix.dirname(rel);
133
+ while (true) {
134
+ const dir = current === "." ? "" : current;
135
+ if (Object.hasOwn(this.#manifest.data.fileDiscovery, dir)) return dir;
136
+ if (dir === "") return;
137
+ current = path.posix.dirname(dir);
138
+ }
139
+ }
140
+ #isManifestFile(rel) {
141
+ return this.#manifestFiles.has(rel);
142
+ }
143
+ #isManifestDirectory(rel) {
144
+ return this.#manifestDirectories.has(rel);
145
+ }
146
+ #listManifestFilesUnder(cwdRel) {
147
+ if (!this.#isManifestDirectory(cwdRel)) return;
148
+ const files = /* @__PURE__ */ new Set();
149
+ for (const [dir, entries] of Object.entries(this.#manifest.data.fileDiscovery)) {
150
+ const prefix = dir === cwdRel ? "" : relativePrefix(cwdRel, dir);
151
+ if (prefix === void 0) continue;
152
+ for (const entry of entries) files.add(path.posix.join(prefix, entry));
153
+ }
154
+ for (const target of Object.values(this.#manifest.data.resolveCache)) {
155
+ if (!target) continue;
156
+ const prefix = relativePrefix(cwdRel, path.posix.dirname(target));
157
+ if (prefix !== void 0) files.add(path.posix.join(prefix, path.posix.basename(target)));
158
+ }
159
+ return [...files].sort();
160
+ }
161
+ #loadBundledModule(rel) {
162
+ const loader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
163
+ if (!loader) return void 0;
164
+ for (const key of this.#bundleKeys(rel)) {
165
+ const loaded = loader(key);
166
+ if (loaded !== void 0) return loaded;
167
+ }
168
+ }
169
+ #bundleKeys(rel) {
170
+ const abs = this.#toAbsolute(rel);
171
+ return [...new Set([rel, normalizePath(abs)])];
172
+ }
173
+ #toRelative(filepath) {
174
+ return normalizePath(path.relative(this.#manifest.baseDir, path.resolve(filepath)));
175
+ }
176
+ #toAbsolute(rel) {
177
+ return path.isAbsolute(rel) ? rel : path.join(this.#manifest.baseDir, rel);
178
+ }
179
+ };
180
+ function normalizePath(filepath) {
181
+ return filepath.replaceAll(path.sep, "/");
182
+ }
183
+ function addManifestDirectory(directories, dir) {
184
+ let current = dir === "." ? "" : dir;
185
+ while (true) {
186
+ directories.add(current);
187
+ if (current === "") return;
188
+ const parent = path.posix.dirname(current);
189
+ current = parent === "." ? "" : parent;
190
+ }
191
+ }
192
+ function relativePrefix(cwdRel, dirRel) {
193
+ if (cwdRel === "") return dirRel;
194
+ if (dirRel === cwdRel) return "";
195
+ if (dirRel.startsWith(cwdRel + "/")) return dirRel.slice(cwdRel.length + 1);
196
+ }
197
+ function filterManifestGlob(files, patterns, options) {
198
+ const patternList = Array.isArray(patterns) ? patterns : [patterns];
199
+ if (!patternList.some((pattern) => !pattern.startsWith("!"))) return [];
200
+ const ignoreList = Array.isArray(options?.ignore) ? options.ignore.map(String) : options?.ignore ? [String(options.ignore)] : [];
201
+ return multimatch(files, patternList.map(normalizeAlternationGroups).concat(ignoreList.map((pattern) => `!${normalizeAlternationGroups(pattern)}`)), toMultimatchOptions(options));
202
+ }
203
+ function toMultimatchOptions(options) {
204
+ return {
205
+ ...options?.dot !== void 0 ? { dot: options.dot } : {},
206
+ ...options?.caseSensitiveMatch !== void 0 ? { nocase: !options.caseSensitiveMatch } : {},
207
+ ...options?.braceExpansion !== void 0 ? { nobrace: !options.braceExpansion } : {},
208
+ ...options?.extglob !== void 0 ? { noext: !options.extglob } : {},
209
+ ...options?.globstar !== void 0 ? { noglobstar: !options.globstar } : {},
210
+ ...options?.baseNameMatch !== void 0 ? { matchBase: options.baseNameMatch } : {}
211
+ };
212
+ }
213
+ function normalizeAlternationGroups(pattern) {
214
+ let normalized = "";
215
+ for (let index = 0; index < pattern.length; index++) {
216
+ const char = pattern[index];
217
+ if (char !== "(" || isExtglobPrefix(pattern[index - 1])) {
218
+ normalized += char;
219
+ continue;
220
+ }
221
+ const end = pattern.indexOf(")", index + 1);
222
+ if (end === -1) {
223
+ normalized += char;
224
+ continue;
225
+ }
226
+ const group = pattern.slice(index + 1, end);
227
+ if (group.includes("|")) {
228
+ normalized += `{${group.replaceAll("|", ",")}}`;
229
+ index = end;
230
+ } else normalized += char;
231
+ }
232
+ return normalized;
233
+ }
234
+ function isExtglobPrefix(char) {
235
+ return char === "@" || char === "!" || char === "?" || char === "+" || char === "*";
236
+ }
237
+ function unwrapDefaultExport(value) {
238
+ let unwrapped = value;
239
+ if (isRecord(unwrapped) && isRecord(unwrapped.default) && unwrapped.default.__esModule === true) unwrapped = unwrapped.default;
240
+ if (isRecord(unwrapped) && "default" in unwrapped) return unwrapped.default;
241
+ return unwrapped;
242
+ }
243
+ function isRecord(value) {
244
+ return value !== null && typeof value === "object";
245
+ }
246
+ function createVirtualStats(type) {
247
+ const stat = Object.create(fs.Stats.prototype);
248
+ const isFile = type === "file";
249
+ const timestamp = /* @__PURE__ */ new Date(0);
250
+ Object.defineProperties(stat, {
251
+ size: { value: 0 },
252
+ atimeMs: { value: 0 },
253
+ mtimeMs: { value: 0 },
254
+ ctimeMs: { value: 0 },
255
+ birthtimeMs: { value: 0 },
256
+ atime: { value: timestamp },
257
+ mtime: { value: timestamp },
258
+ ctime: { value: timestamp },
259
+ birthtime: { value: timestamp },
260
+ isFile: { value: () => isFile },
261
+ isDirectory: { value: () => !isFile },
262
+ isSymbolicLink: { value: () => false }
263
+ });
264
+ return stat;
265
+ }
266
+ function createNotFoundError(syscall, filepath) {
267
+ const err = /* @__PURE__ */ new Error(`ENOENT: no such file or directory, ${syscall} '${filepath}'`);
268
+ err.code = "ENOENT";
269
+ err.errno = -2;
270
+ err.syscall = syscall;
271
+ err.path = filepath;
272
+ return err;
273
+ }
274
+
275
+ //#endregion
276
+ export { ManifestLoaderFS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eggjs/loader-fs",
3
- "version": "1.0.0-beta.25",
3
+ "version": "1.0.0-beta.26",
4
4
  "description": "Loader-facing filesystem abstraction for Egg",
5
5
  "keywords": [
6
6
  "egg",
@@ -41,12 +41,13 @@
41
41
  "test": "vitest run"
42
42
  },
43
43
  "dependencies": {
44
- "@eggjs/utils": "5.0.2-beta.25",
44
+ "@eggjs/utils": "5.0.2-beta.26",
45
45
  "globby": "^11.0.2",
46
+ "multimatch": "^7.0.0",
46
47
  "utility": "^2.5.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@eggjs/tsconfig": "3.1.2-beta.25",
50
+ "@eggjs/tsconfig": "3.1.2-beta.26",
50
51
  "@types/node": "^24.10.2",
51
52
  "typescript": "^5.9.3",
52
53
  "vitest": "^4.0.15"