@forgeax/engine-pack 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +429 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts +2 -0
- package/dist/__tests__/ai-recovery.unit.test.d.ts.map +1 -0
- package/dist/__tests__/artifact-path.test.d.ts +2 -0
- package/dist/__tests__/artifact-path.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts +2 -0
- package/dist/__tests__/asset-evidence-schema.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +2 -0
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +1 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +2 -0
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/cli.unit.test.d.ts +2 -0
- package/dist/__tests__/cli.unit.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-collision.unit.test.d.ts.map +1 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/load-asset-config.test.d.ts +2 -0
- package/dist/__tests__/load-asset-config.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts +2 -0
- package/dist/__tests__/meta-schema-optional.test.d.ts.map +1 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts +2 -0
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +1 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts +2 -0
- package/dist/__tests__/native-cooker-registry.test.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +2 -0
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts +2 -0
- package/dist/__tests__/offline-evidence.integration.test.d.ts.map +1 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +1 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts +2 -0
- package/dist/__tests__/pack-v2-schema.test.d.ts.map +1 -0
- package/dist/__tests__/pack.unit.test.d.ts +2 -0
- package/dist/__tests__/pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts +2 -0
- package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts +2 -0
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-source.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-projection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime.browser.test.d.ts +2 -0
- package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-inventory.test.d.ts +2 -0
- package/dist/__tests__/scanner-inventory.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts +2 -0
- package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts +2 -0
- package/dist/__tests__/topology-actions.unit.test.d.ts.map +1 -0
- package/dist/__tests__/topology.unit.test.d.ts +2 -0
- package/dist/__tests__/topology.unit.test.d.ts.map +1 -0
- package/dist/artifact-path.d.ts +9 -0
- package/dist/artifact-path.d.ts.map +1 -0
- package/dist/artifact-path.mjs +53 -0
- package/dist/artifact-path.mjs.map +1 -0
- package/dist/atlas/run-atlas.d.ts +8 -0
- package/dist/atlas/run-atlas.d.ts.map +1 -0
- package/dist/atlas/shelf-pack.d.ts +55 -0
- package/dist/atlas/shelf-pack.d.ts.map +1 -0
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.mjs +3 -0
- package/dist/bridge.mjs.map +1 -0
- package/dist/build.d.ts +19 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.mjs +10203 -0
- package/dist/build.mjs.map +1 -0
- package/dist/builtin.d.ts +44 -0
- package/dist/builtin.d.ts.map +1 -0
- package/dist/builtin.mjs +141 -0
- package/dist/builtin.mjs.map +1 -0
- package/dist/catalog-builder.d.ts +43 -0
- package/dist/catalog-builder.d.ts.map +1 -0
- package/dist/catalog-delta.d.ts +11 -0
- package/dist/catalog-delta.d.ts.map +1 -0
- package/dist/catalog-projection.d.ts +91 -0
- package/dist/catalog-projection.d.ts.map +1 -0
- package/dist/cli-asset.d.ts +22 -0
- package/dist/cli-asset.d.ts.map +1 -0
- package/dist/cli-asset.mjs +9697 -0
- package/dist/cli-asset.mjs.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.mjs +29 -0
- package/dist/config.mjs.map +1 -0
- package/dist/deriveAssetName.d.ts +15 -0
- package/dist/deriveAssetName.d.ts.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.mjs +19 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/evidence/material-cook.d.ts +92 -0
- package/dist/evidence/material-cook.d.ts.map +1 -0
- package/dist/evidence/offline-evidence.d.ts +25 -0
- package/dist/evidence/offline-evidence.d.ts.map +1 -0
- package/dist/evidence/source-inventory.d.ts +29 -0
- package/dist/evidence/source-inventory.d.ts.map +1 -0
- package/dist/guid.d.ts +45 -0
- package/dist/guid.d.ts.map +1 -0
- package/dist/guid.mjs +94 -0
- package/dist/guid.mjs.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +8612 -0
- package/dist/index.mjs.map +1 -0
- package/dist/material/artifact-writer.d.ts +14 -0
- package/dist/material/artifact-writer.d.ts.map +1 -0
- package/dist/material-cook.d.ts +2 -0
- package/dist/material-cook.d.ts.map +1 -0
- package/dist/material-cook.mjs +248 -0
- package/dist/material-cook.mjs.map +1 -0
- package/dist/mesh-bin-contract.d.ts +36 -0
- package/dist/mesh-bin-contract.d.ts.map +1 -0
- package/dist/mesh-bin-contract.mjs +96 -0
- package/dist/mesh-bin-contract.mjs.map +1 -0
- package/dist/name.mjs +22 -0
- package/dist/name.mjs.map +1 -0
- package/dist/native-cooker-registry.d.ts +47 -0
- package/dist/native-cooker-registry.d.ts.map +1 -0
- package/dist/native-cooker.d.ts +3 -0
- package/dist/native-cooker.d.ts.map +1 -0
- package/dist/native-cooker.mjs +142 -0
- package/dist/native-cooker.mjs.map +1 -0
- package/dist/package-finalizer.d.ts +121 -0
- package/dist/package-finalizer.d.ts.map +1 -0
- package/dist/producer-contract.d.ts +19 -0
- package/dist/producer-contract.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts +4 -0
- package/dist/resolve-asset-source.d.ts.map +1 -0
- package/dist/resolve-asset-source.mjs +109 -0
- package/dist/resolve-asset-source.mjs.map +1 -0
- package/dist/runtime-projection.d.ts +14 -0
- package/dist/runtime-projection.d.ts.map +1 -0
- package/dist/runtime-publication.d.ts +44 -0
- package/dist/runtime-publication.d.ts.map +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.mjs +7261 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/scanner.d.ts +113 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.mjs +8819 -0
- package/dist/scanner.mjs.map +1 -0
- package/dist/schema-compiled.d.ts +12 -0
- package/dist/schema-compiled.d.ts.map +1 -0
- package/dist/schema.mjs +7369 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/scriptable-pack-node.d.ts +62 -0
- package/dist/scriptable-pack-node.d.ts.map +1 -0
- package/dist/scriptable-pack-node.mjs +1674 -0
- package/dist/scriptable-pack-node.mjs.map +1 -0
- package/dist/scriptable-pack-worker.d.ts +2 -0
- package/dist/scriptable-pack-worker.d.ts.map +1 -0
- package/dist/scriptable-pack-worker.mjs +224 -0
- package/dist/scriptable-pack-worker.mjs.map +1 -0
- package/dist/scriptable-pack.d.ts +558 -0
- package/dist/scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack.mjs +672 -0
- package/dist/scriptable-pack.mjs.map +1 -0
- package/dist/topology.d.ts +10 -0
- package/dist/topology.d.ts.map +1 -0
- package/package.json +154 -0
- package/schema/meta.schema.json +173 -0
- package/schema/pack.schema.json +232 -0
- package/src/__tests__/ai-recovery.unit.test.ts +114 -0
- package/src/__tests__/artifact-path.test.ts +41 -0
- package/src/__tests__/asset-evidence-error-hints.test.ts +46 -0
- package/src/__tests__/asset-evidence-lifecycle.test.ts +104 -0
- package/src/__tests__/asset-evidence-schema.test.ts +86 -0
- package/src/__tests__/atlas-cli-region-mismatch.test.ts +134 -0
- package/src/__tests__/atlas-error-code-owner.test-d.ts +23 -0
- package/src/__tests__/cli-asset-evidence.integration.test.ts +172 -0
- package/src/__tests__/cli.unit.test.ts +448 -0
- package/src/__tests__/evidence-source-inventory.unit.test.ts +37 -0
- package/src/__tests__/guid-collision.unit.test.ts +112 -0
- package/src/__tests__/guid-validator-surface.unit.test.ts +20 -0
- package/src/__tests__/load-asset-config.test.ts +121 -0
- package/src/__tests__/material-cook-dependencies.unit.test.ts +112 -0
- package/src/__tests__/material-cook-receipt.unit.test.ts +148 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +152 -0
- package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +22 -0
- package/src/__tests__/mesh-bin-contract.unit.test.ts +32 -0
- package/src/__tests__/meta-schema-optional.test.ts +116 -0
- package/src/__tests__/meta-source-overrides.unit.test.ts +71 -0
- package/src/__tests__/native-cooker-registry.test.ts +64 -0
- package/src/__tests__/offline-evidence-artifact-verification-owner.test-d.ts +64 -0
- package/src/__tests__/offline-evidence.integration.test.ts +106 -0
- package/src/__tests__/owner-chain.integration.test.ts +78 -0
- package/src/__tests__/pack-v2-fixture-migration.test.ts +87 -0
- package/src/__tests__/pack-v2-schema.test.ts +142 -0
- package/src/__tests__/pack.unit.test.ts +2218 -0
- package/src/__tests__/package-finalizer.contract.test.ts +105 -0
- package/src/__tests__/producer-contract-schema.unit.test.ts +92 -0
- package/src/__tests__/resolve-asset-source.test.ts +121 -0
- package/src/__tests__/runtime-projection.unit.test.ts +87 -0
- package/src/__tests__/runtime.browser.test.ts +15 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
- package/src/__tests__/scanner-inventory.test.ts +76 -0
- package/src/__tests__/scanner-mount-cycle.test.ts +232 -0
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +47 -0
- package/src/__tests__/scriptable-pack-ddc-key.unit.test.ts +63 -0
- package/src/__tests__/scriptable-pack.test-d.ts +131 -0
- package/src/__tests__/scriptable-pack.unit.test.ts +789 -0
- package/src/__tests__/topology-actions.unit.test.ts +94 -0
- package/src/__tests__/topology.unit.test.ts +85 -0
- package/src/artifact-path.ts +69 -0
- package/src/atlas/run-atlas.ts +358 -0
- package/src/atlas/shelf-pack.ts +172 -0
- package/src/atlas/upng-ambient.d.ts +13 -0
- package/src/bridge.ts +2 -0
- package/src/build.ts +139 -0
- package/src/builtin.ts +117 -0
- package/src/catalog-builder.ts +313 -0
- package/src/catalog-delta.ts +189 -0
- package/src/catalog-projection.ts +344 -0
- package/src/cli-asset.ts +586 -0
- package/src/config.ts +36 -0
- package/src/deriveAssetName.ts +42 -0
- package/src/errors.ts +47 -0
- package/src/evidence/material-cook.ts +411 -0
- package/src/evidence/offline-evidence.ts +90 -0
- package/src/evidence/source-inventory.ts +193 -0
- package/src/guid.ts +99 -0
- package/src/index.ts +108 -0
- package/src/material/artifact-writer.ts +26 -0
- package/src/material-cook.ts +15 -0
- package/src/mesh-bin-contract.ts +150 -0
- package/src/native-cooker-registry.ts +235 -0
- package/src/native-cooker.ts +2 -0
- package/src/package-finalizer.ts +394 -0
- package/src/producer-contract.ts +277 -0
- package/src/resolve-asset-source.ts +88 -0
- package/src/runtime-projection.ts +100 -0
- package/src/runtime-publication.ts +190 -0
- package/src/runtime.ts +23 -0
- package/src/scanner.ts +715 -0
- package/src/schema/material-cook.schema.json +119 -0
- package/src/schema-compiled.ts +238 -0
- package/src/scriptable-pack-node.ts +1394 -0
- package/src/scriptable-pack-worker.ts +308 -0
- package/src/scriptable-pack.ts +1006 -0
- package/src/topology.ts +157 -0
|
@@ -0,0 +1,1394 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { Worker } from 'node:worker_threads';
|
|
8
|
+
import type { AssetGuid, Result } from '@forgeax/engine-types';
|
|
9
|
+
import { AssetError, err, ImportError, ok } from '@forgeax/engine-types';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
import {
|
|
12
|
+
type ProducerSemanticIdentityInput,
|
|
13
|
+
producerRelativeDdcKey,
|
|
14
|
+
} from './evidence/source-inventory.js';
|
|
15
|
+
import { AssetGuid as AssetGuidCodec } from './guid.js';
|
|
16
|
+
import {
|
|
17
|
+
type AssetReader,
|
|
18
|
+
projectScriptablePackMeta,
|
|
19
|
+
type ScriptablePackAssetKind,
|
|
20
|
+
type ScriptablePackAuthoringMutation,
|
|
21
|
+
type ScriptablePackAuthoringPort,
|
|
22
|
+
type ScriptablePackDefinition,
|
|
23
|
+
type ScriptablePackError,
|
|
24
|
+
type ScriptablePackReadError,
|
|
25
|
+
type ScriptablePackSourceClosureEntry,
|
|
26
|
+
validateScriptablePackDefinition,
|
|
27
|
+
} from './scriptable-pack.js';
|
|
28
|
+
|
|
29
|
+
const IMPORT_META_RESOLVE_FLAG = '--experimental-import-meta-resolve';
|
|
30
|
+
|
|
31
|
+
function scriptablePackWorkerExecArgv(): string[] {
|
|
32
|
+
// import.meta.resolve(parentURL) is the canonical ESM resolver, but Node
|
|
33
|
+
// keeps its parentURL overload behind this flag. Filter --input-type as
|
|
34
|
+
// well: stdin/eval hosts may pass it through process.execArgv, and Node
|
|
35
|
+
// rejects that option inside a file-backed worker.
|
|
36
|
+
const inherited = process.execArgv.filter((arg) => !arg.startsWith('--input-type'));
|
|
37
|
+
return inherited.includes(IMPORT_META_RESOLVE_FLAG)
|
|
38
|
+
? inherited
|
|
39
|
+
: [...inherited, IMPORT_META_RESOLVE_FLAG];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ScriptablePackModuleExecutor {
|
|
43
|
+
load(sourcePath: string): Promise<unknown>;
|
|
44
|
+
dispose?(reason: 'complete' | 'timeout' | 'failure'): void | Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ScriptablePackModuleExecutorPool {
|
|
48
|
+
acquire(): Promise<ScriptablePackModuleExecutor>;
|
|
49
|
+
dispose(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ScriptablePackModuleExecutorPoolOptions {
|
|
53
|
+
readonly maxWorkers?: number;
|
|
54
|
+
readonly maxTasksPerWorker?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function scriptablePackDdcKey(input: ProducerSemanticIdentityInput): string {
|
|
58
|
+
return producerRelativeDdcKey(input);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface OptionalBuildCache<T> {
|
|
62
|
+
readonly read: () => Promise<T | null>;
|
|
63
|
+
readonly coldCook: () => Promise<T>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build CAS is a performance hint; source cold cook remains the correctness path. */
|
|
67
|
+
export async function readOptionalBuildCache<T>(cache: OptionalBuildCache<T>): Promise<{
|
|
68
|
+
readonly value: T;
|
|
69
|
+
readonly fromCache: boolean;
|
|
70
|
+
}> {
|
|
71
|
+
try {
|
|
72
|
+
const cached = await cache.read();
|
|
73
|
+
if (cached !== null) return { value: cached, fromCache: true };
|
|
74
|
+
} catch {
|
|
75
|
+
// Cache deletion, read-only storage, and corrupt optional objects fail open.
|
|
76
|
+
}
|
|
77
|
+
return { value: await cache.coldCook(), fromCache: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LoadScriptablePackOptions {
|
|
81
|
+
/** Maximum time allowed for module initialization in the isolated worker. */
|
|
82
|
+
readonly timeoutMs?: number;
|
|
83
|
+
/** Maximum time allowed for one definition.build(reader) in the isolated worker. */
|
|
84
|
+
readonly buildTimeoutMs?: number;
|
|
85
|
+
readonly executor?: ScriptablePackModuleExecutor;
|
|
86
|
+
/** Release the isolated loader after identity projection when build will not be called. */
|
|
87
|
+
readonly metadataOnly?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function resolveRelativeImport(
|
|
91
|
+
sourcePath: string,
|
|
92
|
+
specifier: string,
|
|
93
|
+
): Promise<string | undefined> {
|
|
94
|
+
if (!specifier.startsWith('.')) return undefined;
|
|
95
|
+
const raw = resolve(dirname(sourcePath), specifier);
|
|
96
|
+
const candidates =
|
|
97
|
+
extname(raw).length > 0
|
|
98
|
+
? [raw]
|
|
99
|
+
: [
|
|
100
|
+
raw,
|
|
101
|
+
`${raw}.ts`,
|
|
102
|
+
`${raw}.tsx`,
|
|
103
|
+
`${raw}.mts`,
|
|
104
|
+
`${raw}.js`,
|
|
105
|
+
`${raw}.mjs`,
|
|
106
|
+
`${raw}.json`,
|
|
107
|
+
resolve(raw, 'index.ts'),
|
|
108
|
+
];
|
|
109
|
+
for (const candidate of candidates) {
|
|
110
|
+
try {
|
|
111
|
+
if ((await stat(candidate)).isFile()) return candidate;
|
|
112
|
+
} catch {
|
|
113
|
+
// Continue through deterministic extension candidates.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Capture one immutable ScriptablePack module closure for inventory and production. */
|
|
120
|
+
export async function inventoryScriptablePackSource(
|
|
121
|
+
sourcePath: string,
|
|
122
|
+
initialSourceText?: string,
|
|
123
|
+
): Promise<readonly ScriptablePackSourceClosureEntry[]> {
|
|
124
|
+
const root = await realpath(sourcePath);
|
|
125
|
+
const pending = [root];
|
|
126
|
+
const seen = new Set<string>();
|
|
127
|
+
const entries: ScriptablePackSourceClosureEntry[] = [];
|
|
128
|
+
while (pending.length > 0) {
|
|
129
|
+
const path = pending.pop();
|
|
130
|
+
if (path === undefined || seen.has(path)) continue;
|
|
131
|
+
seen.add(path);
|
|
132
|
+
const bytes =
|
|
133
|
+
path === root && initialSourceText !== undefined
|
|
134
|
+
? new TextEncoder().encode(initialSourceText)
|
|
135
|
+
: await readFile(path);
|
|
136
|
+
entries.push({ path, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` });
|
|
137
|
+
const source = new TextDecoder().decode(bytes);
|
|
138
|
+
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
|
|
139
|
+
const resolved = await resolveRelativeImport(path, imported.fileName);
|
|
140
|
+
if (resolved !== undefined) pending.push(await realpath(resolved));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface StructuredFailure {
|
|
147
|
+
readonly name?: unknown;
|
|
148
|
+
readonly code?: unknown;
|
|
149
|
+
readonly expected?: unknown;
|
|
150
|
+
readonly actual?: unknown;
|
|
151
|
+
readonly hint?: unknown;
|
|
152
|
+
readonly detail?: unknown;
|
|
153
|
+
readonly message?: unknown;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function serializeFailure(value: unknown): StructuredFailure {
|
|
157
|
+
if (value !== null && typeof value === 'object') {
|
|
158
|
+
const failure = value as Record<string, unknown>;
|
|
159
|
+
return {
|
|
160
|
+
name: failure.name,
|
|
161
|
+
code: failure.code,
|
|
162
|
+
expected: failure.expected,
|
|
163
|
+
actual: failure.actual,
|
|
164
|
+
hint: failure.hint,
|
|
165
|
+
detail: failure.detail,
|
|
166
|
+
message: failure.message,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { message: String(value) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hydrateFailure(value: unknown): unknown {
|
|
173
|
+
if (value === null || typeof value !== 'object') return value;
|
|
174
|
+
const failure = value as StructuredFailure;
|
|
175
|
+
if (
|
|
176
|
+
failure.name === 'AssetError' &&
|
|
177
|
+
typeof failure.code === 'string' &&
|
|
178
|
+
typeof failure.expected === 'string' &&
|
|
179
|
+
typeof failure.hint === 'string'
|
|
180
|
+
) {
|
|
181
|
+
const args = {
|
|
182
|
+
code: failure.code as ConstructorParameters<typeof AssetError>[0]['code'],
|
|
183
|
+
expected: failure.expected,
|
|
184
|
+
hint: failure.hint,
|
|
185
|
+
};
|
|
186
|
+
return failure.detail === undefined
|
|
187
|
+
? new AssetError(args)
|
|
188
|
+
: new AssetError({
|
|
189
|
+
...args,
|
|
190
|
+
detail: failure.detail as NonNullable<
|
|
191
|
+
ConstructorParameters<typeof AssetError>[0]['detail']
|
|
192
|
+
>,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (
|
|
196
|
+
failure.name === 'ImportError' &&
|
|
197
|
+
typeof failure.code === 'string' &&
|
|
198
|
+
typeof failure.expected === 'string' &&
|
|
199
|
+
typeof failure.hint === 'string' &&
|
|
200
|
+
failure.detail !== undefined
|
|
201
|
+
) {
|
|
202
|
+
return new ImportError({
|
|
203
|
+
code: failure.code as ConstructorParameters<typeof ImportError>[0]['code'],
|
|
204
|
+
expected: failure.expected,
|
|
205
|
+
hint: failure.hint,
|
|
206
|
+
detail: failure.detail as ConstructorParameters<typeof ImportError>[0]['detail'],
|
|
207
|
+
...(typeof failure.actual === 'string' ? { actual: failure.actual } : {}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (typeof failure.message === 'string') {
|
|
211
|
+
const error = new Error(failure.message);
|
|
212
|
+
if (typeof failure.name === 'string') error.name = failure.name;
|
|
213
|
+
return error;
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
|
|
219
|
+
private worker: Worker | undefined;
|
|
220
|
+
private compileRoot: string | undefined;
|
|
221
|
+
private nextBuildId = 0;
|
|
222
|
+
private disposal: Promise<void> | undefined;
|
|
223
|
+
private build:
|
|
224
|
+
| {
|
|
225
|
+
readonly id: number;
|
|
226
|
+
readonly reader: AssetReader;
|
|
227
|
+
readonly resolve: (value: unknown) => void;
|
|
228
|
+
readonly reject: (reason: unknown) => void;
|
|
229
|
+
}
|
|
230
|
+
| undefined;
|
|
231
|
+
|
|
232
|
+
async load(sourcePath: string): Promise<unknown> {
|
|
233
|
+
const workerUrl = scriptablePackWorkerUrl();
|
|
234
|
+
const compileRoot = await mkdtemp(resolve(tmpdir(), 'forgeax-scriptable-pack-'));
|
|
235
|
+
this.compileRoot = compileRoot;
|
|
236
|
+
const worker = new Worker(workerUrl, {
|
|
237
|
+
workerData: { sourcePath: resolve(sourcePath), compileRoot },
|
|
238
|
+
execArgv: scriptablePackWorkerExecArgv(),
|
|
239
|
+
resourceLimits: { maxOldGenerationSizeMb: 256, maxYoungGenerationSizeMb: 64 },
|
|
240
|
+
});
|
|
241
|
+
this.worker = worker;
|
|
242
|
+
worker.on('message', (message: unknown) => this.onMessage(message));
|
|
243
|
+
worker.unref();
|
|
244
|
+
return new Promise((resolveLoad, rejectLoad) => {
|
|
245
|
+
const onMessage = (message: unknown): void => {
|
|
246
|
+
if (message === null || typeof message !== 'object') return;
|
|
247
|
+
const value = message as Record<string, unknown>;
|
|
248
|
+
if (value.kind === 'loaded') {
|
|
249
|
+
// The scan keeps this executor alive for the later no-reopen build
|
|
250
|
+
// route, but an idle source worker must not keep a Vite/Vitest host
|
|
251
|
+
// process alive after its own work has settled.
|
|
252
|
+
worker.unref();
|
|
253
|
+
worker.off('message', onMessage);
|
|
254
|
+
resolveLoad({
|
|
255
|
+
default:
|
|
256
|
+
value.definition === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: {
|
|
259
|
+
...(value.definition as Record<string, unknown>),
|
|
260
|
+
build: (reader: AssetReader) => this.runBuild(reader),
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
} else if (value.kind === 'load-threw') {
|
|
264
|
+
worker.off('message', onMessage);
|
|
265
|
+
rejectLoad(hydrateFailure(value.error));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
worker.on('message', onMessage);
|
|
269
|
+
worker.once('error', rejectLoad);
|
|
270
|
+
worker.once('exit', (code) => {
|
|
271
|
+
if (code !== 0) rejectLoad(new Error(`ScriptablePack worker exited with code ${code}`));
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
dispose(reason: 'complete' | 'timeout' | 'failure' = 'complete'): Promise<void> {
|
|
277
|
+
if (this.disposal !== undefined) return this.disposal;
|
|
278
|
+
const worker = this.worker;
|
|
279
|
+
this.worker = undefined;
|
|
280
|
+
const compileRoot = this.compileRoot;
|
|
281
|
+
this.compileRoot = undefined;
|
|
282
|
+
const active = this.build;
|
|
283
|
+
this.build = undefined;
|
|
284
|
+
if (active !== undefined) {
|
|
285
|
+
active.reject(new Error(`ScriptablePack build disposed during ${reason}`));
|
|
286
|
+
}
|
|
287
|
+
this.disposal = (async () => {
|
|
288
|
+
if (worker !== undefined) await worker.terminate();
|
|
289
|
+
if (compileRoot !== undefined) await rm(compileRoot, { recursive: true, force: true });
|
|
290
|
+
})();
|
|
291
|
+
return this.disposal;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private runBuild(reader: AssetReader): Promise<unknown> {
|
|
295
|
+
const worker = this.worker;
|
|
296
|
+
if (worker === undefined) return Promise.reject(new Error('ScriptablePack worker is closed'));
|
|
297
|
+
if (this.build !== undefined)
|
|
298
|
+
return Promise.reject(new Error('ScriptablePack worker already has an active build'));
|
|
299
|
+
const id = this.nextBuildId++;
|
|
300
|
+
return new Promise((resolveBuild, rejectBuild) => {
|
|
301
|
+
this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
|
|
302
|
+
worker.postMessage({ kind: 'build', buildId: id });
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private onMessage(message: unknown): void {
|
|
307
|
+
if (message === null || typeof message !== 'object') return;
|
|
308
|
+
const value = message as Record<string, unknown>;
|
|
309
|
+
const active = this.build;
|
|
310
|
+
if (active === undefined || value.buildId !== active.id) return;
|
|
311
|
+
if (value.kind === 'asset-read' && typeof value.readId === 'number') {
|
|
312
|
+
void active.reader
|
|
313
|
+
.readByGuid(value.guid as AssetGuid)
|
|
314
|
+
.then((result) =>
|
|
315
|
+
this.worker?.postMessage({
|
|
316
|
+
kind: 'asset-result',
|
|
317
|
+
readId: value.readId,
|
|
318
|
+
result:
|
|
319
|
+
result.ok === true ? result : { ...result, error: serializeFailure(result.error) },
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
.catch((error: unknown) => {
|
|
323
|
+
if (this.build?.id !== active.id) return;
|
|
324
|
+
this.build = undefined;
|
|
325
|
+
active.reject(error);
|
|
326
|
+
void this.dispose('failure');
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.build = undefined;
|
|
331
|
+
if (value.kind === 'build-result') active.resolve(value.result);
|
|
332
|
+
else if (value.kind === 'build-threw') active.reject(hydrateFailure(value.error));
|
|
333
|
+
else this.build = active;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function scriptablePackWorkerUrl(): URL {
|
|
338
|
+
const bundledWorker = new URL('./scriptable-pack-worker.mjs', import.meta.url);
|
|
339
|
+
return existsSync(fileURLToPath(bundledWorker))
|
|
340
|
+
? bundledWorker
|
|
341
|
+
: new URL('../dist/scriptable-pack-worker.mjs', import.meta.url);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
|
|
345
|
+
private readonly worker: Worker;
|
|
346
|
+
private readonly compileRootReady = mkdtemp(resolve(tmpdir(), 'forgeax-scriptable-pack-pool-'));
|
|
347
|
+
private compileRoot: string | undefined;
|
|
348
|
+
private nextLoadId = 0;
|
|
349
|
+
private nextBuildId = 0;
|
|
350
|
+
private pendingLoad:
|
|
351
|
+
| {
|
|
352
|
+
readonly id: number;
|
|
353
|
+
readonly resolve: (value: unknown) => void;
|
|
354
|
+
readonly reject: (reason: unknown) => void;
|
|
355
|
+
}
|
|
356
|
+
| undefined;
|
|
357
|
+
private build:
|
|
358
|
+
| {
|
|
359
|
+
readonly id: number;
|
|
360
|
+
readonly reader: AssetReader;
|
|
361
|
+
readonly resolve: (value: unknown) => void;
|
|
362
|
+
readonly reject: (reason: unknown) => void;
|
|
363
|
+
}
|
|
364
|
+
| undefined;
|
|
365
|
+
private disposal: Promise<void> | undefined;
|
|
366
|
+
private forceDispose = false;
|
|
367
|
+
private failed = false;
|
|
368
|
+
private taskCount = 0;
|
|
369
|
+
private released = false;
|
|
370
|
+
|
|
371
|
+
constructor(
|
|
372
|
+
private readonly maxTasksPerWorker: number,
|
|
373
|
+
private readonly release: (
|
|
374
|
+
executor: ReusableWorkerScriptablePackExecutor,
|
|
375
|
+
reusable: boolean,
|
|
376
|
+
) => void,
|
|
377
|
+
) {
|
|
378
|
+
this.worker = new Worker(scriptablePackWorkerUrl(), {
|
|
379
|
+
workerData: { mode: 'reusable' },
|
|
380
|
+
execArgv: scriptablePackWorkerExecArgv(),
|
|
381
|
+
resourceLimits: { maxOldGenerationSizeMb: 256, maxYoungGenerationSizeMb: 64 },
|
|
382
|
+
});
|
|
383
|
+
this.worker.on('message', (message: unknown) => this.onMessage(message));
|
|
384
|
+
this.worker.on('error', (error: Error) => {
|
|
385
|
+
this.failed = true;
|
|
386
|
+
this.rejectActive(error);
|
|
387
|
+
void this.dispose('failure');
|
|
388
|
+
});
|
|
389
|
+
this.worker.on('exit', (code) => {
|
|
390
|
+
if (code === 0 || this.disposal !== undefined) return;
|
|
391
|
+
this.failed = true;
|
|
392
|
+
this.rejectActive(new Error(`ScriptablePack worker exited with code ${code}`));
|
|
393
|
+
void this.dispose('failure');
|
|
394
|
+
});
|
|
395
|
+
// Listener registration can retain the worker's MessagePort in Node.
|
|
396
|
+
// A reusable worker must not keep a completed Vite build alive while its
|
|
397
|
+
// pool is draining; it still processes messages while the host is live.
|
|
398
|
+
this.worker.unref();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async load(sourcePath: string): Promise<unknown> {
|
|
402
|
+
const activeDisposal = this.disposal;
|
|
403
|
+
if (activeDisposal !== undefined) {
|
|
404
|
+
await activeDisposal;
|
|
405
|
+
if (this.failed) {
|
|
406
|
+
return Promise.reject(new Error('ScriptablePack executor is not leased'));
|
|
407
|
+
}
|
|
408
|
+
this.disposal = undefined;
|
|
409
|
+
}
|
|
410
|
+
this.released = false;
|
|
411
|
+
if (this.pendingLoad !== undefined || this.build !== undefined) {
|
|
412
|
+
return Promise.reject(new Error('ScriptablePack executor already has an active task'));
|
|
413
|
+
}
|
|
414
|
+
const compileRoot = this.compileRoot ?? (await this.compileRootReady);
|
|
415
|
+
if (this.disposal !== undefined) {
|
|
416
|
+
const disposal = this.disposal;
|
|
417
|
+
await disposal;
|
|
418
|
+
if (this.failed) {
|
|
419
|
+
return Promise.reject(new Error('ScriptablePack executor is not leased'));
|
|
420
|
+
}
|
|
421
|
+
this.disposal = undefined;
|
|
422
|
+
}
|
|
423
|
+
const loadId = this.nextLoadId++;
|
|
424
|
+
this.compileRoot = compileRoot;
|
|
425
|
+
this.taskCount += 1;
|
|
426
|
+
return new Promise((resolveLoad, rejectLoad) => {
|
|
427
|
+
this.pendingLoad = { id: loadId, resolve: resolveLoad, reject: rejectLoad };
|
|
428
|
+
try {
|
|
429
|
+
this.worker.postMessage({
|
|
430
|
+
kind: 'load',
|
|
431
|
+
loadId,
|
|
432
|
+
sourcePath: resolve(sourcePath),
|
|
433
|
+
compileRoot,
|
|
434
|
+
});
|
|
435
|
+
} catch (error) {
|
|
436
|
+
this.pendingLoad = undefined;
|
|
437
|
+
rejectLoad(error);
|
|
438
|
+
void this.dispose('failure');
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
dispose(reason: 'complete' | 'timeout' | 'failure' = 'complete'): Promise<void> {
|
|
444
|
+
if (reason === 'failure') this.forceDispose = true;
|
|
445
|
+
const activeDisposal = this.disposal;
|
|
446
|
+
if (activeDisposal !== undefined) {
|
|
447
|
+
return activeDisposal;
|
|
448
|
+
}
|
|
449
|
+
this.rejectActive(new Error(`ScriptablePack task disposed during ${reason}`));
|
|
450
|
+
this.disposal = (async () => {
|
|
451
|
+
const compileRoot = this.compileRoot ?? (await this.compileRootReady);
|
|
452
|
+
const reusable =
|
|
453
|
+
reason === 'complete' &&
|
|
454
|
+
!this.failed &&
|
|
455
|
+
!this.forceDispose &&
|
|
456
|
+
this.taskCount < this.maxTasksPerWorker;
|
|
457
|
+
this.forceDispose = false;
|
|
458
|
+
if (!reusable) {
|
|
459
|
+
this.compileRoot = undefined;
|
|
460
|
+
await this.worker.terminate();
|
|
461
|
+
await rm(compileRoot, { recursive: true, force: true });
|
|
462
|
+
}
|
|
463
|
+
this.released = true;
|
|
464
|
+
this.disposal = undefined;
|
|
465
|
+
this.release(this, reusable);
|
|
466
|
+
})();
|
|
467
|
+
return this.disposal;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private rejectActive(error: unknown): void {
|
|
471
|
+
const loading = this.pendingLoad;
|
|
472
|
+
this.pendingLoad = undefined;
|
|
473
|
+
loading?.reject(error);
|
|
474
|
+
const building = this.build;
|
|
475
|
+
this.build = undefined;
|
|
476
|
+
building?.reject(error);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private runBuild(reader: AssetReader): Promise<unknown> {
|
|
480
|
+
if (this.released || this.disposal !== undefined) {
|
|
481
|
+
return Promise.reject(new Error('ScriptablePack executor is not leased'));
|
|
482
|
+
}
|
|
483
|
+
if (this.build !== undefined) {
|
|
484
|
+
return Promise.reject(new Error('ScriptablePack worker already has an active build'));
|
|
485
|
+
}
|
|
486
|
+
const id = this.nextBuildId++;
|
|
487
|
+
return new Promise((resolveBuild, rejectBuild) => {
|
|
488
|
+
this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
|
|
489
|
+
try {
|
|
490
|
+
this.worker.postMessage({ kind: 'build', buildId: id });
|
|
491
|
+
} catch (error) {
|
|
492
|
+
this.build = undefined;
|
|
493
|
+
rejectBuild(error);
|
|
494
|
+
void this.dispose('failure');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private onMessage(message: unknown): void {
|
|
500
|
+
if (message === null || typeof message !== 'object') return;
|
|
501
|
+
const value = message as Record<string, unknown>;
|
|
502
|
+
const loading = this.pendingLoad;
|
|
503
|
+
if (loading !== undefined && value.kind === 'loaded' && value.loadId === loading.id) {
|
|
504
|
+
this.pendingLoad = undefined;
|
|
505
|
+
loading.resolve({
|
|
506
|
+
default:
|
|
507
|
+
value.definition === undefined
|
|
508
|
+
? undefined
|
|
509
|
+
: {
|
|
510
|
+
...(value.definition as Record<string, unknown>),
|
|
511
|
+
build: (reader: AssetReader) => this.runBuild(reader),
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (loading !== undefined && value.kind === 'load-threw' && value.loadId === loading.id) {
|
|
517
|
+
this.pendingLoad = undefined;
|
|
518
|
+
loading.reject(hydrateFailure(value.error));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const active = this.build;
|
|
522
|
+
if (active === undefined || value.buildId !== active.id) return;
|
|
523
|
+
if (value.kind === 'asset-read' && typeof value.readId === 'number') {
|
|
524
|
+
void active.reader
|
|
525
|
+
.readByGuid(value.guid as AssetGuid)
|
|
526
|
+
.then((result) =>
|
|
527
|
+
this.worker.postMessage({
|
|
528
|
+
kind: 'asset-result',
|
|
529
|
+
readId: value.readId,
|
|
530
|
+
result:
|
|
531
|
+
result.ok === true ? result : { ...result, error: serializeFailure(result.error) },
|
|
532
|
+
}),
|
|
533
|
+
)
|
|
534
|
+
.catch((error: unknown) => {
|
|
535
|
+
if (this.build?.id !== active.id) return;
|
|
536
|
+
this.build = undefined;
|
|
537
|
+
active.reject(error);
|
|
538
|
+
void this.dispose('failure');
|
|
539
|
+
});
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
this.build = undefined;
|
|
543
|
+
if (value.kind === 'build-result') active.resolve(value.result);
|
|
544
|
+
else if (value.kind === 'build-threw') active.reject(hydrateFailure(value.error));
|
|
545
|
+
else this.build = active;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
class DefaultScriptablePackModuleExecutorPool implements ScriptablePackModuleExecutorPool {
|
|
550
|
+
private readonly maxWorkers: number;
|
|
551
|
+
private readonly maxTasksPerWorker: number;
|
|
552
|
+
private readonly idle: ReusableWorkerScriptablePackExecutor[] = [];
|
|
553
|
+
private readonly waiters: {
|
|
554
|
+
readonly resolve: (executor: ScriptablePackModuleExecutor) => void;
|
|
555
|
+
readonly reject: (reason: unknown) => void;
|
|
556
|
+
}[] = [];
|
|
557
|
+
private readonly executors = new Set<ReusableWorkerScriptablePackExecutor>();
|
|
558
|
+
private workerCount = 0;
|
|
559
|
+
private closed = false;
|
|
560
|
+
|
|
561
|
+
constructor(options: ScriptablePackModuleExecutorPoolOptions = {}) {
|
|
562
|
+
this.maxWorkers = Math.max(1, Math.trunc(options.maxWorkers ?? 2));
|
|
563
|
+
this.maxTasksPerWorker = Math.max(1, Math.trunc(options.maxTasksPerWorker ?? 32));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
acquire(): Promise<ScriptablePackModuleExecutor> {
|
|
567
|
+
if (this.closed) return Promise.reject(new Error('ScriptablePack executor pool is closed'));
|
|
568
|
+
const executor = this.idle.pop();
|
|
569
|
+
if (executor !== undefined) return Promise.resolve(executor);
|
|
570
|
+
if (this.workerCount < this.maxWorkers) {
|
|
571
|
+
this.workerCount += 1;
|
|
572
|
+
return Promise.resolve(this.createExecutor());
|
|
573
|
+
}
|
|
574
|
+
return new Promise((resolveAcquire, rejectAcquire) =>
|
|
575
|
+
this.waiters.push({ resolve: resolveAcquire, reject: rejectAcquire }),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async dispose(): Promise<void> {
|
|
580
|
+
if (this.closed) return;
|
|
581
|
+
this.closed = true;
|
|
582
|
+
const waiters = this.waiters.splice(0);
|
|
583
|
+
for (const waiter of waiters) {
|
|
584
|
+
waiter.reject(new Error('ScriptablePack executor pool is closed'));
|
|
585
|
+
}
|
|
586
|
+
this.idle.splice(0);
|
|
587
|
+
await Promise.all([...this.executors].map((executor) => executor.dispose('failure')));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private createExecutor(): ReusableWorkerScriptablePackExecutor {
|
|
591
|
+
const executor = new ReusableWorkerScriptablePackExecutor(
|
|
592
|
+
this.maxTasksPerWorker,
|
|
593
|
+
(executor, reusable) => {
|
|
594
|
+
if (this.closed) {
|
|
595
|
+
this.executors.delete(executor);
|
|
596
|
+
if (!reusable) this.workerCount -= 1;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (!reusable) {
|
|
600
|
+
this.executors.delete(executor);
|
|
601
|
+
this.workerCount -= 1;
|
|
602
|
+
} else {
|
|
603
|
+
this.idle.push(executor);
|
|
604
|
+
}
|
|
605
|
+
this.drain();
|
|
606
|
+
},
|
|
607
|
+
);
|
|
608
|
+
this.executors.add(executor);
|
|
609
|
+
return executor;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private drain(): void {
|
|
613
|
+
if (this.closed) return;
|
|
614
|
+
while (this.waiters.length > 0) {
|
|
615
|
+
const idle = this.idle.pop();
|
|
616
|
+
if (idle !== undefined) {
|
|
617
|
+
this.waiters.shift()?.resolve(idle);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (this.workerCount >= this.maxWorkers) return;
|
|
621
|
+
this.workerCount += 1;
|
|
622
|
+
this.waiters.shift()?.resolve(this.createExecutor());
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function createScriptablePackModuleExecutorPool(
|
|
628
|
+
options: ScriptablePackModuleExecutorPoolOptions = {},
|
|
629
|
+
): ScriptablePackModuleExecutorPool {
|
|
630
|
+
return new DefaultScriptablePackModuleExecutorPool(options);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function loadFailure(
|
|
634
|
+
sourcePath: string,
|
|
635
|
+
reason: 'module-load' | 'timeout',
|
|
636
|
+
phase: 'module-load' | 'build',
|
|
637
|
+
diagnostic: string,
|
|
638
|
+
timeoutMs?: number,
|
|
639
|
+
): Result<never, ScriptablePackError> {
|
|
640
|
+
return err({
|
|
641
|
+
code: 'pack-source-load-failed',
|
|
642
|
+
expected: 'a trusted synchronous module with a valid default ScriptablePack export',
|
|
643
|
+
hint: 'repair the module load or initialization failure, then inspect Meta again',
|
|
644
|
+
detail: {
|
|
645
|
+
sourcePath,
|
|
646
|
+
reason,
|
|
647
|
+
phase,
|
|
648
|
+
diagnostic,
|
|
649
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildTimeoutFailure(sourcePath: string, timeoutMs: number): ScriptablePackReadError {
|
|
655
|
+
return {
|
|
656
|
+
code: 'pack-source-load-failed',
|
|
657
|
+
expected: 'a ScriptablePack build to settle within the configured build timeout',
|
|
658
|
+
hint: 'repair the authored build, then retry through a fresh ScriptablePack generation',
|
|
659
|
+
detail: {
|
|
660
|
+
sourcePath,
|
|
661
|
+
reason: 'timeout',
|
|
662
|
+
phase: 'build',
|
|
663
|
+
timeoutMs,
|
|
664
|
+
diagnostic: `ScriptablePack build exceeded ${timeoutMs}ms`,
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
export async function loadScriptablePack(
|
|
670
|
+
sourcePath: string,
|
|
671
|
+
options: LoadScriptablePackOptions = {},
|
|
672
|
+
): Promise<Result<Readonly<ScriptablePackDefinition>, ScriptablePackError>> {
|
|
673
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
674
|
+
const buildTimeoutMs = options.buildTimeoutMs ?? 5_000;
|
|
675
|
+
const executor = options.executor ?? new WorkerScriptablePackExecutor();
|
|
676
|
+
let disposeReason: 'complete' | 'timeout' | 'failure' | undefined;
|
|
677
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
678
|
+
try {
|
|
679
|
+
const timeoutSignal = Symbol('scriptable-pack-module-timeout');
|
|
680
|
+
const loaded = await Promise.race([
|
|
681
|
+
executor.load(sourcePath),
|
|
682
|
+
new Promise<typeof timeoutSignal>((resolve) => {
|
|
683
|
+
timeout = setTimeout(() => resolve(timeoutSignal), timeoutMs);
|
|
684
|
+
}),
|
|
685
|
+
]);
|
|
686
|
+
if (loaded === timeoutSignal) {
|
|
687
|
+
disposeReason = 'timeout';
|
|
688
|
+
return loadFailure(
|
|
689
|
+
sourcePath,
|
|
690
|
+
'timeout',
|
|
691
|
+
'module-load',
|
|
692
|
+
`ScriptablePack module initialization exceeded ${timeoutMs}ms`,
|
|
693
|
+
timeoutMs,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const moduleValue =
|
|
697
|
+
loaded !== null && typeof loaded === 'object' && 'default' in loaded
|
|
698
|
+
? (loaded as { readonly default: unknown }).default
|
|
699
|
+
: undefined;
|
|
700
|
+
const validated = validateScriptablePackDefinition(moduleValue, sourcePath);
|
|
701
|
+
if (!validated.ok) {
|
|
702
|
+
disposeReason = 'failure';
|
|
703
|
+
return validated;
|
|
704
|
+
}
|
|
705
|
+
if (options.metadataOnly === true) {
|
|
706
|
+
disposeReason = 'complete';
|
|
707
|
+
return validated;
|
|
708
|
+
}
|
|
709
|
+
const definition = validated.value;
|
|
710
|
+
type BuildOutcome = Awaited<ReturnType<ScriptablePackDefinition['build']>>;
|
|
711
|
+
return ok({
|
|
712
|
+
...definition,
|
|
713
|
+
build: async (reader: AssetReader): Promise<BuildOutcome> => {
|
|
714
|
+
let buildTimedOut = false;
|
|
715
|
+
let buildTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
716
|
+
const timeoutResult = err(buildTimeoutFailure(sourcePath, buildTimeoutMs));
|
|
717
|
+
try {
|
|
718
|
+
const built = await Promise.race([
|
|
719
|
+
Promise.resolve(definition.build(reader)),
|
|
720
|
+
new Promise<BuildOutcome>((resolve) => {
|
|
721
|
+
buildTimeout = setTimeout(() => {
|
|
722
|
+
buildTimedOut = true;
|
|
723
|
+
resolve(timeoutResult as BuildOutcome);
|
|
724
|
+
}, buildTimeoutMs);
|
|
725
|
+
}),
|
|
726
|
+
]);
|
|
727
|
+
if (buildTimedOut) {
|
|
728
|
+
await executor.dispose?.('timeout');
|
|
729
|
+
return built;
|
|
730
|
+
}
|
|
731
|
+
await executor.dispose?.('complete');
|
|
732
|
+
return built;
|
|
733
|
+
} catch (error) {
|
|
734
|
+
await executor.dispose?.('failure');
|
|
735
|
+
throw error;
|
|
736
|
+
} finally {
|
|
737
|
+
if (buildTimeout !== undefined) clearTimeout(buildTimeout);
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const diagnostic = error instanceof Error ? error.message : String(error);
|
|
743
|
+
disposeReason = 'failure';
|
|
744
|
+
return loadFailure(sourcePath, 'module-load', 'module-load', diagnostic);
|
|
745
|
+
} finally {
|
|
746
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
747
|
+
if (disposeReason !== undefined) await executor.dispose?.(disposeReason);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const CANONICAL_SCAFFOLD_MARKER = '// @forgeax-scriptable-pack canonical-v1';
|
|
752
|
+
const CANONICAL_MANIFEST_NAME = 'canonicalManifest';
|
|
753
|
+
const CANONICAL_MUTABLE_KINDS = new Set<ScriptablePackAssetKind>(['scene', 'mesh', 'material']);
|
|
754
|
+
|
|
755
|
+
interface CanonicalOutput {
|
|
756
|
+
readonly guid: string;
|
|
757
|
+
readonly kind: ScriptablePackAssetKind;
|
|
758
|
+
readonly name?: string;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
interface CanonicalManifest {
|
|
762
|
+
readonly schemaVersion: '1.0.0';
|
|
763
|
+
readonly packageId: string;
|
|
764
|
+
readonly name?: string;
|
|
765
|
+
readonly assets: Readonly<Record<string, CanonicalOutput>>;
|
|
766
|
+
readonly externalAssets: Readonly<Record<string, string>>;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export interface FileSystemScriptablePackAuthoringOptions {
|
|
770
|
+
/** All source and target paths are confined to this game root. */
|
|
771
|
+
readonly gameRoot: string;
|
|
772
|
+
readonly incomingRefs?: (sourcePath: string, sourceKey?: string) => Promise<readonly string[]>;
|
|
773
|
+
readonly rebuild?: (
|
|
774
|
+
sourcePath: string,
|
|
775
|
+
mode: 'rebuild' | 'cold-cook',
|
|
776
|
+
) => Promise<Result<void, ScriptablePackError>>;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function authoringError(
|
|
780
|
+
code:
|
|
781
|
+
| 'pack-source-path-invalid'
|
|
782
|
+
| 'pack-source-revision-conflict'
|
|
783
|
+
| 'pack-source-mutation-unsupported'
|
|
784
|
+
| 'pack-source-reference-conflict'
|
|
785
|
+
| 'pack-source-write-failed',
|
|
786
|
+
input: {
|
|
787
|
+
readonly expected: string;
|
|
788
|
+
readonly actual?: string;
|
|
789
|
+
readonly hint: string;
|
|
790
|
+
readonly retryable: boolean;
|
|
791
|
+
readonly recoveryActions: readonly string[];
|
|
792
|
+
readonly requestId?: string;
|
|
793
|
+
readonly sourcePath?: string;
|
|
794
|
+
readonly sourceKey?: string;
|
|
795
|
+
readonly incomingRefs?: readonly string[];
|
|
796
|
+
},
|
|
797
|
+
): ScriptablePackError {
|
|
798
|
+
return {
|
|
799
|
+
code,
|
|
800
|
+
expected: input.expected,
|
|
801
|
+
...(input.actual === undefined ? {} : { actual: input.actual }),
|
|
802
|
+
hint: input.hint,
|
|
803
|
+
retryable: input.retryable,
|
|
804
|
+
recoveryActions: input.recoveryActions,
|
|
805
|
+
detail: {
|
|
806
|
+
...(input.requestId === undefined ? {} : { requestId: input.requestId }),
|
|
807
|
+
...(input.sourcePath === undefined ? {} : { sourcePath: input.sourcePath }),
|
|
808
|
+
...(input.sourceKey === undefined ? {} : { sourceKey: input.sourceKey }),
|
|
809
|
+
...(input.incomingRefs === undefined ? {} : { incomingRefs: input.incomingRefs }),
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function confinedSourcePath(
|
|
815
|
+
gameRoot: string,
|
|
816
|
+
sourcePath: string,
|
|
817
|
+
): Result<{ readonly absolute: string; readonly relative: string }, ScriptablePackError> {
|
|
818
|
+
const root = resolve(gameRoot);
|
|
819
|
+
const absolute = resolve(root, sourcePath);
|
|
820
|
+
const rel = relative(root, absolute);
|
|
821
|
+
if (
|
|
822
|
+
rel.length === 0 ||
|
|
823
|
+
rel === '..' ||
|
|
824
|
+
rel.startsWith(`..${sep}`) ||
|
|
825
|
+
isAbsolute(sourcePath) ||
|
|
826
|
+
!rel.endsWith('.pack.ts')
|
|
827
|
+
) {
|
|
828
|
+
return err(
|
|
829
|
+
authoringError('pack-source-path-invalid', {
|
|
830
|
+
expected: 'a game-root-relative *.pack.ts path confined to the selected game',
|
|
831
|
+
actual: sourcePath,
|
|
832
|
+
hint: 'choose a relative ScriptablePack source path inside the selected game root',
|
|
833
|
+
retryable: false,
|
|
834
|
+
recoveryActions: ['choose-game-relative-source-path'],
|
|
835
|
+
sourcePath,
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
return ok({ absolute, relative: rel.split(sep).join('/') });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function revisionOf(source: string): string {
|
|
843
|
+
return createHash('sha256').update(source).digest('hex');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
847
|
+
if (
|
|
848
|
+
ts.isAsExpression(expression) ||
|
|
849
|
+
ts.isSatisfiesExpression(expression) ||
|
|
850
|
+
ts.isParenthesizedExpression(expression)
|
|
851
|
+
) {
|
|
852
|
+
return unwrapExpression(expression.expression);
|
|
853
|
+
}
|
|
854
|
+
return expression;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function literalValue(expression: ts.Expression): unknown {
|
|
858
|
+
const value = unwrapExpression(expression);
|
|
859
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return value.text;
|
|
860
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
861
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
862
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
863
|
+
if (ts.isNumericLiteral(value)) return Number(value.text);
|
|
864
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.map(literalValue);
|
|
865
|
+
if (ts.isObjectLiteralExpression(value)) {
|
|
866
|
+
const result: Record<string, unknown> = {};
|
|
867
|
+
for (const property of value.properties) {
|
|
868
|
+
if (!ts.isPropertyAssignment(property))
|
|
869
|
+
throw new TypeError('manifest properties must be data');
|
|
870
|
+
const name = property.name;
|
|
871
|
+
const key =
|
|
872
|
+
ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)
|
|
873
|
+
? name.text
|
|
874
|
+
: undefined;
|
|
875
|
+
if (key === undefined) throw new TypeError('manifest property names must be static');
|
|
876
|
+
result[key] = literalValue(property.initializer);
|
|
877
|
+
}
|
|
878
|
+
return result;
|
|
879
|
+
}
|
|
880
|
+
throw new TypeError('manifest values must be literal data');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function parseCanonicalManifest(source: string, sourcePath: string): CanonicalManifest | undefined {
|
|
884
|
+
if (!source.startsWith(`${CANONICAL_SCAFFOLD_MARKER}\n`)) return undefined;
|
|
885
|
+
const file = ts.createSourceFile(
|
|
886
|
+
sourcePath,
|
|
887
|
+
source,
|
|
888
|
+
ts.ScriptTarget.Latest,
|
|
889
|
+
true,
|
|
890
|
+
ts.ScriptKind.TS,
|
|
891
|
+
);
|
|
892
|
+
for (const statement of file.statements) {
|
|
893
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
894
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
895
|
+
if (
|
|
896
|
+
ts.isIdentifier(declaration.name) &&
|
|
897
|
+
declaration.name.text === CANONICAL_MANIFEST_NAME &&
|
|
898
|
+
declaration.initializer !== undefined
|
|
899
|
+
) {
|
|
900
|
+
const value = literalValue(declaration.initializer);
|
|
901
|
+
if (value === null || typeof value !== 'object') return undefined;
|
|
902
|
+
return value as CanonicalManifest;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function guidExpression(value: string): string {
|
|
910
|
+
return `parseGuid(${JSON.stringify(value)})`;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Render the one source shape that the file authoring port may mutate structurally. */
|
|
914
|
+
export function renderCanonicalScriptablePack(manifest: CanonicalManifest): string {
|
|
915
|
+
const data = JSON.stringify(manifest, null, 2);
|
|
916
|
+
return `${CANONICAL_SCAFFOLD_MARKER}
|
|
917
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
918
|
+
import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
|
|
919
|
+
import type { ScriptablePackDefinition } from '@forgeax/engine-pack/source';
|
|
920
|
+
import type { Asset, AssetGuid as AssetGuidType } from '@forgeax/engine-types';
|
|
921
|
+
import { ok } from '@forgeax/engine-types';
|
|
922
|
+
|
|
923
|
+
const ${CANONICAL_MANIFEST_NAME} = ${data} as const;
|
|
924
|
+
|
|
925
|
+
function parseGuid(value: string): AssetGuidType {
|
|
926
|
+
const parsed = AssetGuid.parse(value);
|
|
927
|
+
if (!parsed.ok) throw parsed.error;
|
|
928
|
+
return parsed.value;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function createOutput(kind: string): Asset {
|
|
932
|
+
switch (kind) {
|
|
933
|
+
case 'scene':
|
|
934
|
+
return { kind: 'scene', entities: [], mounts: [] };
|
|
935
|
+
case 'material':
|
|
936
|
+
return { kind: 'material', values: {} };
|
|
937
|
+
case 'mesh': {
|
|
938
|
+
const vertices = new Float32Array([
|
|
939
|
+
0, 0.7, 0, 0, 0, 1, 0.5, 1, 0, 0, 0, 1,
|
|
940
|
+
-0.7, -0.6, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
|
|
941
|
+
0.7, -0.6, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1,
|
|
942
|
+
]);
|
|
943
|
+
return {
|
|
944
|
+
kind: 'mesh',
|
|
945
|
+
vertices,
|
|
946
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
947
|
+
attributes: { ...buildMeshAttributeMapForUvSets(1), position: vertices },
|
|
948
|
+
aabb: new Float32Array([-0.7, -0.6, 0, 0.7, 0.7, 0]),
|
|
949
|
+
submeshes: [{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 }],
|
|
950
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
default:
|
|
954
|
+
throw new Error(\`canonical ScriptablePack output kind is not mutable: \${kind}\`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const assets = Object.fromEntries(
|
|
959
|
+
Object.entries(${CANONICAL_MANIFEST_NAME}.assets).map(([sourceKey, asset]) => [
|
|
960
|
+
sourceKey,
|
|
961
|
+
{ ...asset, guid: parseGuid(asset.guid) },
|
|
962
|
+
]),
|
|
963
|
+
);
|
|
964
|
+
const externalAssets = Object.fromEntries(
|
|
965
|
+
Object.entries(${CANONICAL_MANIFEST_NAME}.externalAssets).map(([alias, guid]) => [alias, parseGuid(guid)]),
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
export default {
|
|
969
|
+
schemaVersion: '1.0.0',
|
|
970
|
+
packageId: ${guidExpression(manifest.packageId)},
|
|
971
|
+
${manifest.name === undefined ? '' : `name: ${JSON.stringify(manifest.name)},\n `}assets,
|
|
972
|
+
externalAssets,
|
|
973
|
+
build: () => ok(Object.fromEntries(Object.entries(assets).map(([sourceKey, asset]) => [sourceKey, createOutput(asset.kind)])) as Record<string, Asset>),
|
|
974
|
+
} satisfies ScriptablePackDefinition;
|
|
975
|
+
`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function definitionFromCanonical(
|
|
979
|
+
manifest: CanonicalManifest,
|
|
980
|
+
): Result<ScriptablePackDefinition, ScriptablePackError> {
|
|
981
|
+
const packageId = AssetGuidCodec.parse(manifest.packageId);
|
|
982
|
+
if (!packageId.ok) return invalidCanonicalGuid('$.packageId', manifest.packageId);
|
|
983
|
+
const assets: Record<string, { guid: AssetGuid; kind: ScriptablePackAssetKind; name?: string }> =
|
|
984
|
+
{};
|
|
985
|
+
for (const [sourceKey, output] of Object.entries(manifest.assets)) {
|
|
986
|
+
const guid = AssetGuidCodec.parse(output.guid);
|
|
987
|
+
if (!guid.ok)
|
|
988
|
+
return invalidCanonicalGuid(`$.assets[${JSON.stringify(sourceKey)}].guid`, output.guid);
|
|
989
|
+
assets[sourceKey] = {
|
|
990
|
+
guid: guid.value,
|
|
991
|
+
kind: output.kind,
|
|
992
|
+
...(output.name === undefined ? {} : { name: output.name }),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const externalAssets: Record<string, AssetGuid> = {};
|
|
996
|
+
for (const [alias, value] of Object.entries(manifest.externalAssets)) {
|
|
997
|
+
const guid = AssetGuidCodec.parse(value);
|
|
998
|
+
if (!guid.ok) return invalidCanonicalGuid(`$.externalAssets[${JSON.stringify(alias)}]`, value);
|
|
999
|
+
externalAssets[alias] = guid.value;
|
|
1000
|
+
}
|
|
1001
|
+
return validateScriptablePackDefinition({
|
|
1002
|
+
schemaVersion: '1.0.0',
|
|
1003
|
+
packageId: packageId.value,
|
|
1004
|
+
...(manifest.name === undefined ? {} : { name: manifest.name }),
|
|
1005
|
+
assets,
|
|
1006
|
+
externalAssets,
|
|
1007
|
+
build: () => ok({} as Record<string, never>),
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function invalidCanonicalGuid(
|
|
1012
|
+
propertyPath: string,
|
|
1013
|
+
actual: string,
|
|
1014
|
+
): Result<never, ScriptablePackError> {
|
|
1015
|
+
return err({
|
|
1016
|
+
code: 'pack-source-definition-invalid',
|
|
1017
|
+
expected: 'a canonical UUID string',
|
|
1018
|
+
hint: 'repair the canonical scaffold manifest GUID and inspect Meta again',
|
|
1019
|
+
detail: { propertyPath, actual },
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function invalidCanonicalSource(
|
|
1024
|
+
sourcePath: string,
|
|
1025
|
+
error: unknown,
|
|
1026
|
+
): Result<never, ScriptablePackError> {
|
|
1027
|
+
return err({
|
|
1028
|
+
code: 'pack-source-definition-invalid',
|
|
1029
|
+
expected: 'a literal canonical-v1 manifest with the required package/assets/external fields',
|
|
1030
|
+
hint: 'repair the canonical scaffold or open it as custom source without structured mutation',
|
|
1031
|
+
detail: {
|
|
1032
|
+
sourcePath,
|
|
1033
|
+
propertyPath: '$.canonicalManifest',
|
|
1034
|
+
actual: error instanceof Error ? error.message : String(error),
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async function atomicWrite(path: string, source: string): Promise<void> {
|
|
1040
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1041
|
+
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1042
|
+
try {
|
|
1043
|
+
await writeFile(temporary, source, { encoding: 'utf8', flag: 'wx' });
|
|
1044
|
+
await rename(temporary, path);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
await rm(temporary, { force: true });
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
|
|
1052
|
+
return [...left].sort().join('\0') === [...right].sort().join('\0');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Node filesystem adapter for the public ScriptablePack authoring gateway. */
|
|
1056
|
+
export function createFileSystemScriptablePackAuthoringPort(
|
|
1057
|
+
options: FileSystemScriptablePackAuthoringOptions,
|
|
1058
|
+
): ScriptablePackAuthoringPort {
|
|
1059
|
+
const resolvePath = (sourcePath: string) => confinedSourcePath(options.gameRoot, sourcePath);
|
|
1060
|
+
|
|
1061
|
+
async function readSource(sourcePath: string) {
|
|
1062
|
+
const path = resolvePath(sourcePath);
|
|
1063
|
+
if (!path.ok) return path;
|
|
1064
|
+
try {
|
|
1065
|
+
const source = await readFile(path.value.absolute, 'utf8');
|
|
1066
|
+
return ok({ ...path.value, source, revision: revisionOf(source) });
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
return err(
|
|
1069
|
+
authoringError('pack-source-write-failed', {
|
|
1070
|
+
expected: 'a readable ScriptablePack source file',
|
|
1071
|
+
actual: error instanceof Error ? error.message : String(error),
|
|
1072
|
+
hint: 'restore the source file or create it through the ScriptablePack gateway',
|
|
1073
|
+
retryable: true,
|
|
1074
|
+
recoveryActions: ['inspect-source-path', 'retry'],
|
|
1075
|
+
sourcePath,
|
|
1076
|
+
}),
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
async function inspect(sourcePath: string) {
|
|
1082
|
+
const read = await readSource(sourcePath);
|
|
1083
|
+
if (!read.ok) return read;
|
|
1084
|
+
let canonical: CanonicalManifest | undefined;
|
|
1085
|
+
try {
|
|
1086
|
+
canonical = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
return invalidCanonicalSource(read.value.relative, error);
|
|
1089
|
+
}
|
|
1090
|
+
const loaded =
|
|
1091
|
+
canonical === undefined
|
|
1092
|
+
? await loadScriptablePack(read.value.absolute, { metadataOnly: true })
|
|
1093
|
+
: definitionFromCanonical(canonical);
|
|
1094
|
+
if (!loaded.ok) return loaded;
|
|
1095
|
+
return ok({
|
|
1096
|
+
revision: read.value.revision,
|
|
1097
|
+
meta: projectScriptablePackMeta(loaded.value, read.value.relative),
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async function preflight(sourcePath: string) {
|
|
1102
|
+
const read = await readSource(sourcePath);
|
|
1103
|
+
if (!read.ok) return read;
|
|
1104
|
+
const inspected = await inspect(sourcePath);
|
|
1105
|
+
if (!inspected.ok) return inspected;
|
|
1106
|
+
let canonical: CanonicalManifest | undefined;
|
|
1107
|
+
try {
|
|
1108
|
+
canonical = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
return invalidCanonicalSource(read.value.relative, error);
|
|
1111
|
+
}
|
|
1112
|
+
const incomingRefs = (await options.incomingRefs?.(read.value.relative)) ?? [];
|
|
1113
|
+
return ok({
|
|
1114
|
+
revision: inspected.value.revision,
|
|
1115
|
+
meta: inspected.value.meta,
|
|
1116
|
+
capabilities:
|
|
1117
|
+
canonical === undefined
|
|
1118
|
+
? ({
|
|
1119
|
+
inspect: true,
|
|
1120
|
+
rebuild: true,
|
|
1121
|
+
coldCook: true,
|
|
1122
|
+
reason: 'structured mutation is available only for canonical-v1 scaffolds',
|
|
1123
|
+
} as const)
|
|
1124
|
+
: ({
|
|
1125
|
+
inspect: true,
|
|
1126
|
+
rebuild: true,
|
|
1127
|
+
coldCook: true,
|
|
1128
|
+
addOutput: true,
|
|
1129
|
+
addExternalAsset: true,
|
|
1130
|
+
renameDisplay: true,
|
|
1131
|
+
removeOutput: true,
|
|
1132
|
+
clone: true,
|
|
1133
|
+
} as const),
|
|
1134
|
+
incomingRefs,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function mutate(operation: ScriptablePackAuthoringMutation) {
|
|
1139
|
+
const targetPath =
|
|
1140
|
+
operation.kind === 'clone-scriptable-pack' ? operation.targetPath : operation.sourcePath;
|
|
1141
|
+
const target = resolvePath(targetPath);
|
|
1142
|
+
if (!target.ok) return target;
|
|
1143
|
+
|
|
1144
|
+
let manifest: CanonicalManifest;
|
|
1145
|
+
let currentRevision: string | undefined;
|
|
1146
|
+
if (operation.kind === 'create-scriptable-pack') {
|
|
1147
|
+
if (!CANONICAL_MUTABLE_KINDS.has(operation.initialOutput.kind)) {
|
|
1148
|
+
return err(
|
|
1149
|
+
authoringError('pack-source-mutation-unsupported', {
|
|
1150
|
+
expected: 'a canonical scene, mesh, or material initial output',
|
|
1151
|
+
actual: operation.initialOutput.kind,
|
|
1152
|
+
hint: 'create one of the canonical output kinds, then hand-author advanced outputs',
|
|
1153
|
+
retryable: false,
|
|
1154
|
+
recoveryActions: ['choose-supported-output-kind'],
|
|
1155
|
+
requestId: operation.requestId,
|
|
1156
|
+
sourcePath: operation.sourcePath,
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
try {
|
|
1161
|
+
await stat(target.value.absolute);
|
|
1162
|
+
return err(
|
|
1163
|
+
authoringError('pack-source-revision-conflict', {
|
|
1164
|
+
expected: 'an unused target source path',
|
|
1165
|
+
actual: 'source already exists',
|
|
1166
|
+
hint: 'inspect the existing source or choose a new path',
|
|
1167
|
+
retryable: false,
|
|
1168
|
+
recoveryActions: ['asset.preflight', 'choose-new-source-path'],
|
|
1169
|
+
requestId: operation.requestId,
|
|
1170
|
+
sourcePath: operation.sourcePath,
|
|
1171
|
+
}),
|
|
1172
|
+
);
|
|
1173
|
+
} catch {
|
|
1174
|
+
// Missing is the required create precondition.
|
|
1175
|
+
}
|
|
1176
|
+
manifest = {
|
|
1177
|
+
schemaVersion: '1.0.0',
|
|
1178
|
+
packageId: AssetGuidCodec.format(operation.packageId),
|
|
1179
|
+
...(operation.name === undefined ? {} : { name: operation.name }),
|
|
1180
|
+
assets: {
|
|
1181
|
+
[operation.initialOutput.sourceKey]: {
|
|
1182
|
+
guid: AssetGuidCodec.format(operation.initialOutput.guid),
|
|
1183
|
+
kind: operation.initialOutput.kind,
|
|
1184
|
+
...(operation.initialOutput.name === undefined
|
|
1185
|
+
? {}
|
|
1186
|
+
: { name: operation.initialOutput.name }),
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
externalAssets: {},
|
|
1190
|
+
};
|
|
1191
|
+
} else {
|
|
1192
|
+
const read = await readSource(operation.sourcePath);
|
|
1193
|
+
if (!read.ok) return read;
|
|
1194
|
+
currentRevision = read.value.revision;
|
|
1195
|
+
if (
|
|
1196
|
+
operation.expectedRevision !== undefined &&
|
|
1197
|
+
operation.expectedRevision !== currentRevision
|
|
1198
|
+
) {
|
|
1199
|
+
return err(
|
|
1200
|
+
authoringError('pack-source-revision-conflict', {
|
|
1201
|
+
expected: operation.expectedRevision,
|
|
1202
|
+
actual: currentRevision,
|
|
1203
|
+
hint: 'inspect the current source revision, reconcile the edit, then retry with a new requestId',
|
|
1204
|
+
retryable: true,
|
|
1205
|
+
recoveryActions: ['asset.preflight', 'mint-request-id'],
|
|
1206
|
+
requestId: operation.requestId,
|
|
1207
|
+
sourcePath: operation.sourcePath,
|
|
1208
|
+
}),
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
let parsed: CanonicalManifest | undefined;
|
|
1212
|
+
try {
|
|
1213
|
+
parsed = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
return invalidCanonicalSource(read.value.relative, error);
|
|
1216
|
+
}
|
|
1217
|
+
if (parsed === undefined) {
|
|
1218
|
+
return err(
|
|
1219
|
+
authoringError('pack-source-mutation-unsupported', {
|
|
1220
|
+
expected: 'a canonical-v1 scaffold for structured source mutation',
|
|
1221
|
+
actual: 'custom ScriptablePack source',
|
|
1222
|
+
hint: 'clone into a canonical scaffold or edit custom source with a code editor',
|
|
1223
|
+
retryable: false,
|
|
1224
|
+
recoveryActions: ['clone-scriptable-pack', 'open-code-editor'],
|
|
1225
|
+
requestId: operation.requestId,
|
|
1226
|
+
sourcePath: operation.sourcePath,
|
|
1227
|
+
}),
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
manifest = parsed;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (operation.kind === 'clone-scriptable-pack') {
|
|
1234
|
+
try {
|
|
1235
|
+
await stat(target.value.absolute);
|
|
1236
|
+
return err(
|
|
1237
|
+
authoringError('pack-source-revision-conflict', {
|
|
1238
|
+
expected: 'an unused clone target source path',
|
|
1239
|
+
actual: 'source already exists',
|
|
1240
|
+
hint: 'inspect the existing target or choose a new path',
|
|
1241
|
+
retryable: false,
|
|
1242
|
+
recoveryActions: ['asset.preflight', 'choose-new-source-path'],
|
|
1243
|
+
requestId: operation.requestId,
|
|
1244
|
+
sourcePath: target.value.relative,
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
} catch {
|
|
1248
|
+
// Missing is the required clone precondition.
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (operation.kind === 'add-output') {
|
|
1253
|
+
if (!CANONICAL_MUTABLE_KINDS.has(operation.assetKind)) {
|
|
1254
|
+
return err(
|
|
1255
|
+
authoringError('pack-source-mutation-unsupported', {
|
|
1256
|
+
expected: 'a canonical scene, mesh, or material output',
|
|
1257
|
+
actual: operation.assetKind,
|
|
1258
|
+
hint: 'hand-author advanced output kinds in the source module',
|
|
1259
|
+
retryable: false,
|
|
1260
|
+
recoveryActions: ['open-code-editor'],
|
|
1261
|
+
requestId: operation.requestId,
|
|
1262
|
+
sourcePath: operation.sourcePath,
|
|
1263
|
+
sourceKey: operation.sourceKey,
|
|
1264
|
+
}),
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
manifest = {
|
|
1268
|
+
...manifest,
|
|
1269
|
+
assets: {
|
|
1270
|
+
...manifest.assets,
|
|
1271
|
+
[operation.sourceKey]: {
|
|
1272
|
+
guid: AssetGuidCodec.format(operation.guid),
|
|
1273
|
+
kind: operation.assetKind,
|
|
1274
|
+
...(operation.name === undefined ? {} : { name: operation.name }),
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
};
|
|
1278
|
+
} else if (operation.kind === 'add-external-asset') {
|
|
1279
|
+
manifest = {
|
|
1280
|
+
...manifest,
|
|
1281
|
+
externalAssets: {
|
|
1282
|
+
...manifest.externalAssets,
|
|
1283
|
+
[operation.alias]: AssetGuidCodec.format(operation.guid),
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
} else if (operation.kind === 'rename-display') {
|
|
1287
|
+
if (operation.target.kind === 'package') manifest = { ...manifest, name: operation.name };
|
|
1288
|
+
else {
|
|
1289
|
+
const output = manifest.assets[operation.target.sourceKey];
|
|
1290
|
+
if (output === undefined)
|
|
1291
|
+
return err(
|
|
1292
|
+
authoringError('pack-source-mutation-unsupported', {
|
|
1293
|
+
expected: 'an existing sourceKey',
|
|
1294
|
+
actual: operation.target.sourceKey,
|
|
1295
|
+
hint: 'inspect Meta and choose a current output',
|
|
1296
|
+
retryable: true,
|
|
1297
|
+
recoveryActions: ['asset.preflight'],
|
|
1298
|
+
requestId: operation.requestId,
|
|
1299
|
+
sourcePath: operation.sourcePath,
|
|
1300
|
+
sourceKey: operation.target.sourceKey,
|
|
1301
|
+
}),
|
|
1302
|
+
);
|
|
1303
|
+
manifest = {
|
|
1304
|
+
...manifest,
|
|
1305
|
+
assets: {
|
|
1306
|
+
...manifest.assets,
|
|
1307
|
+
[operation.target.sourceKey]: { ...output, name: operation.name },
|
|
1308
|
+
},
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
} else if (operation.kind === 'remove-output') {
|
|
1312
|
+
const incomingRefs =
|
|
1313
|
+
(await options.incomingRefs?.(operation.sourcePath, operation.sourceKey)) ?? [];
|
|
1314
|
+
if (
|
|
1315
|
+
incomingRefs.length > 0 &&
|
|
1316
|
+
!sameStrings(incomingRefs, operation.confirmIncomingRefs ?? [])
|
|
1317
|
+
) {
|
|
1318
|
+
return err(
|
|
1319
|
+
authoringError('pack-source-reference-conflict', {
|
|
1320
|
+
expected: 'explicit acknowledgement of every current incoming reference',
|
|
1321
|
+
actual: `${incomingRefs.length} incoming references`,
|
|
1322
|
+
hint: 'inspect dependents, then confirm the exact incoming reference identities or cancel',
|
|
1323
|
+
retryable: true,
|
|
1324
|
+
recoveryActions: ['inspect-incoming-refs', 'confirm-remove-output'],
|
|
1325
|
+
requestId: operation.requestId,
|
|
1326
|
+
sourcePath: operation.sourcePath,
|
|
1327
|
+
sourceKey: operation.sourceKey,
|
|
1328
|
+
incomingRefs,
|
|
1329
|
+
}),
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const { [operation.sourceKey]: removed, ...assets } = manifest.assets;
|
|
1333
|
+
if (removed === undefined || Object.keys(assets).length === 0) {
|
|
1334
|
+
return err(
|
|
1335
|
+
authoringError('pack-source-mutation-unsupported', {
|
|
1336
|
+
expected: 'an existing output while retaining at least one package output',
|
|
1337
|
+
actual: operation.sourceKey,
|
|
1338
|
+
hint: 'keep one output or remove the entire source file through an explicit file operation',
|
|
1339
|
+
retryable: false,
|
|
1340
|
+
recoveryActions: ['asset.preflight'],
|
|
1341
|
+
requestId: operation.requestId,
|
|
1342
|
+
sourcePath: operation.sourcePath,
|
|
1343
|
+
sourceKey: operation.sourceKey,
|
|
1344
|
+
}),
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
manifest = { ...manifest, assets };
|
|
1348
|
+
} else if (operation.kind === 'clone-scriptable-pack') {
|
|
1349
|
+
manifest = {
|
|
1350
|
+
...manifest,
|
|
1351
|
+
packageId: AssetGuidCodec.format(operation.packageId),
|
|
1352
|
+
assets: Object.fromEntries(
|
|
1353
|
+
Object.entries(manifest.assets).map(([sourceKey, output]) => [
|
|
1354
|
+
sourceKey,
|
|
1355
|
+
{
|
|
1356
|
+
...output,
|
|
1357
|
+
guid: AssetGuidCodec.format(operation.outputGuids[sourceKey] as AssetGuid),
|
|
1358
|
+
},
|
|
1359
|
+
]),
|
|
1360
|
+
),
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const source = renderCanonicalScriptablePack(manifest);
|
|
1365
|
+
try {
|
|
1366
|
+
await atomicWrite(target.value.absolute, source);
|
|
1367
|
+
return ok({ sourcePath: target.value.relative, revision: revisionOf(source) });
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
return err(
|
|
1370
|
+
authoringError('pack-source-write-failed', {
|
|
1371
|
+
expected: 'an atomic source-file replacement',
|
|
1372
|
+
actual: error instanceof Error ? error.message : String(error),
|
|
1373
|
+
hint: 'repair filesystem permissions or disk capacity, then retry with a new requestId',
|
|
1374
|
+
retryable: true,
|
|
1375
|
+
recoveryActions: ['inspect-filesystem', 'mint-request-id'],
|
|
1376
|
+
requestId: operation.requestId,
|
|
1377
|
+
sourcePath: target.value.relative,
|
|
1378
|
+
}),
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return {
|
|
1384
|
+
preflight,
|
|
1385
|
+
mutate,
|
|
1386
|
+
inspect,
|
|
1387
|
+
async rebuild(sourcePath, mode) {
|
|
1388
|
+
const path = resolvePath(sourcePath);
|
|
1389
|
+
if (!path.ok) return path;
|
|
1390
|
+
const rebuilt = (await options.rebuild?.(path.value.relative, mode)) ?? ok(undefined);
|
|
1391
|
+
return rebuilt.ok ? inspect(path.value.relative) : rebuilt;
|
|
1392
|
+
},
|
|
1393
|
+
};
|
|
1394
|
+
}
|