@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
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { FileSystem } from './file-system';
|
|
2
|
+
/** A single JSON Schema validation failure — records the JSON pointer path and a human-readable message. */
|
|
1
3
|
export interface JsonSchemaViolation {
|
|
2
4
|
path: string;
|
|
3
5
|
message: string;
|
|
4
6
|
}
|
|
7
|
+
/** Subset of JSON Schema 2020-12 keywords used for runtime configuration validation. */
|
|
5
8
|
export interface JsonSchema {
|
|
6
9
|
type?: string | string[];
|
|
7
10
|
required?: string[];
|
|
@@ -15,6 +18,7 @@ export interface JsonSchema {
|
|
|
15
18
|
$ref?: string;
|
|
16
19
|
$defs?: Record<string, JsonSchema>;
|
|
17
20
|
}
|
|
21
|
+
/** Options for loading and validating structured configuration files (YAML/JSON with `$schema`). */
|
|
18
22
|
export interface StructuredConfigLoadOptions {
|
|
19
23
|
validateSchema?: boolean;
|
|
20
24
|
/**
|
|
@@ -30,13 +34,25 @@ export interface StructuredConfigLoadOptions {
|
|
|
30
34
|
* Injectable for testing.
|
|
31
35
|
*/
|
|
32
36
|
resolve?: (specifier: string, from: string) => string;
|
|
37
|
+
/**
|
|
38
|
+
* File system used to read the config and local schema files. Defaults to the
|
|
39
|
+
* deprecated `getFs()` global; supply a {@link FileSystem} from the runtime factory
|
|
40
|
+
* (`createNodeFileSystem()`) to route reads through the factory path or to inject a
|
|
41
|
+
* virtual file system in tests.
|
|
42
|
+
*/
|
|
43
|
+
fileSystem?: Pick<FileSystem, 'readFile'>;
|
|
33
44
|
}
|
|
45
|
+
/** Error thrown when structured config validation fails, carrying the list of {@link JsonSchemaViolation}s. */
|
|
34
46
|
export declare class StructuredConfigSchemaError extends Error {
|
|
35
47
|
readonly violations: readonly JsonSchemaViolation[];
|
|
36
48
|
constructor(message: string, violations?: readonly JsonSchemaViolation[]);
|
|
37
49
|
}
|
|
50
|
+
/** Reads a config file from disk, parses it (YAML or JSON), and validates against its declared `$schema`. */
|
|
38
51
|
export declare function loadStructuredConfig(path: string, options?: StructuredConfigLoadOptions): Promise<unknown>;
|
|
52
|
+
/** Parses a config string (YAML or JSON) and validates against its declared `$schema` if present. */
|
|
39
53
|
export declare function parseStructuredConfig(content: string, source: string, options?: StructuredConfigLoadOptions): Promise<unknown>;
|
|
54
|
+
/** Extracts the `$schema` reference from a parsed config object, resolves and fetches the schema, then validates. */
|
|
40
55
|
export declare function validateDeclaredJsonSchema(value: unknown, source: string, options?: StructuredConfigLoadOptions): Promise<void>;
|
|
56
|
+
/** Validates a value against a {@link JsonSchema}, returning a list of {@link JsonSchemaViolation}s. Does not throw. */
|
|
41
57
|
export declare function validateJsonSchema(value: unknown, schema: JsonSchema, path?: string, defs?: Record<string, JsonSchema>, seenRefs?: ReadonlySet<string>): JsonSchemaViolation[];
|
|
42
58
|
//# sourceMappingURL=schema-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-validation.d.ts","sourceRoot":"","sources":["../src/schema-validation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAUhD,4GAA4G;AAC5G,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACvB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACtC;AAED,oGAAoG;AACpG,MAAM,WAAW,2BAA2B;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtD;;;;;OAKG;IACH,UAAU,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;CAC7C;AAED,+GAA+G;AAC/G,qBAAa,2BAA4B,SAAQ,KAAK;IAG9C,QAAQ,CAAC,UAAU,EAAE,SAAS,mBAAmB,EAAE;gBADnD,OAAO,EAAE,MAAM,EACN,UAAU,GAAE,SAAS,mBAAmB,EAAO;CAK/D;AAED,6GAA6G;AAC7G,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,2BAAgC,GAAG,OAAO,CAAC,OAAO,CAAC,CAGpH;AAED,qGAAqG;AACrG,wBAAsB,qBAAqB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,qHAAqH;AACrH,wBAAsB,0BAA0B,CAC5C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED,wHAAwH;AACxH,wBAAgB,kBAAkB,CAC9B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,SAAK,EACT,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAM,EACrC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GAC1C,mBAAmB,EAAE,CAwDvB"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { parse as parseYaml } from 'yaml';
|
|
2
2
|
import { getFs } from './fs.js';
|
|
3
|
-
import { dirnamePath, isAbsolutePath, joinPath } from './path.js';
|
|
3
|
+
import { dirnamePath, getProcessCwd, isAbsolutePath, joinPath } from './path.js';
|
|
4
4
|
/** Default time budget for a single remote schema fetch. */
|
|
5
5
|
const REMOTE_SCHEMA_FETCH_TIMEOUT_MS = 5_000;
|
|
6
6
|
/** Upper bound on a remote schema body. A timeout alone lets a slow multi-GB drip exhaust memory. */
|
|
7
7
|
const REMOTE_SCHEMA_MAX_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
/** Error thrown when structured config validation fails, carrying the list of {@link JsonSchemaViolation}s. */
|
|
8
9
|
export class StructuredConfigSchemaError extends Error {
|
|
9
10
|
violations;
|
|
10
11
|
constructor(message, violations = []) {
|
|
@@ -13,10 +14,12 @@ export class StructuredConfigSchemaError extends Error {
|
|
|
13
14
|
this.name = 'StructuredConfigSchemaError';
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
/** Reads a config file from disk, parses it (YAML or JSON), and validates against its declared `$schema`. */
|
|
16
18
|
export async function loadStructuredConfig(path, options = {}) {
|
|
17
|
-
const content = await getFs().readFile(path);
|
|
19
|
+
const content = await (options.fileSystem ?? getFs()).readFile(path);
|
|
18
20
|
return await parseStructuredConfig(content, path, options);
|
|
19
21
|
}
|
|
22
|
+
/** Parses a config string (YAML or JSON) and validates against its declared `$schema` if present. */
|
|
20
23
|
export async function parseStructuredConfig(content, source, options = {}) {
|
|
21
24
|
const parsed = source.endsWith('.json') ? JSON.parse(content) : parseYaml(content);
|
|
22
25
|
if (options.validateSchema !== false) {
|
|
@@ -24,6 +27,7 @@ export async function parseStructuredConfig(content, source, options = {}) {
|
|
|
24
27
|
}
|
|
25
28
|
return parsed;
|
|
26
29
|
}
|
|
30
|
+
/** Extracts the `$schema` reference from a parsed config object, resolves and fetches the schema, then validates. */
|
|
27
31
|
export async function validateDeclaredJsonSchema(value, source, options = {}) {
|
|
28
32
|
if (!isObject(value))
|
|
29
33
|
return;
|
|
@@ -49,6 +53,7 @@ export async function validateDeclaredJsonSchema(value, source, options = {}) {
|
|
|
49
53
|
.join('; ')}`, violations);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
56
|
+
/** Validates a value against a {@link JsonSchema}, returning a list of {@link JsonSchemaViolation}s. Does not throw. */
|
|
52
57
|
export function validateJsonSchema(value, schema, path = '', defs = {}, seenRefs = new Set()) {
|
|
53
58
|
const violations = [];
|
|
54
59
|
// Applicator keywords compose with their siblings (logical AND), per JSON Schema 2020-12 —
|
|
@@ -183,7 +188,7 @@ function resolvePackageSchema(specifier, source, resolve) {
|
|
|
183
188
|
if (subpath.length === 0) {
|
|
184
189
|
throw new StructuredConfigSchemaError(`Package schema ref "${specifier}" referenced by "${source}" must include a path within the package`);
|
|
185
190
|
}
|
|
186
|
-
const from = isRemoteRef(source) ?
|
|
191
|
+
const from = isRemoteRef(source) ? getProcessCwd() : dirnamePath(source);
|
|
187
192
|
try {
|
|
188
193
|
// Resolve the package root via its always-present package.json, then join the subpath.
|
|
189
194
|
// This sidesteps `exports` gating on arbitrary JSON subpaths.
|
|
@@ -211,7 +216,7 @@ async function readSchema(schemaLocation, options) {
|
|
|
211
216
|
}
|
|
212
217
|
return await readBoundedBody(response, schemaLocation);
|
|
213
218
|
}
|
|
214
|
-
return await getFs().readFile(schemaLocation);
|
|
219
|
+
return await (options.fileSystem ?? getFs()).readFile(schemaLocation);
|
|
215
220
|
}
|
|
216
221
|
/**
|
|
217
222
|
* Read a response body under a hard byte cap. `Content-Length` is a fast-path reject, but servers
|
package/dist/types.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { Config } from './config';
|
|
2
|
+
/** Identifier for the target runtime platform. */
|
|
2
3
|
export type RuntimeName = 'node-bun' | 'cloudflare-workers' | 'test';
|
|
4
|
+
/** Feature flags describing what a runtime platform supports (filesystem, process execution, persistent storage). */
|
|
3
5
|
export interface RuntimeCapabilities {
|
|
4
6
|
readonly hasFilesystem: boolean;
|
|
5
7
|
readonly hasProcessExecution: boolean;
|
|
6
8
|
readonly hasPersistentStorage: boolean;
|
|
7
9
|
}
|
|
10
|
+
/** Options passed to config loading functions, including overrides and environment variable bindings. */
|
|
8
11
|
export interface LoadConfigOptions {
|
|
9
12
|
overrides?: Partial<Config>;
|
|
10
13
|
envBindings?: Record<string, unknown>;
|
|
11
14
|
}
|
|
15
|
+
/** Minimal distributed tracing context for W3C trace/span propagation. */
|
|
12
16
|
export interface SpanContext {
|
|
13
17
|
traceId: string;
|
|
14
18
|
spanId: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAC1D"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,kDAAkD;AAClD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,MAAM,CAAC;AAErE,qHAAqH;AACrH,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;IACtC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CAC1C;AAED,yGAAyG;AACzG,MAAM,WAAW,iBAAiB;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzC;AAED,0EAA0E;AAC1E,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CAC1D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/ts-runtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "@gobing-ai/ts-runtime — Runtime abstractions for Bun, Node, and Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./bun-sqlite": {
|
|
36
36
|
"types": "./dist/bun-sqlite.d.ts",
|
|
37
37
|
"import": "./dist/bun-sqlite.js"
|
|
38
|
+
},
|
|
39
|
+
"./plugin": {
|
|
40
|
+
"types": "./dist/plugin.d.ts",
|
|
41
|
+
"import": "./dist/plugin.js"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": [
|
|
@@ -54,7 +58,7 @@
|
|
|
54
58
|
"release": "echo 'Manual publish is disabled. Releases go through GitHub Actions via Trusted Publishing — push a tag: git tag @gobing-ai/ts-runtime-v<version> && git push --tags' && exit 1"
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
57
|
-
"@gobing-ai/ts-utils": "^0.3.
|
|
61
|
+
"@gobing-ai/ts-utils": "^0.3.2",
|
|
58
62
|
"execa": "^9.5.0",
|
|
59
63
|
"yaml": "^2.7.0",
|
|
60
64
|
"zod": "^4.1.0"
|
package/src/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { deepMerge } from '@gobing-ai/ts-utils';
|
|
|
2
2
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
3
3
|
import { type ZodIssue, z } from 'zod';
|
|
4
4
|
|
|
5
|
+
/** Zod schema for the application configuration object, providing defaults for app, database, and logging sections. */
|
|
5
6
|
export const configSchema = z.object({
|
|
6
7
|
app: z
|
|
7
8
|
.object({
|
|
@@ -26,6 +27,7 @@ export const configSchema = z.object({
|
|
|
26
27
|
.default({ level: 'info', console: true, file: false, json: false }),
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
/** Inferred TypeScript type of a validated configuration object, derived from {@link configSchema}. */
|
|
29
31
|
export type Config = z.output<typeof configSchema>;
|
|
30
32
|
|
|
31
33
|
const ENV_INTERPOLATION_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
@@ -64,6 +66,7 @@ export function stringifyYamlObject(value: Record<string, unknown>): string {
|
|
|
64
66
|
return stringifyYaml(value);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** Error thrown when configuration validation fails, carrying the Zod validation issues for diagnostics. */
|
|
67
70
|
export class ConfigLoadError extends Error {
|
|
68
71
|
readonly issues: ZodIssue[];
|
|
69
72
|
|
|
@@ -77,27 +80,36 @@ export class ConfigLoadError extends Error {
|
|
|
77
80
|
// These accessors read `process.env` directly and are node-bun only (ADR-008). On
|
|
78
81
|
// `cloudflare-workers` there is no `process`; inject config explicitly rather than calling these.
|
|
79
82
|
|
|
83
|
+
/** Returns the value of `process.env.NODE_ENV`, or `"development"` as default. Node/Bun only. */
|
|
80
84
|
export function getNodeEnv(): string {
|
|
81
85
|
return process.env.NODE_ENV ?? 'development';
|
|
82
86
|
}
|
|
83
|
-
|
|
87
|
+
/** Returns `true` when `NODE_ENV` is `"test"`. Node/Bun only. */
|
|
84
88
|
export function isTestEnv(): boolean {
|
|
85
89
|
return getNodeEnv() === 'test';
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/** Returns `process.env` as a plain object. Node/Bun only. */
|
|
88
93
|
export function getProcessEnv(): Record<string, string | undefined> {
|
|
89
94
|
return process.env;
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/** Returns `process.env.DATABASE_URL`, or `undefined` if not set. Node/Bun only. */
|
|
92
98
|
export function getDatabaseUrl(): string | undefined {
|
|
93
99
|
return process.env.DATABASE_URL;
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/** Returns the user's home directory (`HOME`, falling back to `USERPROFILE` on Windows), or `undefined` if unset. Node/Bun only. */
|
|
103
|
+
export function getHomeDir(): string | undefined {
|
|
104
|
+
return process.env.HOME ?? process.env.USERPROFILE;
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
/** Node-bun only: interpolates `${VAR}` from `process.env` (see note above). */
|
|
97
108
|
export function interpolateEnv(value: string): string {
|
|
98
109
|
return value.replace(ENV_INTERPOLATION_RE, (_match, name: string) => process.env[name] ?? `\${${name}}`);
|
|
99
110
|
}
|
|
100
111
|
|
|
112
|
+
/** Recursively interpolates `${VAR}` environment variables in all string leaves of a nested object or array. Node/Bun only. */
|
|
101
113
|
export function interpolateTree(value: unknown): unknown {
|
|
102
114
|
if (typeof value === 'string') return interpolateEnv(value);
|
|
103
115
|
if (Array.isArray(value)) return value.map(interpolateTree);
|
|
@@ -106,7 +118,7 @@ export function interpolateTree(value: unknown): unknown {
|
|
|
106
118
|
}
|
|
107
119
|
return value;
|
|
108
120
|
}
|
|
109
|
-
|
|
121
|
+
/** Interpolates env vars, merges overrides, validates against {@link configSchema}, and returns a frozen {@link Config}. */
|
|
110
122
|
export function buildConfigFromObject(
|
|
111
123
|
raw: Record<string, unknown>,
|
|
112
124
|
options: { overrides?: Partial<Config> } = {},
|
|
@@ -121,7 +133,7 @@ export function buildConfigFromObject(
|
|
|
121
133
|
}
|
|
122
134
|
return deepFreeze(result.data);
|
|
123
135
|
}
|
|
124
|
-
|
|
136
|
+
/** Parses a YAML configuration string into a raw object, throwing {@link ConfigLoadError} on failure. */
|
|
125
137
|
export function parseConfigYaml(yamlText: string): Record<string, unknown> {
|
|
126
138
|
try {
|
|
127
139
|
return parseYamlObject(yamlText);
|
|
@@ -130,7 +142,7 @@ export function parseConfigYaml(yamlText: string): Record<string, unknown> {
|
|
|
130
142
|
throw new ConfigLoadError(`Config YAML parsing failed: ${(error as Error).message}`);
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
|
-
|
|
145
|
+
/** Parses YAML text and builds a validated {@link Config}, equivalent to `buildConfigFromObject(parseConfigYaml(…))`. */
|
|
134
146
|
export function buildConfigFromYaml(yamlText: string, options: { overrides?: Partial<Config> } = {}): Config {
|
|
135
147
|
return buildConfigFromObject(parseConfigYaml(yamlText), options);
|
|
136
148
|
}
|
package/src/context.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import type { Config } from './config';
|
|
2
2
|
import { buildConfigFromObject } from './config';
|
|
3
|
-
import type { FileSystem } from './
|
|
4
|
-
import {
|
|
3
|
+
import type { FileSystem } from './file-system';
|
|
4
|
+
import { createNodeFileSystem } from './file-system-node';
|
|
5
|
+
import { loadRuntimeFactory } from './platform';
|
|
6
|
+
import type { ProcessExecutor as ProcessExecutorService } from './process-executor';
|
|
7
|
+
import { ProcessExecutor } from './process-executor';
|
|
5
8
|
import type { RuntimeCapabilities, RuntimeName } from './types';
|
|
6
|
-
|
|
9
|
+
/** Execution scope of a runtime context — determines service lifecycle and availability. */
|
|
7
10
|
export type RuntimeScope = 'process' | 'server-request' | 'scheduled-event' | 'test';
|
|
8
11
|
|
|
12
|
+
/** Map of named services available to a runtime context, including the required `config` and `fileSystem`. */
|
|
9
13
|
export interface RuntimeServiceMap {
|
|
10
14
|
config: Config;
|
|
11
15
|
fileSystem: FileSystem;
|
|
16
|
+
processExecutor?: ProcessExecutorService;
|
|
12
17
|
[serviceName: string]: unknown;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
/** Options for constructing a {@link RuntimeContext}. */
|
|
15
21
|
export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
|
|
16
22
|
scope?: RuntimeScope;
|
|
17
23
|
runtimeName?: RuntimeName;
|
|
@@ -19,6 +25,7 @@ export interface RuntimeContextOptions<TServices extends RuntimeServiceMap = Run
|
|
|
19
25
|
services?: Partial<TServices>;
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
/** Injectable service container scoped to a runtime environment (process, request, event, or test). */
|
|
22
29
|
export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap> {
|
|
23
30
|
readonly scope: RuntimeScope;
|
|
24
31
|
readonly runtimeName: RuntimeName;
|
|
@@ -37,7 +44,13 @@ export class RuntimeContext<TServices extends RuntimeServiceMap = RuntimeService
|
|
|
37
44
|
} satisfies RuntimeCapabilities);
|
|
38
45
|
|
|
39
46
|
this.register('config', (options.services?.config ?? buildConfigFromObject({})) as TServices['config']);
|
|
40
|
-
this.register(
|
|
47
|
+
this.register(
|
|
48
|
+
'fileSystem',
|
|
49
|
+
(options.services?.fileSystem ?? createNodeFileSystem()) as TServices['fileSystem'],
|
|
50
|
+
);
|
|
51
|
+
if (this.capabilities.hasProcessExecution && options.services?.processExecutor === undefined) {
|
|
52
|
+
this.register('processExecutor', new ProcessExecutor() as TServices['processExecutor']);
|
|
53
|
+
}
|
|
41
54
|
|
|
42
55
|
for (const [key, value] of Object.entries(options.services ?? {})) {
|
|
43
56
|
if (value !== undefined) {
|
|
@@ -88,6 +101,47 @@ function isDisposable(value: unknown): value is { dispose(): void | Promise<void
|
|
|
88
101
|
return typeof value === 'object' && value !== null && 'dispose' in value && typeof value.dispose === 'function';
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Create a {@link RuntimeContext} wired to the auto-detected runtime factory.
|
|
106
|
+
*
|
|
107
|
+
* Loads the platform-specific factory via {@link loadRuntimeFactory},
|
|
108
|
+
* auto-wires FileSystem, ProcessExecutor, and Config, then returns a
|
|
109
|
+
* ready-to-use context. Call this at the entry point of any app or worker.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const ctx = await createRuntimeContextFromFactory();
|
|
114
|
+
* const fs = ctx.require('fileSystem');
|
|
115
|
+
* const config = ctx.require('config');
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export async function createRuntimeContextFromFactory<TServices extends RuntimeServiceMap = RuntimeServiceMap>(
|
|
119
|
+
options?: RuntimeContextOptions<TServices>,
|
|
120
|
+
): Promise<RuntimeContext<TServices>> {
|
|
121
|
+
const factory = await loadRuntimeFactory();
|
|
122
|
+
const config = await factory.loadConfig();
|
|
123
|
+
const fileSystem = factory.createFileSystem();
|
|
124
|
+
const processExecutor = factory.capabilities.hasProcessExecution ? factory.createProcessExecutor() : undefined;
|
|
125
|
+
|
|
126
|
+
return new RuntimeContext<TServices>({
|
|
127
|
+
scope: 'process',
|
|
128
|
+
runtimeName: factory.runtimeName,
|
|
129
|
+
capabilities: factory.capabilities,
|
|
130
|
+
services: {
|
|
131
|
+
config,
|
|
132
|
+
fileSystem,
|
|
133
|
+
...(processExecutor !== undefined ? { processExecutor } : {}),
|
|
134
|
+
...options?.services,
|
|
135
|
+
},
|
|
136
|
+
} as RuntimeContextOptions<TServices>);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @deprecated Use {@link createRuntimeContextFromFactory} instead —
|
|
141
|
+
* it auto-detects the platform and wires the correct services from the factory.
|
|
142
|
+
* This function is kept for backward compatibility with synchronous callers
|
|
143
|
+
* that manually configure services.
|
|
144
|
+
*/
|
|
91
145
|
export function createRuntimeContext<TServices extends RuntimeServiceMap = RuntimeServiceMap>(
|
|
92
146
|
options: RuntimeContextOptions<TServices> = {},
|
|
93
147
|
): RuntimeContext<TServices> {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers {@link FileSystem} stub.
|
|
3
|
+
*
|
|
4
|
+
* CF Workers have only an ephemeral, per-request virtual filesystem.
|
|
5
|
+
* Persistent file operations are not available. This stub throws clear
|
|
6
|
+
* errors directing developers to use D1, KV, or R2 instead.
|
|
7
|
+
*
|
|
8
|
+
* Non-mutating operations (`resolve`, `getProjectRoot`) return
|
|
9
|
+
* Worker-appropriate values without throwing.
|
|
10
|
+
*
|
|
11
|
+
* Note: Cloudflare Workers with `nodejs_compat` + `compatibility_date >= 2025-09-01`
|
|
12
|
+
* DO expose `node:fs` as an ephemeral, per-request virtual filesystem.
|
|
13
|
+
* We deliberately do NOT use it — the ephemeral nature means files written
|
|
14
|
+
* in one request silently vanish in the next. Throwing with guidance toward
|
|
15
|
+
* D1/KV/R2 is better DX than silent data loss.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { FileSystem } from './file-system';
|
|
19
|
+
|
|
20
|
+
const UNSUPPORTED = 'FileSystem is not available on Cloudflare Workers. Use D1, KV, or R2 for persistent storage.';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a Cloudflare Workers {@link FileSystem} stub.
|
|
24
|
+
*
|
|
25
|
+
* `resolve()` and `getProjectRoot()` work as path utilities.
|
|
26
|
+
* All other methods throw with guidance toward CF-native storage.
|
|
27
|
+
*/
|
|
28
|
+
export function createCfFileSystem(): FileSystem {
|
|
29
|
+
return {
|
|
30
|
+
getProjectRoot: () => '/bundle',
|
|
31
|
+
|
|
32
|
+
resolve: (...segments: string[]) => {
|
|
33
|
+
const joined = segments.join('/').replaceAll(/\/+/g, '/');
|
|
34
|
+
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
exists: (_path: string) => false,
|
|
38
|
+
|
|
39
|
+
readFile: (_path: string): never => {
|
|
40
|
+
throw new Error(`FileSystem.readFile: ${UNSUPPORTED}`);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
writeFile: (_path: string, _content: string): never => {
|
|
44
|
+
throw new Error(`FileSystem.writeFile: ${UNSUPPORTED}`);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
appendFile: (_path: string, _content: string): never => {
|
|
48
|
+
throw new Error(`FileSystem.appendFile: ${UNSUPPORTED}`);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
ensureDir: (_path: string): void => {
|
|
52
|
+
// No-op: CF Workers virtual filesystem handles directory
|
|
53
|
+
// creation internally when files are written.
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
readDir: (_path: string): never => {
|
|
57
|
+
throw new Error(`FileSystem.readDir: ${UNSUPPORTED}`);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
deleteFile: (_path: string): never => {
|
|
61
|
+
throw new Error(`FileSystem.deleteFile: ${UNSUPPORTED}`);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
createWriteStream: (_path: string): never => {
|
|
65
|
+
throw new Error(`FileSystem.createWriteStream: ${UNSUPPORTED}`);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
copy: (_src: string, _dest: string): never => {
|
|
69
|
+
throw new Error(`FileSystem.copy: ${UNSUPPORTED}`);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
stat: (_path: string) => null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `node:fs`-backed {@link FileSystem} implementation for Bun/Node.js.
|
|
3
|
+
*
|
|
4
|
+
* This is the production implementation for local development, VPS, and
|
|
5
|
+
* any environment with a real filesystem. Tests should inject a virtual
|
|
6
|
+
* file system.
|
|
7
|
+
*
|
|
8
|
+
* The implementation uses `node:fs` sync APIs by default. Bun polyfills
|
|
9
|
+
* `node:fs` fully, so this works on both runtimes without a Bun-specific
|
|
10
|
+
* variant.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
appendFileSync,
|
|
15
|
+
cpSync,
|
|
16
|
+
createWriteStream,
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
25
|
+
import { dirname, resolve as resolvePath } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import type { FileSystem } from './file-system';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a {@link FileSystem} backed by `node:fs`.
|
|
31
|
+
*
|
|
32
|
+
* @param root - Project root directory (default: walks up from `process.cwd()` looking for `bun.lock` or `package.json`).
|
|
33
|
+
*/
|
|
34
|
+
export function createNodeFileSystem(root?: string): FileSystem {
|
|
35
|
+
const projectRoot = root ?? findProjectRoot(process.cwd());
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
getProjectRoot: () => projectRoot,
|
|
39
|
+
|
|
40
|
+
resolve: (...segments: string[]) => resolvePath(projectRoot, ...segments),
|
|
41
|
+
|
|
42
|
+
exists: (path: string) => existsSync(path),
|
|
43
|
+
|
|
44
|
+
readFile: (path: string) => readFileSync(path, 'utf-8'),
|
|
45
|
+
|
|
46
|
+
writeFile: (path: string, content: string) => {
|
|
47
|
+
ensureParentDir(path);
|
|
48
|
+
writeFileSync(path, content, 'utf-8');
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
appendFile: (path: string, content: string) => {
|
|
52
|
+
ensureParentDir(path);
|
|
53
|
+
appendFileSync(path, content, 'utf-8');
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
ensureDir: (path: string) => {
|
|
57
|
+
mkdirSync(path, { recursive: true });
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
readDir: (path: string) => readdirSync(path),
|
|
61
|
+
|
|
62
|
+
deleteFile: (path: string) => {
|
|
63
|
+
rmSync(path, { recursive: true, force: true });
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
createWriteStream: (path: string) => {
|
|
67
|
+
ensureParentDir(path);
|
|
68
|
+
return createWriteStream(path, { flags: 'a' });
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
copy: (src: string, dest: string) => {
|
|
72
|
+
cpSync(src, dest, { recursive: true });
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
stat: (path: string) => {
|
|
76
|
+
try {
|
|
77
|
+
const s = statSync(path);
|
|
78
|
+
return {
|
|
79
|
+
isFile: () => s.isFile(),
|
|
80
|
+
isDirectory: () => s.isDirectory(),
|
|
81
|
+
size: s.size,
|
|
82
|
+
mtimeMs: s.mtimeMs,
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function ensureParentDir(filePath: string): void {
|
|
94
|
+
const dir = dirname(filePath);
|
|
95
|
+
if (!existsSync(dir)) {
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Find the project root by walking up from `startDir` looking for a `bun.lock`
|
|
102
|
+
* or `package.json` marker. Uses `existsSync`, so it works on both Node and Bun.
|
|
103
|
+
*
|
|
104
|
+
* This is the single project-root discovery implementation; the deprecated
|
|
105
|
+
* {@link import('./fs').getProjectRoot} delegates here.
|
|
106
|
+
*
|
|
107
|
+
* @internal — exported for reuse by config loading.
|
|
108
|
+
*/
|
|
109
|
+
export function findProjectRoot(startDir: string): string {
|
|
110
|
+
let dir = resolvePath(startDir);
|
|
111
|
+
const root = resolvePath('/');
|
|
112
|
+
while (dir !== root) {
|
|
113
|
+
if (existsSync(resolvePath(dir, 'bun.lock')) || existsSync(resolvePath(dir, 'package.json'))) {
|
|
114
|
+
return dir;
|
|
115
|
+
}
|
|
116
|
+
const parent = resolvePath(dir, '..');
|
|
117
|
+
if (parent === dir) break;
|
|
118
|
+
dir = parent;
|
|
119
|
+
}
|
|
120
|
+
// Fallback: return the directory we started from.
|
|
121
|
+
return startDir;
|
|
122
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Portable file stat subset. */
|
|
2
|
+
export interface FileStat {
|
|
3
|
+
isFile(): boolean;
|
|
4
|
+
isDirectory(): boolean;
|
|
5
|
+
size: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runtime-agnostic file system abstraction.
|
|
11
|
+
*
|
|
12
|
+
* Bun/Node backend uses `node:fs`. Cloudflare Workers backend provides stubs
|
|
13
|
+
* that throw clear "not available" errors, directing developers to use D1,
|
|
14
|
+
* KV, or R2 for persistent storage.
|
|
15
|
+
*
|
|
16
|
+
* All code outside `packages/runtime/src/file-system*.ts` MUST use this
|
|
17
|
+
* interface — never import `node:fs` or call `Bun.write`/`Bun.file` directly.
|
|
18
|
+
*/
|
|
19
|
+
export interface FileSystem {
|
|
20
|
+
/** Check whether a path exists. */
|
|
21
|
+
exists(path: string): boolean | Promise<boolean>;
|
|
22
|
+
|
|
23
|
+
/** Read file contents as UTF-8 string. Throws if not found. */
|
|
24
|
+
readFile(path: string): string | Promise<string>;
|
|
25
|
+
|
|
26
|
+
/** Write file contents, creating parent directories as needed. */
|
|
27
|
+
writeFile(path: string, content: string): void | Promise<void>;
|
|
28
|
+
|
|
29
|
+
/** Append content to a file, creating it if it doesn't exist. */
|
|
30
|
+
appendFile(path: string, content: string): void | Promise<void>;
|
|
31
|
+
|
|
32
|
+
/** Ensure a directory exists, creating it (and parents) recursively if needed. */
|
|
33
|
+
ensureDir(path: string): void | Promise<void>;
|
|
34
|
+
|
|
35
|
+
/** List directory entries (names only, not full paths). */
|
|
36
|
+
readDir(path: string): string[] | Promise<string[]>;
|
|
37
|
+
|
|
38
|
+
/** Delete a file or directory recursively. */
|
|
39
|
+
deleteFile(path: string): void | Promise<void>;
|
|
40
|
+
|
|
41
|
+
/** Recursively copy a file or directory. */
|
|
42
|
+
copy(src: string, dest: string): void | Promise<void>;
|
|
43
|
+
|
|
44
|
+
/** Get file or directory stats. Returns `null` if the path doesn't exist. */
|
|
45
|
+
stat(path: string): FileStat | null | Promise<FileStat | null>;
|
|
46
|
+
|
|
47
|
+
/** Create a writable stream for append-only output (Node/Bun only). Throws on CF Workers. */
|
|
48
|
+
createWriteStream(path: string): { write(chunk: string): void; end(): void };
|
|
49
|
+
|
|
50
|
+
/** Resolve path segments relative to the project root. */
|
|
51
|
+
resolve(...segments: string[]): string;
|
|
52
|
+
|
|
53
|
+
/** Get the project root directory path. */
|
|
54
|
+
getProjectRoot(): string;
|
|
55
|
+
}
|