@gobing-ai/ts-runtime 0.3.0 → 0.3.2
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 +234 -176
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/context.d.ts +28 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +45 -2
- package/dist/file-system-cf.d.ts +25 -0
- package/dist/file-system-cf.d.ts.map +1 -0
- package/dist/file-system-cf.js +59 -0
- package/dist/file-system-node.d.ts +29 -0
- package/dist/file-system-node.d.ts.map +1 -0
- package/dist/file-system-node.js +94 -0
- package/dist/file-system.d.ts +47 -0
- package/dist/file-system.d.ts.map +1 -0
- package/dist/file-system.js +0 -0
- package/dist/fs.d.ts +31 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +32 -19
- package/dist/index.d.ts +21 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +65 -4
- package/dist/platform.d.ts +12 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +41 -0
- package/dist/plugin/capability-registry.d.ts +35 -0
- package/dist/plugin/capability-registry.d.ts.map +1 -0
- package/dist/plugin/capability-registry.js +43 -0
- package/dist/plugin/extension-loader.d.ts +66 -0
- package/dist/plugin/extension-loader.d.ts.map +1 -0
- package/dist/plugin/extension-loader.js +47 -0
- package/dist/plugin/extension-path.d.ts +15 -0
- package/dist/plugin/extension-path.d.ts.map +1 -0
- package/dist/plugin/extension-path.js +20 -0
- package/dist/plugin/index.d.ts +4 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +1 -0
- package/dist/process-executor.d.ts +77 -19
- package/dist/process-executor.d.ts.map +1 -1
- package/dist/process-executor.js +209 -37
- package/dist/runtime-cf.d.ts +6 -0
- package/dist/runtime-cf.d.ts.map +1 -0
- package/dist/runtime-cf.js +33 -0
- package/dist/runtime-factory.d.ts +24 -0
- package/dist/runtime-factory.d.ts.map +1 -0
- package/dist/runtime-factory.js +0 -0
- package/dist/runtime-node-bun.d.ts +8 -0
- package/dist/runtime-node-bun.d.ts.map +1 -0
- package/dist/runtime-node-bun.js +67 -0
- package/dist/schema-validation.d.ts +16 -0
- package/dist/schema-validation.d.ts.map +1 -1
- package/dist/schema-validation.js +9 -4
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/config.ts +16 -4
- package/src/context.ts +58 -4
- package/src/file-system-cf.ts +74 -0
- package/src/file-system-node.ts +122 -0
- package/src/file-system.ts +55 -0
- package/src/fs.ts +35 -18
- package/src/index.ts +57 -2
- package/src/path.ts +68 -4
- package/src/platform.ts +47 -0
- package/src/plugin/capability-registry.ts +58 -0
- package/src/plugin/extension-loader.ts +105 -0
- package/src/plugin/extension-path.ts +20 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin.ts +1 -0
- package/src/process-executor.ts +296 -58
- package/src/runtime-cf.ts +44 -0
- package/src/runtime-factory.ts +28 -0
- package/src/runtime-node-bun.ts +83 -0
- package/src/schema-validation.ts +20 -4
- package/src/types.ts +4 -0
package/dist/fs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { findProjectRoot } from './file-system-node.js';
|
|
2
3
|
import { dirnamePath, getProcessCwd, joinPath, resolvePath } from './path.js';
|
|
3
4
|
let fsPromisesModule = null;
|
|
4
5
|
let fsModule = null;
|
|
@@ -10,6 +11,8 @@ function nodeFs() {
|
|
|
10
11
|
fsModule ??= import('node:fs');
|
|
11
12
|
return fsModule;
|
|
12
13
|
}
|
|
14
|
+
/** {@link FileSystem} backed by `node:fs/promises`. Lazy-loads the module to avoid top-level import cost. */
|
|
15
|
+
/** @deprecated Use createNodeFileSystem() from './file-system-node.js' instead. */
|
|
13
16
|
export class NodeFileSystem {
|
|
14
17
|
async readFile(path) {
|
|
15
18
|
const { readFile } = await nodeFsPromises();
|
|
@@ -78,6 +81,8 @@ export class NodeFileSystem {
|
|
|
78
81
|
return new LazyNodeLogStream(path);
|
|
79
82
|
}
|
|
80
83
|
}
|
|
84
|
+
/** {@link SyncFileSystem} backed by `node:fs` synchronous APIs. */
|
|
85
|
+
/** @deprecated Use createNodeFileSystem() from './file-system-node.js' instead — its FileSystem interface has union return types, eliminating the need for a separate sync implementation. */
|
|
81
86
|
export class NodeSyncFileSystem {
|
|
82
87
|
readFile(path) {
|
|
83
88
|
return readFileSync(path, 'utf-8');
|
|
@@ -149,6 +154,8 @@ class LazyNodeLogStream {
|
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
const CLOUDFLARE_FS_ERROR = 'FileSystem is not available on Cloudflare Workers. Use D1, KV, or R2.';
|
|
157
|
+
/** {@link FileSystem} stub for Cloudflare Workers — all file operations throw. Use D1, KV, or R2 instead. */
|
|
158
|
+
/** @deprecated Use createCfFileSystem() from './file-system-cf.js' instead. */
|
|
152
159
|
export class CloudflareFileSystem {
|
|
153
160
|
async readFile(path) {
|
|
154
161
|
throw unsupportedCloudflareFs('readFile', path);
|
|
@@ -191,6 +198,8 @@ function unsupportedCloudflareFs(operation, path) {
|
|
|
191
198
|
return new Error(`CloudflareFileSystem.${operation} failed for "${path}": ${CLOUDFLARE_FS_ERROR}`);
|
|
192
199
|
}
|
|
193
200
|
let activeFileSystem = new NodeFileSystem();
|
|
201
|
+
/** Swaps the active global file system, returning a restore function for the previous instance. */
|
|
202
|
+
/** @deprecated Use RuntimeFactory.createFileSystem() or ctx.require('fileSystem') instead. The global swap is replaced by factory-based DI. */
|
|
194
203
|
export function setFileSystem(fileSystem) {
|
|
195
204
|
const previous = activeFileSystem;
|
|
196
205
|
activeFileSystem = fileSystem;
|
|
@@ -198,38 +207,50 @@ export function setFileSystem(fileSystem) {
|
|
|
198
207
|
activeFileSystem = previous;
|
|
199
208
|
};
|
|
200
209
|
}
|
|
210
|
+
/** Returns the currently active global {@link FileSystem} instance. */
|
|
211
|
+
/** @deprecated Use RuntimeFactory.createFileSystem() or ctx.require('fileSystem') instead. */
|
|
201
212
|
export function getFs() {
|
|
202
213
|
return activeFileSystem;
|
|
203
214
|
}
|
|
215
|
+
/** Creates parent directories for a file path before writing. */
|
|
204
216
|
export async function ensureDirForFile(path, fs = getFs()) {
|
|
205
217
|
await fs.mkdir(dirnamePath(path));
|
|
206
218
|
}
|
|
219
|
+
/** Synchronous variant of {@link ensureDirForFile}. */
|
|
220
|
+
/** @deprecated The new createNodeFileSystem() handles parent-directory creation internally. */
|
|
207
221
|
export function ensureDirForFileSync(path, fs) {
|
|
208
222
|
fs.mkdir(dirnamePath(path));
|
|
209
223
|
}
|
|
224
|
+
/** Atomically writes a file by writing to a temp path then renaming, avoiding partial writes on crash. */
|
|
210
225
|
export async function atomicWriteFile(path, content, fs = getFs()) {
|
|
211
226
|
await ensureDirForFile(path, fs);
|
|
212
227
|
const tempPath = `${path}.${getProcessPid()}.${uniqueToken()}.tmp`;
|
|
213
228
|
await fs.writeFile(tempPath, content);
|
|
214
229
|
await fs.rename(tempPath, path);
|
|
215
230
|
}
|
|
231
|
+
/** Atomically writes a value as JSON with trailing newline. */
|
|
216
232
|
export async function atomicWriteJson(path, value, fs = getFs()) {
|
|
217
233
|
await atomicWriteFile(path, `${JSON.stringify(value, null, 2)}\n`, fs);
|
|
218
234
|
}
|
|
235
|
+
/** Reads and parses a JSON file. */
|
|
219
236
|
export async function readJsonFile(path, fs = getFs()) {
|
|
220
237
|
return JSON.parse(await fs.readFile(path));
|
|
221
238
|
}
|
|
239
|
+
/** Writes a value as JSON with 2-space indentation and trailing newline. */
|
|
222
240
|
export async function writeJsonFile(path, value, fs = getFs()) {
|
|
223
241
|
await fs.writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
224
242
|
}
|
|
225
|
-
|
|
243
|
+
/** Recursively walks a directory, returning sorted paths to all files, optionally excluding entries by name. */
|
|
244
|
+
export async function walkDir(path, fs = getFs(), exclude) {
|
|
226
245
|
const entries = (await fs.readDir(path)).sort();
|
|
227
246
|
const result = [];
|
|
228
247
|
for (const entry of entries) {
|
|
248
|
+
if (exclude?.has(entry))
|
|
249
|
+
continue;
|
|
229
250
|
const fullPath = joinPath(path, entry);
|
|
230
251
|
const entryStat = await fs.stat(fullPath);
|
|
231
252
|
if (entryStat?.isDirectory()) {
|
|
232
|
-
result.push(...(await walkDir(fullPath, fs)));
|
|
253
|
+
result.push(...(await walkDir(fullPath, fs, exclude)));
|
|
233
254
|
}
|
|
234
255
|
else if (entryStat?.isFile()) {
|
|
235
256
|
result.push(fullPath);
|
|
@@ -237,31 +258,23 @@ export async function walkDir(path, fs = getFs()) {
|
|
|
237
258
|
}
|
|
238
259
|
return result;
|
|
239
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Walks up from `startDir` looking for a `package.json` or `bun.lock` to locate the project root.
|
|
263
|
+
*
|
|
264
|
+
* @deprecated Use {@link import('./file-system-node.js').findProjectRoot} (or `createNodeFileSystem().getProjectRoot()`).
|
|
265
|
+
* This delegates to the single shared implementation.
|
|
266
|
+
*/
|
|
240
267
|
export function getProjectRoot(startDir = getProcessCwd()) {
|
|
241
|
-
|
|
242
|
-
for (let i = 0; i < 12; i++) {
|
|
243
|
-
if (hasBunFile(joinPath(current, 'bun.lock')) || hasBunFile(joinPath(current, 'package.json'))) {
|
|
244
|
-
return current;
|
|
245
|
-
}
|
|
246
|
-
const parent = dirnamePath(current);
|
|
247
|
-
if (parent === current)
|
|
248
|
-
return startDir;
|
|
249
|
-
current = parent;
|
|
250
|
-
}
|
|
251
|
-
return startDir;
|
|
268
|
+
return findProjectRoot(startDir);
|
|
252
269
|
}
|
|
270
|
+
/** Resolves path segments relative to the project root. */
|
|
253
271
|
export function resolveProjectPath(...segments) {
|
|
254
272
|
return resolvePath(getProjectRoot(), ...segments);
|
|
255
273
|
}
|
|
274
|
+
/** Creates a {@link LogStream} at the given path using the active file system. */
|
|
256
275
|
export function createLogStream(path, fs = getFs()) {
|
|
257
276
|
return fs.createLogStream(path);
|
|
258
277
|
}
|
|
259
|
-
function hasBunFile(path) {
|
|
260
|
-
const bun = globalThis.Bun;
|
|
261
|
-
if (bun === undefined)
|
|
262
|
-
return false;
|
|
263
|
-
return bun.file(path).size !== 0;
|
|
264
|
-
}
|
|
265
278
|
function getProcessPid() {
|
|
266
279
|
return globalThis.process?.pid ?? 0;
|
|
267
280
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
export * from './config';
|
|
2
2
|
export * from './context';
|
|
3
|
-
export
|
|
3
|
+
export { createRuntimeContextFromFactory } from './context';
|
|
4
|
+
export type { FileStat, FileSystem } from './file-system';
|
|
5
|
+
export { createCfFileSystem } from './file-system-cf';
|
|
6
|
+
export { createNodeFileSystem, findProjectRoot } from './file-system-node';
|
|
7
|
+
export { atomicWriteFile, atomicWriteJson, CloudflareFileSystem, createLogStream, ensureDirForFile, ensureDirForFileSync, type FileSystem as LegacyFileSystem, getFs, getProjectRoot, NodeFileSystem, NodeSyncFileSystem, readJsonFile, resolveProjectPath, type SyncFileSystem, setFileSystem, walkDir, writeJsonFile, } from './fs';
|
|
4
8
|
export * from './path';
|
|
5
|
-
export
|
|
9
|
+
export { _resetRuntimeFactory, isCloudflareWorkerRuntime, loadRuntimeFactory } from './platform';
|
|
10
|
+
export type { OutputPolicy, PipeProcess, PipeProcessOptions, ProcessEventDetail, ProcessEventSink, ProcessEvents, ProcessExecutorConfig, ProcessExitReason, ProcessOptions, ProcessResult, ProcessSignal, TracerPort, } from './process-executor';
|
|
11
|
+
export { ProcessExecutor } from './process-executor';
|
|
12
|
+
export { cloudflareWorkersFactory } from './runtime-cf';
|
|
13
|
+
export type { RuntimeFactory } from './runtime-factory';
|
|
14
|
+
export { _resetNodeFileSystem, nodeBunFactory } from './runtime-node-bun';
|
|
6
15
|
export * from './schema-validation';
|
|
7
16
|
export * from './types';
|
|
17
|
+
export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeProcessExecutor } from './process-executor';
|
|
18
|
+
/**
|
|
19
|
+
* @deprecated Use {@link ProcessExecutor} directly for async execution.
|
|
20
|
+
* Use `Bun.spawnSync` or `child_process.spawnSync` for sync.
|
|
21
|
+
*/
|
|
22
|
+
export type SyncProcessExecutor = InstanceType<typeof import('./process-executor').BunSyncProcessExecutor>;
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use {@link ProcessExecutor.runStreaming} instead.
|
|
25
|
+
*/
|
|
26
|
+
export type PipeProcessSpawner = InstanceType<typeof import('./process-executor').BunPipeProcessSpawner>;
|
|
8
27
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,+BAA+B,EAAE,MAAM,WAAW,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EACH,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,UAAU,IAAI,gBAAgB,EACnC,KAAK,EACL,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,kBAAkB,EAClB,KAAK,cAAc,EACnB,aAAa,EACb,OAAO,EACP,aAAa,GAChB,MAAM,MAAM,CAAC;AACd,cAAc,QAAQ,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACjG,YAAY,EACR,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,UAAU,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC1E,cAAc,qBAAqB,CAAC;AACpC,cAAc,SAAS,CAAC;AAIxB,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAExG;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,sBAAsB,CAAC,CAAC;AAE3G;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC,cAAc,oBAAoB,EAAE,qBAAqB,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
export * from './config.js';
|
|
2
2
|
export * from './context.js';
|
|
3
|
-
export
|
|
3
|
+
export { createRuntimeContextFromFactory } from './context.js';
|
|
4
|
+
export { createCfFileSystem } from './file-system-cf.js';
|
|
5
|
+
export { createNodeFileSystem, findProjectRoot } from './file-system-node.js';
|
|
6
|
+
export { atomicWriteFile, atomicWriteJson, CloudflareFileSystem, createLogStream, ensureDirForFile, ensureDirForFileSync, getFs, getProjectRoot, NodeFileSystem, NodeSyncFileSystem, readJsonFile, resolveProjectPath, setFileSystem, walkDir, writeJsonFile, } from './fs.js';
|
|
4
7
|
export * from './path.js';
|
|
5
|
-
export
|
|
8
|
+
export { _resetRuntimeFactory, isCloudflareWorkerRuntime, loadRuntimeFactory } from './platform.js';
|
|
9
|
+
export { ProcessExecutor } from './process-executor.js';
|
|
10
|
+
export { cloudflareWorkersFactory } from './runtime-cf.js';
|
|
11
|
+
export { _resetNodeFileSystem, nodeBunFactory } from './runtime-node-bun.js';
|
|
6
12
|
export * from './schema-validation.js';
|
|
7
13
|
export * from './types.js';
|
|
14
|
+
// ── Deprecated re-exports (backward compatibility) ──────────────────────
|
|
15
|
+
export { BunPipeProcessSpawner, BunSyncProcessExecutor, NodeProcessExecutor } from './process-executor.js';
|
package/dist/path.d.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
/** Replaces Windows backslashes with forward slashes for consistent POSIX-style path handling. */
|
|
1
2
|
export declare function normalizeSeparators(path: string): string;
|
|
3
|
+
/** Platform-specific path segment separator (`'/'` on POSIX, `'\\'` on Windows). */
|
|
4
|
+
export declare const SEP: string;
|
|
5
|
+
/** Returns `true` for POSIX absolute paths and Windows drive paths (`C:/…`). */
|
|
2
6
|
export declare function isAbsolutePath(path: string): boolean;
|
|
7
|
+
/** Returns the parent directory of a path. Platform-independent — avoids `node:path`. */
|
|
3
8
|
export declare function dirnamePath(path: string): string;
|
|
9
|
+
/** Return the last segment of a path. Optionally strip a trailing extension. */
|
|
10
|
+
export declare function basenamePath(p: string, ext?: string): string;
|
|
11
|
+
/** Compute a platform-independent relative path from `from` to `to`. Both paths should be absolute. */
|
|
12
|
+
export declare function relativePath(from: string, to: string): string;
|
|
13
|
+
/** Joins path segments with `/`, normalizing separators and collapsing redundant slashes. */
|
|
4
14
|
export declare function joinPath(...segments: string[]): string;
|
|
15
|
+
/** Resolves a sequence of path segments to an absolute path, collapsing `..` and `.`. */
|
|
5
16
|
export declare function resolvePath(...segments: string[]): string;
|
|
17
|
+
/** Returns `process.cwd()` if available, or `/` as fallback (Cloudflare Workers). */
|
|
6
18
|
export declare function getProcessCwd(): string;
|
|
7
19
|
//# sourceMappingURL=path.d.ts.map
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAIA,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAIA,kGAAkG;AAClG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,oFAAoF;AACpF,eAAO,MAAM,GAAG,EAAE,MACgF,CAAC;AAEnG,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpD;AAmBD,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAWhD;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAkB7D;AAED,6FAA6F;AAC7F,wBAAgB,QAAQ,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAMtD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBzD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/path.js
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
// Runtime-portable path math. Deliberately avoids `node:path` so the same logic works on
|
|
2
2
|
// `cloudflare-workers` (no `node:*`) as on node-bun (ADR-008). POSIX-style separators throughout;
|
|
3
3
|
// Windows drive paths (`C:/...`) are normalized and treated as absolute.
|
|
4
|
+
/** Replaces Windows backslashes with forward slashes for consistent POSIX-style path handling. */
|
|
4
5
|
export function normalizeSeparators(path) {
|
|
5
6
|
return path.replaceAll('\\', '/');
|
|
6
7
|
}
|
|
8
|
+
/** Platform-specific path segment separator (`'/'` on POSIX, `'\\'` on Windows). */
|
|
9
|
+
export const SEP = globalThis.process?.platform === 'win32' ? '\\' : '/';
|
|
10
|
+
/** Returns `true` for POSIX absolute paths and Windows drive paths (`C:/…`). */
|
|
7
11
|
export function isAbsolutePath(path) {
|
|
8
12
|
return path.startsWith('/') || /^[A-Za-z]:\//.test(normalizeSeparators(path));
|
|
9
13
|
}
|
|
14
|
+
function splitRoot(path) {
|
|
15
|
+
const normalized = normalizeSeparators(path);
|
|
16
|
+
const drive = normalized.match(/^([A-Za-z]:)(?:\/|$)/);
|
|
17
|
+
if (drive) {
|
|
18
|
+
return { root: drive[1] ?? '', rest: normalized.slice((drive[1] ?? '').length).replace(/^\/+/, '') };
|
|
19
|
+
}
|
|
20
|
+
if (normalized.startsWith('/')) {
|
|
21
|
+
return { root: '/', rest: normalized.replace(/^\/+/, '') };
|
|
22
|
+
}
|
|
23
|
+
return { root: '', rest: normalized };
|
|
24
|
+
}
|
|
25
|
+
function pathParts(path) {
|
|
26
|
+
const { root, rest } = splitRoot(path);
|
|
27
|
+
return { root, parts: rest.split('/').filter(Boolean) };
|
|
28
|
+
}
|
|
29
|
+
/** Returns the parent directory of a path. Platform-independent — avoids `node:path`. */
|
|
10
30
|
export function dirnamePath(path) {
|
|
11
31
|
const input = normalizeSeparators(path);
|
|
32
|
+
const { root } = splitRoot(input);
|
|
33
|
+
if (root !== '' && input.replace(/\/+$/, '') === root)
|
|
34
|
+
return root === '/' ? '/' : `${root}/`;
|
|
12
35
|
if (/^\/+$/.test(input))
|
|
13
36
|
return '/';
|
|
14
37
|
const normalized = input.replace(/\/+$/, '');
|
|
@@ -21,6 +44,36 @@ export function dirnamePath(path) {
|
|
|
21
44
|
return '/';
|
|
22
45
|
return normalized.slice(0, index);
|
|
23
46
|
}
|
|
47
|
+
/** Return the last segment of a path. Optionally strip a trailing extension. */
|
|
48
|
+
export function basenamePath(p, ext) {
|
|
49
|
+
const normalized = normalizeSeparators(p).replace(/\/+$/, '');
|
|
50
|
+
const index = normalized.lastIndexOf('/');
|
|
51
|
+
let base = index < 0 ? normalized : normalized.slice(index + 1);
|
|
52
|
+
if (ext !== undefined && base !== ext && base.endsWith(ext)) {
|
|
53
|
+
base = base.slice(0, -ext.length);
|
|
54
|
+
}
|
|
55
|
+
return base;
|
|
56
|
+
}
|
|
57
|
+
/** Compute a platform-independent relative path from `from` to `to`. Both paths should be absolute. */
|
|
58
|
+
export function relativePath(from, to) {
|
|
59
|
+
const fromParsed = pathParts(resolvePath(from));
|
|
60
|
+
const toParsed = pathParts(resolvePath(to));
|
|
61
|
+
if (fromParsed.root.toLowerCase() !== toParsed.root.toLowerCase()) {
|
|
62
|
+
return resolvePath(to);
|
|
63
|
+
}
|
|
64
|
+
const fromParts = fromParsed.parts;
|
|
65
|
+
const toParts = toParsed.parts;
|
|
66
|
+
// Strip common prefix.
|
|
67
|
+
let i = 0;
|
|
68
|
+
const minLen = Math.min(fromParts.length, toParts.length);
|
|
69
|
+
while (i < minLen && fromParts[i] === toParts[i])
|
|
70
|
+
i++;
|
|
71
|
+
const up = fromParts.slice(i).map(() => '..');
|
|
72
|
+
const down = toParts.slice(i);
|
|
73
|
+
const result = [...up, ...down].join('/');
|
|
74
|
+
return result || '.';
|
|
75
|
+
}
|
|
76
|
+
/** Joins path segments with `/`, normalizing separators and collapsing redundant slashes. */
|
|
24
77
|
export function joinPath(...segments) {
|
|
25
78
|
const filtered = segments.filter((segment) => segment.length > 0).map(normalizeSeparators);
|
|
26
79
|
if (filtered.length === 0)
|
|
@@ -29,6 +82,7 @@ export function joinPath(...segments) {
|
|
|
29
82
|
const joined = filtered.join('/').replace(/\/+/g, '/');
|
|
30
83
|
return absolute ? joined : joined.replace(/^\//, '');
|
|
31
84
|
}
|
|
85
|
+
/** Resolves a sequence of path segments to an absolute path, collapsing `..` and `.`. */
|
|
32
86
|
export function resolvePath(...segments) {
|
|
33
87
|
const candidates = segments.length === 0 ? [getProcessCwd()] : segments;
|
|
34
88
|
let resolved = '';
|
|
@@ -37,19 +91,26 @@ export function resolvePath(...segments) {
|
|
|
37
91
|
continue;
|
|
38
92
|
resolved = isAbsolutePath(segment) ? segment : joinPath(resolved || getProcessCwd(), segment);
|
|
39
93
|
}
|
|
94
|
+
const { root, rest } = splitRoot(resolved);
|
|
40
95
|
const parts = [];
|
|
41
|
-
const absolute =
|
|
42
|
-
for (const part of
|
|
96
|
+
const absolute = root !== '';
|
|
97
|
+
for (const part of rest.split('/')) {
|
|
43
98
|
if (part === '' || part === '.')
|
|
44
99
|
continue;
|
|
45
100
|
if (part === '..') {
|
|
46
|
-
parts.
|
|
101
|
+
if (parts.length > 0)
|
|
102
|
+
parts.pop();
|
|
47
103
|
continue;
|
|
48
104
|
}
|
|
49
105
|
parts.push(part);
|
|
50
106
|
}
|
|
51
|
-
|
|
107
|
+
if (!absolute)
|
|
108
|
+
return parts.join('/') || '.';
|
|
109
|
+
if (root === '/')
|
|
110
|
+
return `/${parts.join('/')}` || '/';
|
|
111
|
+
return parts.length > 0 ? `${root}/${parts.join('/')}` : `${root}/`;
|
|
52
112
|
}
|
|
113
|
+
/** Returns `process.cwd()` if available, or `/` as fallback (Cloudflare Workers). */
|
|
53
114
|
export function getProcessCwd() {
|
|
54
115
|
return globalThis.process?.cwd?.() ?? '/';
|
|
55
116
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RuntimeFactory } from './runtime-factory';
|
|
2
|
+
/** True when running in a Cloudflare Worker (server-tier only). */
|
|
3
|
+
export declare function isCloudflareWorkerRuntime(): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Lazy-load and cache the appropriate {@link RuntimeFactory} based on environment detection.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadRuntimeFactory(): Promise<RuntimeFactory>;
|
|
8
|
+
/**
|
|
9
|
+
* Reset the cached runtime factory and name (for test isolation).
|
|
10
|
+
*/
|
|
11
|
+
export declare function _resetRuntimeFactory(): void;
|
|
12
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAQxD,mEAAmE;AACnE,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAKD;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAalE;AAQD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Single authoritative source for runtime detection.
|
|
2
|
+
// All other code MUST consume RuntimeFactory via loadRuntimeFactory().
|
|
3
|
+
// Probe-once contract: each primitive is invoked at most once per process and
|
|
4
|
+
// the result is cached in `_factory` / `_runtimeName`.
|
|
5
|
+
/** True when running in a Cloudflare Worker (server-tier only). */
|
|
6
|
+
export function isCloudflareWorkerRuntime() {
|
|
7
|
+
return globalThis.navigator?.userAgent?.startsWith('Cloudflare-Workers') ?? false;
|
|
8
|
+
}
|
|
9
|
+
let _factory;
|
|
10
|
+
let _runtimeName;
|
|
11
|
+
/**
|
|
12
|
+
* Lazy-load and cache the appropriate {@link RuntimeFactory} based on environment detection.
|
|
13
|
+
*/
|
|
14
|
+
export async function loadRuntimeFactory() {
|
|
15
|
+
if (_factory)
|
|
16
|
+
return _factory;
|
|
17
|
+
let factory;
|
|
18
|
+
if (getRuntimeName() === 'cloudflare-workers') {
|
|
19
|
+
const { cloudflareWorkersFactory } = await import('./runtime-cf.js');
|
|
20
|
+
factory = cloudflareWorkersFactory;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const { nodeBunFactory } = await import('./runtime-node-bun.js');
|
|
24
|
+
factory = nodeBunFactory;
|
|
25
|
+
}
|
|
26
|
+
_factory = factory;
|
|
27
|
+
return factory;
|
|
28
|
+
}
|
|
29
|
+
function getRuntimeName() {
|
|
30
|
+
if (_runtimeName)
|
|
31
|
+
return _runtimeName;
|
|
32
|
+
_runtimeName = isCloudflareWorkerRuntime() ? 'cloudflare-workers' : 'node-bun';
|
|
33
|
+
return _runtimeName;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reset the cached runtime factory and name (for test isolation).
|
|
37
|
+
*/
|
|
38
|
+
export function _resetRuntimeFactory() {
|
|
39
|
+
_factory = undefined;
|
|
40
|
+
_runtimeName = undefined;
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Registry origin for a host capability. */
|
|
2
|
+
export type CapabilityOrigin = 'builtin' | 'extension';
|
|
3
|
+
/** Registry entry metadata. */
|
|
4
|
+
export interface CapabilityEntry<TCapability> {
|
|
5
|
+
/** Capability implementation. */
|
|
6
|
+
capability: TCapability;
|
|
7
|
+
/** Registration origin. */
|
|
8
|
+
origin: CapabilityOrigin;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generic, domain-agnostic capability registry shared by engine hosts.
|
|
12
|
+
*
|
|
13
|
+
* Stores named capabilities of an opaque type `TCapability` with replace-by-name
|
|
14
|
+
* semantics and `origin` metadata. It knows nothing about what a capability *is* —
|
|
15
|
+
* each engine supplies its own type and owns its own error types and override
|
|
16
|
+
* semantics (the registry only reports what it stores).
|
|
17
|
+
*/
|
|
18
|
+
export declare class CapabilityRegistry<TCapability> {
|
|
19
|
+
private readonly kind;
|
|
20
|
+
private readonly capabilities;
|
|
21
|
+
constructor(kind: string);
|
|
22
|
+
/** Register or replace a capability. */
|
|
23
|
+
register(name: string, capability: TCapability, origin?: CapabilityOrigin): void;
|
|
24
|
+
/** Return true when a capability exists. */
|
|
25
|
+
has(name: string): boolean;
|
|
26
|
+
/** Get a registered capability or throw a clear error. */
|
|
27
|
+
get(name: string): TCapability;
|
|
28
|
+
/** Get a registered entry (capability plus origin) without throwing. */
|
|
29
|
+
getEntry(name: string): CapabilityEntry<TCapability> | undefined;
|
|
30
|
+
/** List registered capability names in insertion order. */
|
|
31
|
+
list(): string[];
|
|
32
|
+
/** List registered entries (name + capability + origin) in insertion order. */
|
|
33
|
+
entries(): Array<readonly [string, CapabilityEntry<TCapability>]>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=capability-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-registry.d.ts","sourceRoot":"","sources":["../../src/plugin/capability-registry.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,WAAW,CAAC;AAEvD,+BAA+B;AAC/B,MAAM,WAAW,eAAe,CAAC,WAAW;IACxC,iCAAiC;IACjC,UAAU,EAAE,WAAW,CAAC;IACxB,2BAA2B;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAkB,CAAC,WAAW;IAG3B,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmD;gBAEnD,IAAI,EAAE,MAAM;IAEzC,wCAAwC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAE,gBAA8B,GAAG,IAAI;IAI7F,4CAA4C;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,0DAA0D;IAC1D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW;IAQ9B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAAC,WAAW,CAAC,GAAG,SAAS;IAIhE,2DAA2D;IAC3D,IAAI,IAAI,MAAM,EAAE;IAIhB,+EAA+E;IAC/E,OAAO,IAAI,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;CAGpE"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic, domain-agnostic capability registry shared by engine hosts.
|
|
3
|
+
*
|
|
4
|
+
* Stores named capabilities of an opaque type `TCapability` with replace-by-name
|
|
5
|
+
* semantics and `origin` metadata. It knows nothing about what a capability *is* —
|
|
6
|
+
* each engine supplies its own type and owns its own error types and override
|
|
7
|
+
* semantics (the registry only reports what it stores).
|
|
8
|
+
*/
|
|
9
|
+
export class CapabilityRegistry {
|
|
10
|
+
kind;
|
|
11
|
+
capabilities = new Map();
|
|
12
|
+
constructor(kind) {
|
|
13
|
+
this.kind = kind;
|
|
14
|
+
}
|
|
15
|
+
/** Register or replace a capability. */
|
|
16
|
+
register(name, capability, origin = 'extension') {
|
|
17
|
+
this.capabilities.set(name, { capability, origin });
|
|
18
|
+
}
|
|
19
|
+
/** Return true when a capability exists. */
|
|
20
|
+
has(name) {
|
|
21
|
+
return this.capabilities.has(name);
|
|
22
|
+
}
|
|
23
|
+
/** Get a registered capability or throw a clear error. */
|
|
24
|
+
get(name) {
|
|
25
|
+
const entry = this.capabilities.get(name);
|
|
26
|
+
if (entry === undefined) {
|
|
27
|
+
throw new Error(`Unknown ${this.kind}: ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return entry.capability;
|
|
30
|
+
}
|
|
31
|
+
/** Get a registered entry (capability plus origin) without throwing. */
|
|
32
|
+
getEntry(name) {
|
|
33
|
+
return this.capabilities.get(name);
|
|
34
|
+
}
|
|
35
|
+
/** List registered capability names in insertion order. */
|
|
36
|
+
list() {
|
|
37
|
+
return [...this.capabilities.keys()];
|
|
38
|
+
}
|
|
39
|
+
/** List registered entries (name + capability + origin) in insertion order. */
|
|
40
|
+
entries() {
|
|
41
|
+
return [...this.capabilities.entries()];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single extension module reference.
|
|
3
|
+
*
|
|
4
|
+
* `kind` is an engine-defined tag the registration callback uses to route the
|
|
5
|
+
* module to the right registry — the loader itself never interprets it. `path` is
|
|
6
|
+
* the relative path as authored; the loader **derives** the import target by
|
|
7
|
+
* resolving `path` against `baseDir` *after* validating it, so the trust guard
|
|
8
|
+
* always governs the actual module that gets imported. `sourceName` identifies the
|
|
9
|
+
* declaring config for diagnostics.
|
|
10
|
+
*/
|
|
11
|
+
export interface ExtensionRef<TExtensionKind extends string = string> {
|
|
12
|
+
/** Engine-defined capability tag, used only by the registration callback. */
|
|
13
|
+
readonly kind: TExtensionKind;
|
|
14
|
+
/** Relative path as authored, enforced by {@link assertRelativeExtensionPath}. */
|
|
15
|
+
readonly path: string;
|
|
16
|
+
/** Absolute directory the authored `path` is resolved against. */
|
|
17
|
+
readonly baseDir: string;
|
|
18
|
+
/** Name of the config (preset, workflow, …) that declared this ref. */
|
|
19
|
+
readonly sourceName: string;
|
|
20
|
+
}
|
|
21
|
+
/** Options controlling extension-module loading. */
|
|
22
|
+
export interface LoadExtensionsOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Whether to actually import extension modules. Defaults to `false`: loading
|
|
25
|
+
* arbitrary code is a trust decision the caller must make explicitly. When refs
|
|
26
|
+
* exist and this is not `true`, loading throws **before any import**.
|
|
27
|
+
*/
|
|
28
|
+
readonly allowExtensions?: boolean;
|
|
29
|
+
/** Optional sink for non-fatal warnings (e.g. capability overrides). */
|
|
30
|
+
readonly logger?: {
|
|
31
|
+
warn: (message: string) => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Required module loader. The generic core never performs a dynamic `import`
|
|
35
|
+
* itself — the embedder supplies the import policy (typically
|
|
36
|
+
* `(absPath) => import(absPath)`), and tests pass a stub. Keeping this explicit
|
|
37
|
+
* means the shared core has no ambient code-loading capability of its own.
|
|
38
|
+
*/
|
|
39
|
+
readonly moduleLoader: (absPath: string) => Promise<Record<string, unknown>>;
|
|
40
|
+
}
|
|
41
|
+
/** A validated extension module export: an object carrying at least a string `name`. */
|
|
42
|
+
export interface LoadedExtension {
|
|
43
|
+
/** Stable name of the contributed capability bundle. */
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** Engine-specific contribution payload (actions, evaluators, …). */
|
|
46
|
+
readonly [key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Import each extension module behind an explicit trust gate, validate its export,
|
|
50
|
+
* then hand it to an engine-provided registration callback.
|
|
51
|
+
*
|
|
52
|
+
* The loader is domain-agnostic: it does not know which registry a module belongs to.
|
|
53
|
+
* It enforces the trust boundary (gate + relative-path guard), validates the
|
|
54
|
+
* default/named-`extension` export shape, and delegates routing to `register`.
|
|
55
|
+
*
|
|
56
|
+
* Security invariants:
|
|
57
|
+
* - No refs → no-op.
|
|
58
|
+
* - Refs present but `allowExtensions !== true` → throws **before** any `import` or
|
|
59
|
+
* `moduleLoader` call, so a declared extension is never silently dropped.
|
|
60
|
+
* - Every ref's authored path is re-validated by {@link assertRelativeExtensionPath}.
|
|
61
|
+
*
|
|
62
|
+
* @throws When extensions are present but `allowExtensions` is not `true`, when a path
|
|
63
|
+
* fails the trust guard, or when a module lacks a valid `name`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function loadExtensionModules<TExtensionKind extends string>(refs: readonly ExtensionRef<TExtensionKind>[], options: LoadExtensionsOptions, register: (ref: ExtensionRef<TExtensionKind>, extension: LoadedExtension) => void | Promise<void>): Promise<void>;
|
|
66
|
+
//# sourceMappingURL=extension-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-loader.d.ts","sourceRoot":"","sources":["../../src/plugin/extension-loader.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY,CAAC,cAAc,SAAS,MAAM,GAAG,MAAM;IAChE,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC/B;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IACnC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IACtD;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChF;AAED,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CAAC,cAAc,SAAS,MAAM,EACpE,IAAI,EAAE,SAAS,YAAY,CAAC,cAAc,CAAC,EAAE,EAC7C,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,EAAE,CAAC,GAAG,EAAE,YAAY,CAAC,cAAc,CAAC,EAAE,SAAS,EAAE,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClG,OAAO,CAAC,IAAI,CAAC,CAgCf"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { assertRelativeExtensionPath } from './extension-path.js';
|
|
3
|
+
/**
|
|
4
|
+
* Import each extension module behind an explicit trust gate, validate its export,
|
|
5
|
+
* then hand it to an engine-provided registration callback.
|
|
6
|
+
*
|
|
7
|
+
* The loader is domain-agnostic: it does not know which registry a module belongs to.
|
|
8
|
+
* It enforces the trust boundary (gate + relative-path guard), validates the
|
|
9
|
+
* default/named-`extension` export shape, and delegates routing to `register`.
|
|
10
|
+
*
|
|
11
|
+
* Security invariants:
|
|
12
|
+
* - No refs → no-op.
|
|
13
|
+
* - Refs present but `allowExtensions !== true` → throws **before** any `import` or
|
|
14
|
+
* `moduleLoader` call, so a declared extension is never silently dropped.
|
|
15
|
+
* - Every ref's authored path is re-validated by {@link assertRelativeExtensionPath}.
|
|
16
|
+
*
|
|
17
|
+
* @throws When extensions are present but `allowExtensions` is not `true`, when a path
|
|
18
|
+
* fails the trust guard, or when a module lacks a valid `name`.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadExtensionModules(refs, options, register) {
|
|
21
|
+
if (refs.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
// Fail closed before importing anything: a declared extension under a disabled gate
|
|
24
|
+
// is a hard error, never a silent drop.
|
|
25
|
+
if (options.allowExtensions !== true) {
|
|
26
|
+
const first = refs[0];
|
|
27
|
+
throw new Error(`"${first.sourceName}" declares ${first.kind} extension "${first.path}", but extensions are disabled — pass allowExtensions: true to load extension modules`);
|
|
28
|
+
}
|
|
29
|
+
for (const ref of refs) {
|
|
30
|
+
if (!isAbsolute(ref.baseDir)) {
|
|
31
|
+
throw new Error(`"${ref.sourceName}" extension baseDir "${ref.baseDir}" must be an absolute directory`);
|
|
32
|
+
}
|
|
33
|
+
// Validate the authored path, then derive the import target from it so the
|
|
34
|
+
// trust guard always governs the module actually imported — the loader never
|
|
35
|
+
// imports a caller-supplied absolute path it did not resolve itself.
|
|
36
|
+
assertRelativeExtensionPath(ref.path, { sourceName: ref.sourceName });
|
|
37
|
+
const absPath = resolve(ref.baseDir, ref.path);
|
|
38
|
+
const moduleExports = await options.moduleLoader(absPath);
|
|
39
|
+
const candidate = moduleExports.default ?? moduleExports.extension;
|
|
40
|
+
if (candidate === null ||
|
|
41
|
+
typeof candidate !== 'object' ||
|
|
42
|
+
typeof candidate.name !== 'string') {
|
|
43
|
+
throw new Error(`"${ref.sourceName}" extension "${ref.path}" must export an object with a string "name"`);
|
|
44
|
+
}
|
|
45
|
+
await register(ref, candidate);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assert that an extension module path is relative and does not escape its
|
|
3
|
+
* declaring directory.
|
|
4
|
+
*
|
|
5
|
+
* Extension declarations are data, and a path that is absolute or escapes via `..`
|
|
6
|
+
* is a trust-boundary violation even when extension loading is explicitly allowed.
|
|
7
|
+
* This is a standalone validator (not a schema refinement) so the loader can enforce
|
|
8
|
+
* it at load time, independent of any engine's config schema — defense in depth.
|
|
9
|
+
*
|
|
10
|
+
* @throws When the path is absolute or contains a `..` traversal segment.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertRelativeExtensionPath(path: string, options?: {
|
|
13
|
+
sourceName?: string;
|
|
14
|
+
}): void;
|
|
15
|
+
//# sourceMappingURL=extension-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-path.d.ts","sourceRoot":"","sources":["../../src/plugin/extension-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CAQrG"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assert that an extension module path is relative and does not escape its
|
|
3
|
+
* declaring directory.
|
|
4
|
+
*
|
|
5
|
+
* Extension declarations are data, and a path that is absolute or escapes via `..`
|
|
6
|
+
* is a trust-boundary violation even when extension loading is explicitly allowed.
|
|
7
|
+
* This is a standalone validator (not a schema refinement) so the loader can enforce
|
|
8
|
+
* it at load time, independent of any engine's config schema — defense in depth.
|
|
9
|
+
*
|
|
10
|
+
* @throws When the path is absolute or contains a `..` traversal segment.
|
|
11
|
+
*/
|
|
12
|
+
export function assertRelativeExtensionPath(path, options = {}) {
|
|
13
|
+
const where = options.sourceName !== undefined ? ` declared by "${options.sourceName}"` : '';
|
|
14
|
+
if (/^([/\\]|[A-Za-z]:[/\\])/.test(path)) {
|
|
15
|
+
throw new Error(`extension path "${path}"${where} must be relative (no absolute paths)`);
|
|
16
|
+
}
|
|
17
|
+
if (path.split(/[/\\]/).includes('..')) {
|
|
18
|
+
throw new Error(`extension path "${path}"${where} must not contain ".." traversal`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC"}
|