@forgeax/engine-pack 0.1.20 → 0.1.23
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 +159 -46
- package/dist/__tests__/inventory-schema.test.d.ts +2 -0
- package/dist/__tests__/inventory-schema.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
- package/dist/build.d.ts +9 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +3317 -804
- package/dist/build.mjs.map +1 -1
- package/dist/builtin.mjs +37 -4
- package/dist/builtin.mjs.map +1 -1
- package/dist/catalog-builder.d.ts.map +1 -1
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +1368 -850
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/deriveAssetName.d.ts +11 -7
- package/dist/deriveAssetName.d.ts.map +1 -1
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/evidence/source-inventory.d.ts.map +1 -1
- package/dist/guid.d.ts +15 -0
- package/dist/guid.d.ts.map +1 -1
- package/dist/guid.mjs +53 -5
- package/dist/guid.mjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1255 -561
- package/dist/index.mjs.map +1 -1
- package/dist/inventory/binding.d.ts +6 -0
- package/dist/inventory/binding.d.ts.map +1 -0
- package/dist/inventory/declaration.d.ts +24 -0
- package/dist/inventory/declaration.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +3 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/sync.d.ts +5 -0
- package/dist/inventory/sync.d.ts.map +1 -0
- package/dist/material-cook.mjs +3 -1
- package/dist/material-cook.mjs.map +1 -1
- package/dist/name.mjs +4 -7
- package/dist/name.mjs.map +1 -1
- package/dist/package-finalizer.d.ts +2 -20
- package/dist/package-finalizer.d.ts.map +1 -1
- package/dist/parameterized-pack-node.d.ts +26 -0
- package/dist/parameterized-pack-node.d.ts.map +1 -0
- package/dist/parameterized-pack-node.mjs +11093 -0
- package/dist/parameterized-pack-node.mjs.map +1 -0
- package/dist/parameterized-pack.d.ts +233 -0
- package/dist/parameterized-pack.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts +1 -3
- package/dist/resolve-asset-source.d.ts.map +1 -1
- package/dist/resolve-asset-source.mjs +6 -81
- package/dist/resolve-asset-source.mjs.map +1 -1
- package/dist/runtime.mjs +77 -7
- package/dist/runtime.mjs.map +1 -1
- package/dist/scanner.d.ts +19 -8
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +1169 -690
- package/dist/scanner.mjs.map +1 -1
- package/dist/schema-compiled.d.ts.map +1 -1
- package/dist/schema.mjs +79 -7
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +8 -32
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +576 -1021
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +29 -11
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +19 -532
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +1042 -527
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +7 -7
- package/schema/meta.schema.json +1 -1
- package/schema/pack.schema.json +76 -6
- package/src/__tests__/guid-collision.unit.test.ts +18 -9
- package/src/__tests__/inventory-schema.test.ts +109 -0
- package/src/__tests__/pack.unit.test.ts +1 -238
- package/src/__tests__/package-finalizer.contract.test.ts +55 -0
- package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
- package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
- package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
- package/src/__tests__/resolve-asset-source.test.ts +15 -98
- package/src/__tests__/scanner-blacklist.test.ts +55 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
- package/src/__tests__/scanner-inventory.test.ts +24 -12
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
- package/src/__tests__/topology.unit.test.ts +23 -0
- package/src/build.ts +28 -18
- package/src/catalog-builder.ts +61 -34
- package/src/cli-asset.ts +70 -85
- package/src/deriveAssetName.ts +14 -13
- package/src/evidence/material-cook.ts +3 -1
- package/src/evidence/source-inventory.ts +34 -21
- package/src/guid.ts +65 -4
- package/src/index.ts +12 -11
- package/src/inventory/binding.ts +25 -0
- package/src/inventory/declaration.ts +168 -0
- package/src/inventory/index.ts +18 -0
- package/src/inventory/sync.ts +22 -0
- package/src/package-finalizer.ts +2 -42
- package/src/parameterized-pack-node.ts +1968 -0
- package/src/parameterized-pack.ts +1493 -0
- package/src/resolve-asset-source.ts +4 -76
- package/src/scanner.ts +241 -59
- package/src/schema-compiled.ts +2 -0
- package/src/scriptable-pack-node.ts +111 -722
- package/src/scriptable-pack-worker.ts +48 -16
- package/src/scriptable-pack.ts +27 -939
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/load-asset-config.test.d.ts +0 -2
- package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +0 -1
- package/dist/config.mjs +0 -29
- package/dist/config.mjs.map +0 -1
- package/src/__tests__/load-asset-config.test.ts +0 -121
- package/src/__tests__/scriptable-pack.test-d.ts +0 -131
- package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
- package/src/config.ts +0 -36
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtemp, readFile, realpath, rm, stat } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, extname,
|
|
5
|
+
import { dirname, extname, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { Worker } from 'node:worker_threads';
|
|
8
8
|
import type { AssetGuid, Result } from '@forgeax/engine-types';
|
|
@@ -12,19 +12,14 @@ import {
|
|
|
12
12
|
type ProducerSemanticIdentityInput,
|
|
13
13
|
producerRelativeDdcKey,
|
|
14
14
|
} from './evidence/source-inventory.js';
|
|
15
|
-
import { AssetGuid as AssetGuidCodec } from './guid.js';
|
|
16
15
|
import {
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
type
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type ScriptablePackReadError,
|
|
25
|
-
type ScriptablePackSourceClosureEntry,
|
|
26
|
-
validateScriptablePackDefinition,
|
|
27
|
-
} from './scriptable-pack.js';
|
|
16
|
+
type AnyParameterizedPackDefinition,
|
|
17
|
+
type PackAuthoringError,
|
|
18
|
+
type PackBuildContextWithoutParameters,
|
|
19
|
+
type PackBuildResult,
|
|
20
|
+
validatePackDefinition,
|
|
21
|
+
} from './parameterized-pack.js';
|
|
22
|
+
import type { AssetReader, ScriptablePackSourceClosureEntry } from './scriptable-pack.js';
|
|
28
23
|
|
|
29
24
|
const IMPORT_META_RESOLVE_FLAG = '--experimental-import-meta-resolve';
|
|
30
25
|
|
|
@@ -44,6 +39,15 @@ export interface ScriptablePackModuleExecutor {
|
|
|
44
39
|
dispose?(reason: 'complete' | 'timeout' | 'failure'): void | Promise<void>;
|
|
45
40
|
}
|
|
46
41
|
|
|
42
|
+
interface SerializedParameterizedBuildContext {
|
|
43
|
+
readonly packageId: readonly number[];
|
|
44
|
+
readonly values?: Readonly<Record<string, unknown>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface WorkerBackedScriptablePackExecutor {
|
|
48
|
+
readonly supportsParameterizedContext: true;
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
export interface ScriptablePackModuleExecutorPool {
|
|
48
52
|
acquire(): Promise<ScriptablePackModuleExecutor>;
|
|
49
53
|
dispose(): Promise<void>;
|
|
@@ -77,16 +81,6 @@ export async function readOptionalBuildCache<T>(cache: OptionalBuildCache<T>): P
|
|
|
77
81
|
return { value: await cache.coldCook(), fromCache: false };
|
|
78
82
|
}
|
|
79
83
|
|
|
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
84
|
async function resolveRelativeImport(
|
|
91
85
|
sourcePath: string,
|
|
92
86
|
specifier: string,
|
|
@@ -215,7 +209,10 @@ function hydrateFailure(value: unknown): unknown {
|
|
|
215
209
|
return value;
|
|
216
210
|
}
|
|
217
211
|
|
|
218
|
-
class WorkerScriptablePackExecutor
|
|
212
|
+
class WorkerScriptablePackExecutor
|
|
213
|
+
implements ScriptablePackModuleExecutor, WorkerBackedScriptablePackExecutor
|
|
214
|
+
{
|
|
215
|
+
readonly supportsParameterizedContext = true as const;
|
|
219
216
|
private worker: Worker | undefined;
|
|
220
217
|
private compileRoot: string | undefined;
|
|
221
218
|
private nextBuildId = 0;
|
|
@@ -257,7 +254,10 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
|
|
|
257
254
|
? undefined
|
|
258
255
|
: {
|
|
259
256
|
...(value.definition as Record<string, unknown>),
|
|
260
|
-
build: (
|
|
257
|
+
build: (
|
|
258
|
+
readByGuid: AssetReader['readByGuid'],
|
|
259
|
+
context?: SerializedParameterizedBuildContext,
|
|
260
|
+
) => this.runBuild({ readByGuid }, context),
|
|
261
261
|
},
|
|
262
262
|
});
|
|
263
263
|
} else if (value.kind === 'load-threw') {
|
|
@@ -291,7 +291,10 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
|
|
|
291
291
|
return this.disposal;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
private runBuild(
|
|
294
|
+
private runBuild(
|
|
295
|
+
reader: AssetReader,
|
|
296
|
+
context?: SerializedParameterizedBuildContext,
|
|
297
|
+
): Promise<unknown> {
|
|
295
298
|
const worker = this.worker;
|
|
296
299
|
if (worker === undefined) return Promise.reject(new Error('ScriptablePack worker is closed'));
|
|
297
300
|
if (this.build !== undefined)
|
|
@@ -299,7 +302,7 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
|
|
|
299
302
|
const id = this.nextBuildId++;
|
|
300
303
|
return new Promise((resolveBuild, rejectBuild) => {
|
|
301
304
|
this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
|
|
302
|
-
worker.postMessage({ kind: 'build', buildId: id });
|
|
305
|
+
worker.postMessage({ kind: 'build', buildId: id, context });
|
|
303
306
|
});
|
|
304
307
|
}
|
|
305
308
|
|
|
@@ -341,7 +344,10 @@ function scriptablePackWorkerUrl(): URL {
|
|
|
341
344
|
: new URL('../dist/scriptable-pack-worker.mjs', import.meta.url);
|
|
342
345
|
}
|
|
343
346
|
|
|
344
|
-
class ReusableWorkerScriptablePackExecutor
|
|
347
|
+
class ReusableWorkerScriptablePackExecutor
|
|
348
|
+
implements ScriptablePackModuleExecutor, WorkerBackedScriptablePackExecutor
|
|
349
|
+
{
|
|
350
|
+
readonly supportsParameterizedContext = true as const;
|
|
345
351
|
private readonly worker: Worker;
|
|
346
352
|
private readonly compileRootReady = mkdtemp(resolve(tmpdir(), 'forgeax-scriptable-pack-pool-'));
|
|
347
353
|
private compileRoot: string | undefined;
|
|
@@ -476,7 +482,10 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
|
|
|
476
482
|
building?.reject(error);
|
|
477
483
|
}
|
|
478
484
|
|
|
479
|
-
private runBuild(
|
|
485
|
+
private runBuild(
|
|
486
|
+
reader: AssetReader,
|
|
487
|
+
context?: SerializedParameterizedBuildContext,
|
|
488
|
+
): Promise<unknown> {
|
|
480
489
|
if (this.released || this.disposal !== undefined) {
|
|
481
490
|
return Promise.reject(new Error('ScriptablePack executor is not leased'));
|
|
482
491
|
}
|
|
@@ -487,7 +496,7 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
|
|
|
487
496
|
return new Promise((resolveBuild, rejectBuild) => {
|
|
488
497
|
this.build = { id, reader, resolve: resolveBuild, reject: rejectBuild };
|
|
489
498
|
try {
|
|
490
|
-
this.worker.postMessage({ kind: 'build', buildId: id });
|
|
499
|
+
this.worker.postMessage({ kind: 'build', buildId: id, context });
|
|
491
500
|
} catch (error) {
|
|
492
501
|
this.build = undefined;
|
|
493
502
|
rejectBuild(error);
|
|
@@ -508,7 +517,10 @@ class ReusableWorkerScriptablePackExecutor implements ScriptablePackModuleExecut
|
|
|
508
517
|
? undefined
|
|
509
518
|
: {
|
|
510
519
|
...(value.definition as Record<string, unknown>),
|
|
511
|
-
build: (
|
|
520
|
+
build: (
|
|
521
|
+
readByGuid: AssetReader['readByGuid'],
|
|
522
|
+
context?: SerializedParameterizedBuildContext,
|
|
523
|
+
) => this.runBuild({ readByGuid }, context),
|
|
512
524
|
},
|
|
513
525
|
});
|
|
514
526
|
return;
|
|
@@ -630,17 +642,24 @@ export function createScriptablePackModuleExecutorPool(
|
|
|
630
642
|
return new DefaultScriptablePackModuleExecutorPool(options);
|
|
631
643
|
}
|
|
632
644
|
|
|
633
|
-
|
|
645
|
+
export interface LoadParameterizedScriptablePackOptions {
|
|
646
|
+
readonly timeoutMs?: number;
|
|
647
|
+
readonly buildTimeoutMs?: number;
|
|
648
|
+
readonly executor?: ScriptablePackModuleExecutor;
|
|
649
|
+
readonly metadataOnly?: boolean;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function parameterizedLoadFailure(
|
|
634
653
|
sourcePath: string,
|
|
635
654
|
reason: 'module-load' | 'timeout',
|
|
636
655
|
phase: 'module-load' | 'build',
|
|
637
656
|
diagnostic: string,
|
|
638
657
|
timeoutMs?: number,
|
|
639
|
-
):
|
|
640
|
-
return
|
|
641
|
-
code: 'pack-
|
|
642
|
-
expected: 'a trusted
|
|
643
|
-
hint: 'repair the module
|
|
658
|
+
): PackAuthoringError {
|
|
659
|
+
return {
|
|
660
|
+
code: 'pack-parameter-invalid',
|
|
661
|
+
expected: 'a trusted Pack v2 authoring module with a valid default export',
|
|
662
|
+
hint: 'repair the source module or its build timeout, then inspect the Pack again',
|
|
644
663
|
detail: {
|
|
645
664
|
sourcePath,
|
|
646
665
|
reason,
|
|
@@ -648,38 +667,21 @@ function loadFailure(
|
|
|
648
667
|
diagnostic,
|
|
649
668
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
650
669
|
},
|
|
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
670
|
};
|
|
667
671
|
}
|
|
668
672
|
|
|
669
|
-
|
|
673
|
+
/** Load the parameterized Pack source contract in an isolated module executor. */
|
|
674
|
+
export async function loadParameterizedScriptablePack(
|
|
670
675
|
sourcePath: string,
|
|
671
|
-
options:
|
|
672
|
-
): Promise<Result<Readonly<
|
|
673
|
-
// Module initialization includes transpiling the trusted TypeScript source
|
|
674
|
-
// closure in a fresh Worker. A cold SDK workspace can legitimately exceed
|
|
675
|
-
// the build execution budget before any authored build() code has run.
|
|
676
|
+
options: LoadParameterizedScriptablePackOptions = {},
|
|
677
|
+
): Promise<Result<Readonly<AnyParameterizedPackDefinition>, PackAuthoringError>> {
|
|
676
678
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
677
679
|
const buildTimeoutMs = options.buildTimeoutMs ?? 5_000;
|
|
678
680
|
const executor = options.executor ?? new WorkerScriptablePackExecutor();
|
|
679
681
|
let disposeReason: 'complete' | 'timeout' | 'failure' | undefined;
|
|
680
682
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
681
683
|
try {
|
|
682
|
-
const timeoutSignal = Symbol('
|
|
684
|
+
const timeoutSignal = Symbol('parameterized-pack-module-timeout');
|
|
683
685
|
const loaded = await Promise.race([
|
|
684
686
|
executor.load(sourcePath),
|
|
685
687
|
new Promise<typeof timeoutSignal>((resolve) => {
|
|
@@ -688,42 +690,68 @@ export async function loadScriptablePack(
|
|
|
688
690
|
]);
|
|
689
691
|
if (loaded === timeoutSignal) {
|
|
690
692
|
disposeReason = 'timeout';
|
|
691
|
-
return
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
693
|
+
return err(
|
|
694
|
+
parameterizedLoadFailure(
|
|
695
|
+
sourcePath,
|
|
696
|
+
'timeout',
|
|
697
|
+
'module-load',
|
|
698
|
+
`Pack module initialization exceeded ${timeoutMs}ms`,
|
|
699
|
+
timeoutMs,
|
|
700
|
+
),
|
|
697
701
|
);
|
|
698
702
|
}
|
|
699
703
|
const moduleValue =
|
|
700
704
|
loaded !== null && typeof loaded === 'object' && 'default' in loaded
|
|
701
705
|
? (loaded as { readonly default: unknown }).default
|
|
702
706
|
: undefined;
|
|
703
|
-
const validated =
|
|
707
|
+
const validated = validatePackDefinition(moduleValue, sourcePath);
|
|
704
708
|
if (!validated.ok) {
|
|
705
709
|
disposeReason = 'failure';
|
|
706
710
|
return validated;
|
|
707
711
|
}
|
|
708
712
|
if (options.metadataOnly === true) {
|
|
709
713
|
disposeReason = 'complete';
|
|
710
|
-
return validated;
|
|
714
|
+
return ok(validated.value);
|
|
711
715
|
}
|
|
712
716
|
const definition = validated.value;
|
|
713
|
-
|
|
717
|
+
const build = definition.build as unknown as (
|
|
718
|
+
first: unknown,
|
|
719
|
+
context?: SerializedParameterizedBuildContext,
|
|
720
|
+
) => PackBuildResult;
|
|
714
721
|
return ok({
|
|
715
722
|
...definition,
|
|
716
|
-
build: async (
|
|
723
|
+
build: async (
|
|
724
|
+
context: PackBuildContextWithoutParameters,
|
|
725
|
+
): Promise<Awaited<PackBuildResult>> => {
|
|
717
726
|
let buildTimedOut = false;
|
|
718
727
|
let buildTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
719
|
-
const timeoutResult = err(
|
|
728
|
+
const timeoutResult = err(
|
|
729
|
+
parameterizedLoadFailure(
|
|
730
|
+
sourcePath,
|
|
731
|
+
'timeout',
|
|
732
|
+
'build',
|
|
733
|
+
`ScriptablePack build exceeded ${buildTimeoutMs}ms`,
|
|
734
|
+
buildTimeoutMs,
|
|
735
|
+
),
|
|
736
|
+
);
|
|
720
737
|
try {
|
|
738
|
+
const invocation =
|
|
739
|
+
'supportsParameterizedContext' in executor &&
|
|
740
|
+
(executor as Partial<WorkerBackedScriptablePackExecutor>)
|
|
741
|
+
.supportsParameterizedContext === true
|
|
742
|
+
? build(context.readByGuid, {
|
|
743
|
+
packageId: [...context.packageId],
|
|
744
|
+
...('values' in context && context.values !== undefined
|
|
745
|
+
? { values: context.values as Readonly<Record<string, unknown>> }
|
|
746
|
+
: {}),
|
|
747
|
+
})
|
|
748
|
+
: definition.build(context as never);
|
|
721
749
|
const built = await Promise.race([
|
|
722
|
-
Promise.resolve(
|
|
723
|
-
new Promise<
|
|
750
|
+
Promise.resolve(invocation),
|
|
751
|
+
new Promise<Awaited<PackBuildResult>>((resolve) => {
|
|
724
752
|
buildTimeout = setTimeout(() => {
|
|
725
753
|
buildTimedOut = true;
|
|
726
|
-
resolve(timeoutResult as
|
|
754
|
+
resolve(timeoutResult as Awaited<PackBuildResult>);
|
|
727
755
|
}, buildTimeoutMs);
|
|
728
756
|
}),
|
|
729
757
|
]);
|
|
@@ -740,658 +768,19 @@ export async function loadScriptablePack(
|
|
|
740
768
|
if (buildTimeout !== undefined) clearTimeout(buildTimeout);
|
|
741
769
|
}
|
|
742
770
|
},
|
|
743
|
-
});
|
|
771
|
+
} as AnyParameterizedPackDefinition);
|
|
744
772
|
} catch (error) {
|
|
745
|
-
const diagnostic = error instanceof Error ? error.message : String(error);
|
|
746
773
|
disposeReason = 'failure';
|
|
747
|
-
return loadFailure(sourcePath, 'module-load', 'module-load', diagnostic);
|
|
748
|
-
} finally {
|
|
749
|
-
if (timeout !== undefined) clearTimeout(timeout);
|
|
750
|
-
if (disposeReason !== undefined) await executor.dispose?.(disposeReason);
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const CANONICAL_SCAFFOLD_MARKER = '// @forgeax-scriptable-pack canonical-v1';
|
|
755
|
-
const CANONICAL_MANIFEST_NAME = 'canonicalManifest';
|
|
756
|
-
const CANONICAL_MUTABLE_KINDS = new Set<ScriptablePackAssetKind>(['scene', 'mesh', 'material']);
|
|
757
|
-
|
|
758
|
-
interface CanonicalOutput {
|
|
759
|
-
readonly guid: string;
|
|
760
|
-
readonly kind: ScriptablePackAssetKind;
|
|
761
|
-
readonly name?: string;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
interface CanonicalManifest {
|
|
765
|
-
readonly schemaVersion: '1.0.0';
|
|
766
|
-
readonly packageId: string;
|
|
767
|
-
readonly name?: string;
|
|
768
|
-
readonly assets: Readonly<Record<string, CanonicalOutput>>;
|
|
769
|
-
readonly externalAssets: Readonly<Record<string, string>>;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
export interface FileSystemScriptablePackAuthoringOptions {
|
|
773
|
-
/** All source and target paths are confined to this game root. */
|
|
774
|
-
readonly gameRoot: string;
|
|
775
|
-
readonly incomingRefs?: (sourcePath: string, sourceKey?: string) => Promise<readonly string[]>;
|
|
776
|
-
readonly rebuild?: (
|
|
777
|
-
sourcePath: string,
|
|
778
|
-
mode: 'rebuild' | 'cold-cook',
|
|
779
|
-
) => Promise<Result<void, ScriptablePackError>>;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function authoringError(
|
|
783
|
-
code:
|
|
784
|
-
| 'pack-source-path-invalid'
|
|
785
|
-
| 'pack-source-revision-conflict'
|
|
786
|
-
| 'pack-source-mutation-unsupported'
|
|
787
|
-
| 'pack-source-reference-conflict'
|
|
788
|
-
| 'pack-source-write-failed',
|
|
789
|
-
input: {
|
|
790
|
-
readonly expected: string;
|
|
791
|
-
readonly actual?: string;
|
|
792
|
-
readonly hint: string;
|
|
793
|
-
readonly retryable: boolean;
|
|
794
|
-
readonly recoveryActions: readonly string[];
|
|
795
|
-
readonly requestId?: string;
|
|
796
|
-
readonly sourcePath?: string;
|
|
797
|
-
readonly sourceKey?: string;
|
|
798
|
-
readonly incomingRefs?: readonly string[];
|
|
799
|
-
},
|
|
800
|
-
): ScriptablePackError {
|
|
801
|
-
return {
|
|
802
|
-
code,
|
|
803
|
-
expected: input.expected,
|
|
804
|
-
...(input.actual === undefined ? {} : { actual: input.actual }),
|
|
805
|
-
hint: input.hint,
|
|
806
|
-
retryable: input.retryable,
|
|
807
|
-
recoveryActions: input.recoveryActions,
|
|
808
|
-
detail: {
|
|
809
|
-
...(input.requestId === undefined ? {} : { requestId: input.requestId }),
|
|
810
|
-
...(input.sourcePath === undefined ? {} : { sourcePath: input.sourcePath }),
|
|
811
|
-
...(input.sourceKey === undefined ? {} : { sourceKey: input.sourceKey }),
|
|
812
|
-
...(input.incomingRefs === undefined ? {} : { incomingRefs: input.incomingRefs }),
|
|
813
|
-
},
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function confinedSourcePath(
|
|
818
|
-
gameRoot: string,
|
|
819
|
-
sourcePath: string,
|
|
820
|
-
): Result<{ readonly absolute: string; readonly relative: string }, ScriptablePackError> {
|
|
821
|
-
const root = resolve(gameRoot);
|
|
822
|
-
const absolute = resolve(root, sourcePath);
|
|
823
|
-
const rel = relative(root, absolute);
|
|
824
|
-
if (
|
|
825
|
-
rel.length === 0 ||
|
|
826
|
-
rel === '..' ||
|
|
827
|
-
rel.startsWith(`..${sep}`) ||
|
|
828
|
-
isAbsolute(sourcePath) ||
|
|
829
|
-
!rel.endsWith('.pack.ts')
|
|
830
|
-
) {
|
|
831
774
|
return err(
|
|
832
|
-
|
|
833
|
-
expected: 'a game-root-relative *.pack.ts path confined to the selected game',
|
|
834
|
-
actual: sourcePath,
|
|
835
|
-
hint: 'choose a relative ScriptablePack source path inside the selected game root',
|
|
836
|
-
retryable: false,
|
|
837
|
-
recoveryActions: ['choose-game-relative-source-path'],
|
|
775
|
+
parameterizedLoadFailure(
|
|
838
776
|
sourcePath,
|
|
839
|
-
|
|
777
|
+
'module-load',
|
|
778
|
+
'module-load',
|
|
779
|
+
error instanceof Error ? error.message : String(error),
|
|
780
|
+
),
|
|
840
781
|
);
|
|
782
|
+
} finally {
|
|
783
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
784
|
+
if (disposeReason !== undefined) await executor.dispose?.(disposeReason);
|
|
841
785
|
}
|
|
842
|
-
return ok({ absolute, relative: rel.split(sep).join('/') });
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function revisionOf(source: string): string {
|
|
846
|
-
return createHash('sha256').update(source).digest('hex');
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
850
|
-
if (
|
|
851
|
-
ts.isAsExpression(expression) ||
|
|
852
|
-
ts.isSatisfiesExpression(expression) ||
|
|
853
|
-
ts.isParenthesizedExpression(expression)
|
|
854
|
-
) {
|
|
855
|
-
return unwrapExpression(expression.expression);
|
|
856
|
-
}
|
|
857
|
-
return expression;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function literalValue(expression: ts.Expression): unknown {
|
|
861
|
-
const value = unwrapExpression(expression);
|
|
862
|
-
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) return value.text;
|
|
863
|
-
if (value.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
864
|
-
if (value.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
865
|
-
if (value.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
866
|
-
if (ts.isNumericLiteral(value)) return Number(value.text);
|
|
867
|
-
if (ts.isArrayLiteralExpression(value)) return value.elements.map(literalValue);
|
|
868
|
-
if (ts.isObjectLiteralExpression(value)) {
|
|
869
|
-
const result: Record<string, unknown> = {};
|
|
870
|
-
for (const property of value.properties) {
|
|
871
|
-
if (!ts.isPropertyAssignment(property))
|
|
872
|
-
throw new TypeError('manifest properties must be data');
|
|
873
|
-
const name = property.name;
|
|
874
|
-
const key =
|
|
875
|
-
ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)
|
|
876
|
-
? name.text
|
|
877
|
-
: undefined;
|
|
878
|
-
if (key === undefined) throw new TypeError('manifest property names must be static');
|
|
879
|
-
result[key] = literalValue(property.initializer);
|
|
880
|
-
}
|
|
881
|
-
return result;
|
|
882
|
-
}
|
|
883
|
-
throw new TypeError('manifest values must be literal data');
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function parseCanonicalManifest(source: string, sourcePath: string): CanonicalManifest | undefined {
|
|
887
|
-
if (!source.startsWith(`${CANONICAL_SCAFFOLD_MARKER}\n`)) return undefined;
|
|
888
|
-
const file = ts.createSourceFile(
|
|
889
|
-
sourcePath,
|
|
890
|
-
source,
|
|
891
|
-
ts.ScriptTarget.Latest,
|
|
892
|
-
true,
|
|
893
|
-
ts.ScriptKind.TS,
|
|
894
|
-
);
|
|
895
|
-
for (const statement of file.statements) {
|
|
896
|
-
if (!ts.isVariableStatement(statement)) continue;
|
|
897
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
898
|
-
if (
|
|
899
|
-
ts.isIdentifier(declaration.name) &&
|
|
900
|
-
declaration.name.text === CANONICAL_MANIFEST_NAME &&
|
|
901
|
-
declaration.initializer !== undefined
|
|
902
|
-
) {
|
|
903
|
-
const value = literalValue(declaration.initializer);
|
|
904
|
-
if (value === null || typeof value !== 'object') return undefined;
|
|
905
|
-
return value as CanonicalManifest;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return undefined;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function guidExpression(value: string): string {
|
|
913
|
-
return `parseGuid(${JSON.stringify(value)})`;
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/** Render the one source shape that the file authoring port may mutate structurally. */
|
|
917
|
-
export function renderCanonicalScriptablePack(manifest: CanonicalManifest): string {
|
|
918
|
-
const data = JSON.stringify(manifest, null, 2);
|
|
919
|
-
return `${CANONICAL_SCAFFOLD_MARKER}
|
|
920
|
-
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
921
|
-
import { buildMeshAttributeMapForUvSets } from '@forgeax/engine-geometry';
|
|
922
|
-
import type { ScriptablePackDefinition } from '@forgeax/engine-pack/source';
|
|
923
|
-
import type { Asset, AssetGuid as AssetGuidType } from '@forgeax/engine-types';
|
|
924
|
-
import { ok } from '@forgeax/engine-types';
|
|
925
|
-
|
|
926
|
-
const ${CANONICAL_MANIFEST_NAME} = ${data} as const;
|
|
927
|
-
|
|
928
|
-
function parseGuid(value: string): AssetGuidType {
|
|
929
|
-
const parsed = AssetGuid.parse(value);
|
|
930
|
-
if (!parsed.ok) throw parsed.error;
|
|
931
|
-
return parsed.value;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function createOutput(kind: string): Asset {
|
|
935
|
-
switch (kind) {
|
|
936
|
-
case 'scene':
|
|
937
|
-
return { kind: 'scene', entities: [], mounts: [] };
|
|
938
|
-
case 'material':
|
|
939
|
-
return { kind: 'material', values: {} };
|
|
940
|
-
case 'mesh': {
|
|
941
|
-
const vertices = new Float32Array([
|
|
942
|
-
0, 0.7, 0, 0, 0, 1, 0.5, 1, 0, 0, 0, 1,
|
|
943
|
-
-0.7, -0.6, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
|
|
944
|
-
0.7, -0.6, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1,
|
|
945
|
-
]);
|
|
946
|
-
return {
|
|
947
|
-
kind: 'mesh',
|
|
948
|
-
vertices,
|
|
949
|
-
indices: new Uint16Array([0, 1, 2]),
|
|
950
|
-
attributes: { ...buildMeshAttributeMapForUvSets(1), position: vertices },
|
|
951
|
-
aabb: new Float32Array([-0.7, -0.6, 0, 0.7, 0.7, 0]),
|
|
952
|
-
submeshes: [{ indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 }],
|
|
953
|
-
materialSlots: [{ slotName: 'Default' }],
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
default:
|
|
957
|
-
throw new Error(\`canonical ScriptablePack output kind is not mutable: \${kind}\`);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const assets = Object.fromEntries(
|
|
962
|
-
Object.entries(${CANONICAL_MANIFEST_NAME}.assets).map(([sourceKey, asset]) => [
|
|
963
|
-
sourceKey,
|
|
964
|
-
{ ...asset, guid: parseGuid(asset.guid) },
|
|
965
|
-
]),
|
|
966
|
-
);
|
|
967
|
-
const externalAssets = Object.fromEntries(
|
|
968
|
-
Object.entries(${CANONICAL_MANIFEST_NAME}.externalAssets).map(([alias, guid]) => [alias, parseGuid(guid)]),
|
|
969
|
-
);
|
|
970
|
-
|
|
971
|
-
export default {
|
|
972
|
-
schemaVersion: '1.0.0',
|
|
973
|
-
packageId: ${guidExpression(manifest.packageId)},
|
|
974
|
-
${manifest.name === undefined ? '' : `name: ${JSON.stringify(manifest.name)},\n `}assets,
|
|
975
|
-
externalAssets,
|
|
976
|
-
build: () => ok(Object.fromEntries(Object.entries(assets).map(([sourceKey, asset]) => [sourceKey, createOutput(asset.kind)])) as Record<string, Asset>),
|
|
977
|
-
} satisfies ScriptablePackDefinition;
|
|
978
|
-
`;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function definitionFromCanonical(
|
|
982
|
-
manifest: CanonicalManifest,
|
|
983
|
-
): Result<ScriptablePackDefinition, ScriptablePackError> {
|
|
984
|
-
const packageId = AssetGuidCodec.parse(manifest.packageId);
|
|
985
|
-
if (!packageId.ok) return invalidCanonicalGuid('$.packageId', manifest.packageId);
|
|
986
|
-
const assets: Record<string, { guid: AssetGuid; kind: ScriptablePackAssetKind; name?: string }> =
|
|
987
|
-
{};
|
|
988
|
-
for (const [sourceKey, output] of Object.entries(manifest.assets)) {
|
|
989
|
-
const guid = AssetGuidCodec.parse(output.guid);
|
|
990
|
-
if (!guid.ok)
|
|
991
|
-
return invalidCanonicalGuid(`$.assets[${JSON.stringify(sourceKey)}].guid`, output.guid);
|
|
992
|
-
assets[sourceKey] = {
|
|
993
|
-
guid: guid.value,
|
|
994
|
-
kind: output.kind,
|
|
995
|
-
...(output.name === undefined ? {} : { name: output.name }),
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
const externalAssets: Record<string, AssetGuid> = {};
|
|
999
|
-
for (const [alias, value] of Object.entries(manifest.externalAssets)) {
|
|
1000
|
-
const guid = AssetGuidCodec.parse(value);
|
|
1001
|
-
if (!guid.ok) return invalidCanonicalGuid(`$.externalAssets[${JSON.stringify(alias)}]`, value);
|
|
1002
|
-
externalAssets[alias] = guid.value;
|
|
1003
|
-
}
|
|
1004
|
-
return validateScriptablePackDefinition({
|
|
1005
|
-
schemaVersion: '1.0.0',
|
|
1006
|
-
packageId: packageId.value,
|
|
1007
|
-
...(manifest.name === undefined ? {} : { name: manifest.name }),
|
|
1008
|
-
assets,
|
|
1009
|
-
externalAssets,
|
|
1010
|
-
build: () => ok({} as Record<string, never>),
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function invalidCanonicalGuid(
|
|
1015
|
-
propertyPath: string,
|
|
1016
|
-
actual: string,
|
|
1017
|
-
): Result<never, ScriptablePackError> {
|
|
1018
|
-
return err({
|
|
1019
|
-
code: 'pack-source-definition-invalid',
|
|
1020
|
-
expected: 'a canonical UUID string',
|
|
1021
|
-
hint: 'repair the canonical scaffold manifest GUID and inspect Meta again',
|
|
1022
|
-
detail: { propertyPath, actual },
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
function invalidCanonicalSource(
|
|
1027
|
-
sourcePath: string,
|
|
1028
|
-
error: unknown,
|
|
1029
|
-
): Result<never, ScriptablePackError> {
|
|
1030
|
-
return err({
|
|
1031
|
-
code: 'pack-source-definition-invalid',
|
|
1032
|
-
expected: 'a literal canonical-v1 manifest with the required package/assets/external fields',
|
|
1033
|
-
hint: 'repair the canonical scaffold or open it as custom source without structured mutation',
|
|
1034
|
-
detail: {
|
|
1035
|
-
sourcePath,
|
|
1036
|
-
propertyPath: '$.canonicalManifest',
|
|
1037
|
-
actual: error instanceof Error ? error.message : String(error),
|
|
1038
|
-
},
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
async function atomicWrite(path: string, source: string): Promise<void> {
|
|
1043
|
-
await mkdir(dirname(path), { recursive: true });
|
|
1044
|
-
const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1045
|
-
try {
|
|
1046
|
-
await writeFile(temporary, source, { encoding: 'utf8', flag: 'wx' });
|
|
1047
|
-
await rename(temporary, path);
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
await rm(temporary, { force: true });
|
|
1050
|
-
throw error;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
|
|
1055
|
-
return [...left].sort().join('\0') === [...right].sort().join('\0');
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
/** Node filesystem adapter for the public ScriptablePack authoring gateway. */
|
|
1059
|
-
export function createFileSystemScriptablePackAuthoringPort(
|
|
1060
|
-
options: FileSystemScriptablePackAuthoringOptions,
|
|
1061
|
-
): ScriptablePackAuthoringPort {
|
|
1062
|
-
const resolvePath = (sourcePath: string) => confinedSourcePath(options.gameRoot, sourcePath);
|
|
1063
|
-
|
|
1064
|
-
async function readSource(sourcePath: string) {
|
|
1065
|
-
const path = resolvePath(sourcePath);
|
|
1066
|
-
if (!path.ok) return path;
|
|
1067
|
-
try {
|
|
1068
|
-
const source = await readFile(path.value.absolute, 'utf8');
|
|
1069
|
-
return ok({ ...path.value, source, revision: revisionOf(source) });
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
return err(
|
|
1072
|
-
authoringError('pack-source-write-failed', {
|
|
1073
|
-
expected: 'a readable ScriptablePack source file',
|
|
1074
|
-
actual: error instanceof Error ? error.message : String(error),
|
|
1075
|
-
hint: 'restore the source file or create it through the ScriptablePack gateway',
|
|
1076
|
-
retryable: true,
|
|
1077
|
-
recoveryActions: ['inspect-source-path', 'retry'],
|
|
1078
|
-
sourcePath,
|
|
1079
|
-
}),
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
async function inspect(sourcePath: string) {
|
|
1085
|
-
const read = await readSource(sourcePath);
|
|
1086
|
-
if (!read.ok) return read;
|
|
1087
|
-
let canonical: CanonicalManifest | undefined;
|
|
1088
|
-
try {
|
|
1089
|
-
canonical = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1090
|
-
} catch (error) {
|
|
1091
|
-
return invalidCanonicalSource(read.value.relative, error);
|
|
1092
|
-
}
|
|
1093
|
-
const loaded =
|
|
1094
|
-
canonical === undefined
|
|
1095
|
-
? await loadScriptablePack(read.value.absolute, { metadataOnly: true })
|
|
1096
|
-
: definitionFromCanonical(canonical);
|
|
1097
|
-
if (!loaded.ok) return loaded;
|
|
1098
|
-
return ok({
|
|
1099
|
-
revision: read.value.revision,
|
|
1100
|
-
meta: projectScriptablePackMeta(loaded.value, read.value.relative),
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
async function preflight(sourcePath: string) {
|
|
1105
|
-
const read = await readSource(sourcePath);
|
|
1106
|
-
if (!read.ok) return read;
|
|
1107
|
-
const inspected = await inspect(sourcePath);
|
|
1108
|
-
if (!inspected.ok) return inspected;
|
|
1109
|
-
let canonical: CanonicalManifest | undefined;
|
|
1110
|
-
try {
|
|
1111
|
-
canonical = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1112
|
-
} catch (error) {
|
|
1113
|
-
return invalidCanonicalSource(read.value.relative, error);
|
|
1114
|
-
}
|
|
1115
|
-
const incomingRefs = (await options.incomingRefs?.(read.value.relative)) ?? [];
|
|
1116
|
-
return ok({
|
|
1117
|
-
revision: inspected.value.revision,
|
|
1118
|
-
meta: inspected.value.meta,
|
|
1119
|
-
capabilities:
|
|
1120
|
-
canonical === undefined
|
|
1121
|
-
? ({
|
|
1122
|
-
inspect: true,
|
|
1123
|
-
rebuild: true,
|
|
1124
|
-
coldCook: true,
|
|
1125
|
-
reason: 'structured mutation is available only for canonical-v1 scaffolds',
|
|
1126
|
-
} as const)
|
|
1127
|
-
: ({
|
|
1128
|
-
inspect: true,
|
|
1129
|
-
rebuild: true,
|
|
1130
|
-
coldCook: true,
|
|
1131
|
-
addOutput: true,
|
|
1132
|
-
addExternalAsset: true,
|
|
1133
|
-
renameDisplay: true,
|
|
1134
|
-
removeOutput: true,
|
|
1135
|
-
clone: true,
|
|
1136
|
-
} as const),
|
|
1137
|
-
incomingRefs,
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
async function mutate(operation: ScriptablePackAuthoringMutation) {
|
|
1142
|
-
const targetPath =
|
|
1143
|
-
operation.kind === 'clone-scriptable-pack' ? operation.targetPath : operation.sourcePath;
|
|
1144
|
-
const target = resolvePath(targetPath);
|
|
1145
|
-
if (!target.ok) return target;
|
|
1146
|
-
|
|
1147
|
-
let manifest: CanonicalManifest;
|
|
1148
|
-
let currentRevision: string | undefined;
|
|
1149
|
-
if (operation.kind === 'create-scriptable-pack') {
|
|
1150
|
-
if (!CANONICAL_MUTABLE_KINDS.has(operation.initialOutput.kind)) {
|
|
1151
|
-
return err(
|
|
1152
|
-
authoringError('pack-source-mutation-unsupported', {
|
|
1153
|
-
expected: 'a canonical scene, mesh, or material initial output',
|
|
1154
|
-
actual: operation.initialOutput.kind,
|
|
1155
|
-
hint: 'create one of the canonical output kinds, then hand-author advanced outputs',
|
|
1156
|
-
retryable: false,
|
|
1157
|
-
recoveryActions: ['choose-supported-output-kind'],
|
|
1158
|
-
requestId: operation.requestId,
|
|
1159
|
-
sourcePath: operation.sourcePath,
|
|
1160
|
-
}),
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
try {
|
|
1164
|
-
await stat(target.value.absolute);
|
|
1165
|
-
return err(
|
|
1166
|
-
authoringError('pack-source-revision-conflict', {
|
|
1167
|
-
expected: 'an unused target source path',
|
|
1168
|
-
actual: 'source already exists',
|
|
1169
|
-
hint: 'inspect the existing source or choose a new path',
|
|
1170
|
-
retryable: false,
|
|
1171
|
-
recoveryActions: ['asset.preflight', 'choose-new-source-path'],
|
|
1172
|
-
requestId: operation.requestId,
|
|
1173
|
-
sourcePath: operation.sourcePath,
|
|
1174
|
-
}),
|
|
1175
|
-
);
|
|
1176
|
-
} catch {
|
|
1177
|
-
// Missing is the required create precondition.
|
|
1178
|
-
}
|
|
1179
|
-
manifest = {
|
|
1180
|
-
schemaVersion: '1.0.0',
|
|
1181
|
-
packageId: AssetGuidCodec.format(operation.packageId),
|
|
1182
|
-
...(operation.name === undefined ? {} : { name: operation.name }),
|
|
1183
|
-
assets: {
|
|
1184
|
-
[operation.initialOutput.sourceKey]: {
|
|
1185
|
-
guid: AssetGuidCodec.format(operation.initialOutput.guid),
|
|
1186
|
-
kind: operation.initialOutput.kind,
|
|
1187
|
-
...(operation.initialOutput.name === undefined
|
|
1188
|
-
? {}
|
|
1189
|
-
: { name: operation.initialOutput.name }),
|
|
1190
|
-
},
|
|
1191
|
-
},
|
|
1192
|
-
externalAssets: {},
|
|
1193
|
-
};
|
|
1194
|
-
} else {
|
|
1195
|
-
const read = await readSource(operation.sourcePath);
|
|
1196
|
-
if (!read.ok) return read;
|
|
1197
|
-
currentRevision = read.value.revision;
|
|
1198
|
-
if (
|
|
1199
|
-
operation.expectedRevision !== undefined &&
|
|
1200
|
-
operation.expectedRevision !== currentRevision
|
|
1201
|
-
) {
|
|
1202
|
-
return err(
|
|
1203
|
-
authoringError('pack-source-revision-conflict', {
|
|
1204
|
-
expected: operation.expectedRevision,
|
|
1205
|
-
actual: currentRevision,
|
|
1206
|
-
hint: 'inspect the current source revision, reconcile the edit, then retry with a new requestId',
|
|
1207
|
-
retryable: true,
|
|
1208
|
-
recoveryActions: ['asset.preflight', 'mint-request-id'],
|
|
1209
|
-
requestId: operation.requestId,
|
|
1210
|
-
sourcePath: operation.sourcePath,
|
|
1211
|
-
}),
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
let parsed: CanonicalManifest | undefined;
|
|
1215
|
-
try {
|
|
1216
|
-
parsed = parseCanonicalManifest(read.value.source, read.value.relative);
|
|
1217
|
-
} catch (error) {
|
|
1218
|
-
return invalidCanonicalSource(read.value.relative, error);
|
|
1219
|
-
}
|
|
1220
|
-
if (parsed === undefined) {
|
|
1221
|
-
return err(
|
|
1222
|
-
authoringError('pack-source-mutation-unsupported', {
|
|
1223
|
-
expected: 'a canonical-v1 scaffold for structured source mutation',
|
|
1224
|
-
actual: 'custom ScriptablePack source',
|
|
1225
|
-
hint: 'clone into a canonical scaffold or edit custom source with a code editor',
|
|
1226
|
-
retryable: false,
|
|
1227
|
-
recoveryActions: ['clone-scriptable-pack', 'open-code-editor'],
|
|
1228
|
-
requestId: operation.requestId,
|
|
1229
|
-
sourcePath: operation.sourcePath,
|
|
1230
|
-
}),
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
manifest = parsed;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
if (operation.kind === 'clone-scriptable-pack') {
|
|
1237
|
-
try {
|
|
1238
|
-
await stat(target.value.absolute);
|
|
1239
|
-
return err(
|
|
1240
|
-
authoringError('pack-source-revision-conflict', {
|
|
1241
|
-
expected: 'an unused clone target source path',
|
|
1242
|
-
actual: 'source already exists',
|
|
1243
|
-
hint: 'inspect the existing target or choose a new path',
|
|
1244
|
-
retryable: false,
|
|
1245
|
-
recoveryActions: ['asset.preflight', 'choose-new-source-path'],
|
|
1246
|
-
requestId: operation.requestId,
|
|
1247
|
-
sourcePath: target.value.relative,
|
|
1248
|
-
}),
|
|
1249
|
-
);
|
|
1250
|
-
} catch {
|
|
1251
|
-
// Missing is the required clone precondition.
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
if (operation.kind === 'add-output') {
|
|
1256
|
-
if (!CANONICAL_MUTABLE_KINDS.has(operation.assetKind)) {
|
|
1257
|
-
return err(
|
|
1258
|
-
authoringError('pack-source-mutation-unsupported', {
|
|
1259
|
-
expected: 'a canonical scene, mesh, or material output',
|
|
1260
|
-
actual: operation.assetKind,
|
|
1261
|
-
hint: 'hand-author advanced output kinds in the source module',
|
|
1262
|
-
retryable: false,
|
|
1263
|
-
recoveryActions: ['open-code-editor'],
|
|
1264
|
-
requestId: operation.requestId,
|
|
1265
|
-
sourcePath: operation.sourcePath,
|
|
1266
|
-
sourceKey: operation.sourceKey,
|
|
1267
|
-
}),
|
|
1268
|
-
);
|
|
1269
|
-
}
|
|
1270
|
-
manifest = {
|
|
1271
|
-
...manifest,
|
|
1272
|
-
assets: {
|
|
1273
|
-
...manifest.assets,
|
|
1274
|
-
[operation.sourceKey]: {
|
|
1275
|
-
guid: AssetGuidCodec.format(operation.guid),
|
|
1276
|
-
kind: operation.assetKind,
|
|
1277
|
-
...(operation.name === undefined ? {} : { name: operation.name }),
|
|
1278
|
-
},
|
|
1279
|
-
},
|
|
1280
|
-
};
|
|
1281
|
-
} else if (operation.kind === 'add-external-asset') {
|
|
1282
|
-
manifest = {
|
|
1283
|
-
...manifest,
|
|
1284
|
-
externalAssets: {
|
|
1285
|
-
...manifest.externalAssets,
|
|
1286
|
-
[operation.alias]: AssetGuidCodec.format(operation.guid),
|
|
1287
|
-
},
|
|
1288
|
-
};
|
|
1289
|
-
} else if (operation.kind === 'rename-display') {
|
|
1290
|
-
if (operation.target.kind === 'package') manifest = { ...manifest, name: operation.name };
|
|
1291
|
-
else {
|
|
1292
|
-
const output = manifest.assets[operation.target.sourceKey];
|
|
1293
|
-
if (output === undefined)
|
|
1294
|
-
return err(
|
|
1295
|
-
authoringError('pack-source-mutation-unsupported', {
|
|
1296
|
-
expected: 'an existing sourceKey',
|
|
1297
|
-
actual: operation.target.sourceKey,
|
|
1298
|
-
hint: 'inspect Meta and choose a current output',
|
|
1299
|
-
retryable: true,
|
|
1300
|
-
recoveryActions: ['asset.preflight'],
|
|
1301
|
-
requestId: operation.requestId,
|
|
1302
|
-
sourcePath: operation.sourcePath,
|
|
1303
|
-
sourceKey: operation.target.sourceKey,
|
|
1304
|
-
}),
|
|
1305
|
-
);
|
|
1306
|
-
manifest = {
|
|
1307
|
-
...manifest,
|
|
1308
|
-
assets: {
|
|
1309
|
-
...manifest.assets,
|
|
1310
|
-
[operation.target.sourceKey]: { ...output, name: operation.name },
|
|
1311
|
-
},
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
} else if (operation.kind === 'remove-output') {
|
|
1315
|
-
const incomingRefs =
|
|
1316
|
-
(await options.incomingRefs?.(operation.sourcePath, operation.sourceKey)) ?? [];
|
|
1317
|
-
if (
|
|
1318
|
-
incomingRefs.length > 0 &&
|
|
1319
|
-
!sameStrings(incomingRefs, operation.confirmIncomingRefs ?? [])
|
|
1320
|
-
) {
|
|
1321
|
-
return err(
|
|
1322
|
-
authoringError('pack-source-reference-conflict', {
|
|
1323
|
-
expected: 'explicit acknowledgement of every current incoming reference',
|
|
1324
|
-
actual: `${incomingRefs.length} incoming references`,
|
|
1325
|
-
hint: 'inspect dependents, then confirm the exact incoming reference identities or cancel',
|
|
1326
|
-
retryable: true,
|
|
1327
|
-
recoveryActions: ['inspect-incoming-refs', 'confirm-remove-output'],
|
|
1328
|
-
requestId: operation.requestId,
|
|
1329
|
-
sourcePath: operation.sourcePath,
|
|
1330
|
-
sourceKey: operation.sourceKey,
|
|
1331
|
-
incomingRefs,
|
|
1332
|
-
}),
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
const { [operation.sourceKey]: removed, ...assets } = manifest.assets;
|
|
1336
|
-
if (removed === undefined || Object.keys(assets).length === 0) {
|
|
1337
|
-
return err(
|
|
1338
|
-
authoringError('pack-source-mutation-unsupported', {
|
|
1339
|
-
expected: 'an existing output while retaining at least one package output',
|
|
1340
|
-
actual: operation.sourceKey,
|
|
1341
|
-
hint: 'keep one output or remove the entire source file through an explicit file operation',
|
|
1342
|
-
retryable: false,
|
|
1343
|
-
recoveryActions: ['asset.preflight'],
|
|
1344
|
-
requestId: operation.requestId,
|
|
1345
|
-
sourcePath: operation.sourcePath,
|
|
1346
|
-
sourceKey: operation.sourceKey,
|
|
1347
|
-
}),
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
manifest = { ...manifest, assets };
|
|
1351
|
-
} else if (operation.kind === 'clone-scriptable-pack') {
|
|
1352
|
-
manifest = {
|
|
1353
|
-
...manifest,
|
|
1354
|
-
packageId: AssetGuidCodec.format(operation.packageId),
|
|
1355
|
-
assets: Object.fromEntries(
|
|
1356
|
-
Object.entries(manifest.assets).map(([sourceKey, output]) => [
|
|
1357
|
-
sourceKey,
|
|
1358
|
-
{
|
|
1359
|
-
...output,
|
|
1360
|
-
guid: AssetGuidCodec.format(operation.outputGuids[sourceKey] as AssetGuid),
|
|
1361
|
-
},
|
|
1362
|
-
]),
|
|
1363
|
-
),
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
const source = renderCanonicalScriptablePack(manifest);
|
|
1368
|
-
try {
|
|
1369
|
-
await atomicWrite(target.value.absolute, source);
|
|
1370
|
-
return ok({ sourcePath: target.value.relative, revision: revisionOf(source) });
|
|
1371
|
-
} catch (error) {
|
|
1372
|
-
return err(
|
|
1373
|
-
authoringError('pack-source-write-failed', {
|
|
1374
|
-
expected: 'an atomic source-file replacement',
|
|
1375
|
-
actual: error instanceof Error ? error.message : String(error),
|
|
1376
|
-
hint: 'repair filesystem permissions or disk capacity, then retry with a new requestId',
|
|
1377
|
-
retryable: true,
|
|
1378
|
-
recoveryActions: ['inspect-filesystem', 'mint-request-id'],
|
|
1379
|
-
requestId: operation.requestId,
|
|
1380
|
-
sourcePath: target.value.relative,
|
|
1381
|
-
}),
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
return {
|
|
1387
|
-
preflight,
|
|
1388
|
-
mutate,
|
|
1389
|
-
inspect,
|
|
1390
|
-
async rebuild(sourcePath, mode) {
|
|
1391
|
-
const path = resolvePath(sourcePath);
|
|
1392
|
-
if (!path.ok) return path;
|
|
1393
|
-
const rebuilt = (await options.rebuild?.(path.value.relative, mode)) ?? ok(undefined);
|
|
1394
|
-
return rebuilt.ok ? inspect(path.value.relative) : rebuilt;
|
|
1395
|
-
},
|
|
1396
|
-
};
|
|
1397
786
|
}
|