@forgeax/engine-import 0.1.3 → 0.1.4
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 +6 -5
- package/dist/.tsbuildinfo +1 -1
- 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__/scriptable-pack-product.contract.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
- package/dist/browser.d.ts +5 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.mjs +782 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/build-production.d.ts +48 -0
- package/dist/build-production.d.ts.map +1 -0
- package/dist/catalog-importer-policy.d.ts +13 -0
- package/dist/catalog-importer-policy.d.ts.map +1 -0
- package/dist/catalog-inventory.d.ts +4 -0
- package/dist/catalog-inventory.d.ts.map +1 -0
- package/dist/import-product.d.ts +16 -25
- package/dist/import-product.d.ts.map +1 -1
- package/dist/import-runner.d.ts +6 -3
- package/dist/import-runner.d.ts.map +1 -1
- package/dist/importer-registry.d.ts +9 -1
- package/dist/importer-registry.d.ts.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1315 -155
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-bin.d.ts +1 -1
- package/dist/mesh-bin.d.ts.map +1 -1
- package/dist/mesh-bin.mjs +182 -0
- package/dist/mesh-bin.mjs.map +1 -0
- package/dist/pack-projection.d.ts +5 -0
- package/dist/pack-projection.d.ts.map +1 -0
- package/dist/scriptable-pack-host.d.ts +64 -0
- package/dist/scriptable-pack-host.d.ts.map +1 -0
- package/dist/scriptable-pack-output-producers.d.ts +3 -2
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
- package/dist/scriptable-pack-staged-snapshot.d.ts +1 -2
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack.d.ts +6 -18
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-source-package.d.ts +14 -0
- package/dist/scriptable-source-package.d.ts.map +1 -0
- package/dist/source-package-errors.d.ts +52 -0
- package/dist/source-package-errors.d.ts.map +1 -0
- package/dist/source-package-publication.d.ts +63 -0
- package/dist/source-package-publication.d.ts.map +1 -0
- package/dist/source-package.d.ts +60 -0
- package/dist/source-package.d.ts.map +1 -0
- package/package.json +13 -7
- package/src/__tests__/import-contract-migration.test.ts +33 -0
- package/src/__tests__/import-local-artifacts.test.ts +3 -1
- package/src/__tests__/owner-chain.integration.test.ts +37 -0
- package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +78 -19
- package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +52 -20
- package/src/browser.ts +14 -0
- package/src/build-production.ts +487 -0
- package/src/catalog-importer-policy.ts +58 -0
- package/src/catalog-inventory.ts +24 -0
- package/src/import-product.ts +37 -69
- package/src/import-runner.ts +36 -7
- package/src/importer-registry.ts +24 -1
- package/src/index.ts +79 -8
- package/src/mesh-bin.ts +2 -2
- package/src/pack-projection.ts +21 -0
- package/src/scriptable-pack-host.ts +461 -0
- package/src/scriptable-pack-output-producers.ts +48 -58
- package/src/scriptable-pack-staged-snapshot.ts +17 -6
- package/src/scriptable-pack.ts +37 -26
- package/src/scriptable-source-package.ts +88 -0
- package/src/source-package-errors.ts +201 -0
- package/src/source-package-publication.ts +295 -0
- package/src/source-package.ts +187 -0
- package/dist/__tests__/material-import-product.unit.test.d.ts +0 -2
- package/dist/__tests__/material-import-product.unit.test.d.ts.map +0 -1
- package/src/__tests__/material-import-product.unit.test.ts +0 -56
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { createAcceptedPublication } from '@forgeax/engine-ddc';
|
|
5
|
+
import {
|
|
6
|
+
type AuthoredPackInput,
|
|
7
|
+
finalizePackageTransportSource,
|
|
8
|
+
type PackageFinalizePolicy,
|
|
9
|
+
upgradeLegacyAuthoredPack,
|
|
10
|
+
} from '@forgeax/engine-pack/build';
|
|
11
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
12
|
+
import { type NativeCooker, NativeCookerRegistry } from '@forgeax/engine-pack/native-cooker';
|
|
13
|
+
import type { ScanSourceDeclaration } from '@forgeax/engine-pack/scanner';
|
|
14
|
+
import {
|
|
15
|
+
projectScriptablePackSceneComponents,
|
|
16
|
+
type ScriptablePackDefinition,
|
|
17
|
+
type ScriptablePackSourceClosureEntry,
|
|
18
|
+
} from '@forgeax/engine-pack/source';
|
|
19
|
+
import { inventoryScriptablePackSource } from '@forgeax/engine-pack/source-node';
|
|
20
|
+
import type {
|
|
21
|
+
Asset,
|
|
22
|
+
AssetGuid as AssetGuidType,
|
|
23
|
+
AssetPublicationEnvelope,
|
|
24
|
+
AssetPublicationOutput,
|
|
25
|
+
ResourceRevision,
|
|
26
|
+
Result,
|
|
27
|
+
} from '@forgeax/engine-types';
|
|
28
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
29
|
+
import type { DdcPack } from './import-runner.js';
|
|
30
|
+
import { projectImportProductForBuild } from './pack-projection.js';
|
|
31
|
+
import type { ScriptablePackDomainError, ScriptablePackStagedOutput } from './scriptable-pack.js';
|
|
32
|
+
import { createStandardAssetOutputProducerRegistry } from './scriptable-pack-output-producers.js';
|
|
33
|
+
import {
|
|
34
|
+
createScriptablePackStagedAssetSnapshotSource,
|
|
35
|
+
type ScriptablePackStagedOwner,
|
|
36
|
+
} from './scriptable-pack-staged-snapshot.js';
|
|
37
|
+
import {
|
|
38
|
+
produceScriptableSourcePackage,
|
|
39
|
+
type ScriptableSourcePackageProduct,
|
|
40
|
+
type ScriptableSourcePackageResult,
|
|
41
|
+
} from './scriptable-source-package.js';
|
|
42
|
+
|
|
43
|
+
type FinalizedPackageTransport = Awaited<ReturnType<typeof finalizePackageTransportSource>>;
|
|
44
|
+
|
|
45
|
+
export interface ScriptablePackInput {
|
|
46
|
+
readonly sourcePath: string;
|
|
47
|
+
readonly displaySourcePath: string;
|
|
48
|
+
readonly definition: Readonly<ScriptablePackDefinition>;
|
|
49
|
+
readonly sourceClosure: readonly ScriptablePackSourceClosureEntry[];
|
|
50
|
+
readonly publicationGeneration: number;
|
|
51
|
+
readonly policy: ScriptablePackTransportPolicy;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ScriptablePackPublicationFacts {
|
|
55
|
+
readonly outputs: readonly AssetPublicationOutput[];
|
|
56
|
+
readonly refs: ReadonlyMap<string, readonly string[]>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PreparedScriptablePack {
|
|
60
|
+
readonly product: ScriptableSourcePackageProduct;
|
|
61
|
+
readonly finalized: FinalizedPackageTransport;
|
|
62
|
+
readonly facts: ScriptablePackPublicationFacts;
|
|
63
|
+
readonly revision: ResourceRevision;
|
|
64
|
+
readonly publication: AssetPublicationEnvelope;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ScriptablePackTransportPaths {
|
|
68
|
+
readonly packagePath: string;
|
|
69
|
+
readonly artifactPath: (path: string) => string;
|
|
70
|
+
readonly receiptPath: (guid: string) => string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ScriptablePackTransportSink {
|
|
74
|
+
writePackage(path: string, body: string): void | Promise<void>;
|
|
75
|
+
writeArtifact(path: string, bytes: Uint8Array, mimeType: string): void | Promise<void>;
|
|
76
|
+
writeReceipt(path: string, body: string): void | Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ScriptablePackTransportPolicy =
|
|
80
|
+
| PackageFinalizePolicy
|
|
81
|
+
| ((product: ScriptableSourcePackageProduct) => PackageFinalizePolicy);
|
|
82
|
+
|
|
83
|
+
type ScriptablePackHostError =
|
|
84
|
+
ScriptableSourcePackageResult extends Result<unknown, infer E> ? E : never;
|
|
85
|
+
|
|
86
|
+
export interface CookedAuthoredPack {
|
|
87
|
+
readonly logicalPackage: DdcPack;
|
|
88
|
+
readonly refsByGuid: ReadonlyMap<string, readonly string[]>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AuthoredPackTransport {
|
|
92
|
+
readonly pack: AuthoredPackInput;
|
|
93
|
+
readonly firstGuid?: string;
|
|
94
|
+
readonly cooked?: CookedAuthoredPack;
|
|
95
|
+
readonly finalized?: FinalizedPackageTransport;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function canonicalScriptableSourcePath(sourcePath: string): string {
|
|
99
|
+
const normalized = sourcePath.replaceAll('\\', '/');
|
|
100
|
+
const marker = '/assets/';
|
|
101
|
+
const markerIndex = normalized.indexOf(marker);
|
|
102
|
+
return markerIndex < 0 ? normalized : normalized.slice(markerIndex + 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function scriptablePackInputs(
|
|
106
|
+
sourcePaths: readonly string[],
|
|
107
|
+
declarations: ReadonlyMap<string, ScanSourceDeclaration>,
|
|
108
|
+
cwd: string,
|
|
109
|
+
generationFor: (displaySourcePath: string) => number,
|
|
110
|
+
policyFor: (displaySourcePath: string) => ScriptablePackTransportPolicy,
|
|
111
|
+
): ScriptablePackInput[] {
|
|
112
|
+
return sourcePaths.flatMap((sourcePath) => {
|
|
113
|
+
const declaration = declarations.get(resolve(cwd, sourcePath));
|
|
114
|
+
if (declaration?.format !== 'pack.ts') return [];
|
|
115
|
+
const displaySourcePath = canonicalScriptableSourcePath(sourcePath);
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
sourcePath: resolve(cwd, sourcePath),
|
|
119
|
+
displaySourcePath,
|
|
120
|
+
definition: declaration.definition,
|
|
121
|
+
sourceClosure: declaration.sourceClosure,
|
|
122
|
+
publicationGeneration: generationFor(displaySourcePath),
|
|
123
|
+
policy: policyFor(displaySourcePath),
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Materialize one accepted ScriptablePack without duplicating dev/build loops. */
|
|
130
|
+
export async function materializePreparedScriptablePack(
|
|
131
|
+
prepared: PreparedScriptablePack,
|
|
132
|
+
paths: ScriptablePackTransportPaths,
|
|
133
|
+
sink: ScriptablePackTransportSink,
|
|
134
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
135
|
+
const { product, finalized } = prepared;
|
|
136
|
+
await sink.writePackage(paths.packagePath, JSON.stringify(finalized.pack));
|
|
137
|
+
for (const artifact of finalized.artifacts) {
|
|
138
|
+
await sink.writeArtifact(
|
|
139
|
+
paths.artifactPath(artifact.path),
|
|
140
|
+
artifact.bytes,
|
|
141
|
+
artifact.path.endsWith('.json') ? 'application/json' : artifact.mediaType,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const receipts = new Map<string, string>();
|
|
145
|
+
for (const guid of product.declaredGuids) {
|
|
146
|
+
const path = paths.receiptPath(guid);
|
|
147
|
+
await sink.writeReceipt(
|
|
148
|
+
path,
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
guid,
|
|
151
|
+
origin: 'sourceMeta',
|
|
152
|
+
status: 'succeeded',
|
|
153
|
+
inputFingerprint: product.inputFingerprint,
|
|
154
|
+
outputDigest: finalized.digest,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
receipts.set(guid, path);
|
|
158
|
+
}
|
|
159
|
+
return receipts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function readCookedAuthoredPack(
|
|
163
|
+
authoredPack: AuthoredPackInput,
|
|
164
|
+
cookers: readonly NativeCooker[] = [],
|
|
165
|
+
sourcePath?: string,
|
|
166
|
+
): Promise<CookedAuthoredPack | undefined> {
|
|
167
|
+
const parsed = upgradeLegacyAuthoredPack(authoredPack);
|
|
168
|
+
if (parsed.schemaVersion !== '2.0.0' || parsed.assets === undefined) return undefined;
|
|
169
|
+
const registry = new NativeCookerRegistry();
|
|
170
|
+
for (const cooker of cookers) registry.register(cooker);
|
|
171
|
+
const assets: DdcPack['assets'][number][] = [];
|
|
172
|
+
const refsByGuid = new Map<string, readonly string[]>();
|
|
173
|
+
let hasCookedAsset = false;
|
|
174
|
+
for (const asset of parsed.assets) {
|
|
175
|
+
const shouldCook = asset.execution === 'cooked';
|
|
176
|
+
if (!shouldCook) {
|
|
177
|
+
assets.push({
|
|
178
|
+
guid: asset.guid,
|
|
179
|
+
kind: asset.kind,
|
|
180
|
+
...(asset.name === undefined ? {} : { name: asset.name }),
|
|
181
|
+
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
182
|
+
...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
|
|
183
|
+
...(asset.relations === undefined ? {} : { relations: asset.relations }),
|
|
184
|
+
payload: asset.payload,
|
|
185
|
+
refs: asset.refs ?? [],
|
|
186
|
+
artifacts: {},
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
hasCookedAsset = true;
|
|
191
|
+
const result = await registry.runDraft(asset.kind, {
|
|
192
|
+
guid: asset.guid,
|
|
193
|
+
source: asset.payload,
|
|
194
|
+
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
195
|
+
...(sourcePath === undefined ? {} : { sourcePath }),
|
|
196
|
+
refs: asset.refs ?? [],
|
|
197
|
+
});
|
|
198
|
+
if (!result.ok) throw result.error;
|
|
199
|
+
const draft = result.value;
|
|
200
|
+
const refs = [...draft.refs];
|
|
201
|
+
refsByGuid.set(asset.guid.toLowerCase(), refs);
|
|
202
|
+
assets.push({
|
|
203
|
+
guid: draft.guid,
|
|
204
|
+
kind: asset.kind,
|
|
205
|
+
...(asset.name === undefined ? {} : { name: asset.name }),
|
|
206
|
+
...(asset.sourceKey === undefined ? {} : { sourceKey: asset.sourceKey }),
|
|
207
|
+
...(asset.sourceIndex === undefined ? {} : { sourceIndex: asset.sourceIndex }),
|
|
208
|
+
...(asset.relations === undefined ? {} : { relations: asset.relations }),
|
|
209
|
+
payload: draft.payload as Record<string, unknown>,
|
|
210
|
+
refs,
|
|
211
|
+
artifacts: draft.artifacts,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return hasCookedAsset
|
|
215
|
+
? {
|
|
216
|
+
logicalPackage: { schemaVersion: '2.0.0', kind: 'internal-text-package', assets },
|
|
217
|
+
refsByGuid,
|
|
218
|
+
}
|
|
219
|
+
: undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function declaredPackExternalOutputs(
|
|
223
|
+
declarations: ReadonlyMap<string, ScanSourceDeclaration>,
|
|
224
|
+
cookers: readonly NativeCooker[] = [],
|
|
225
|
+
requiredGuids: readonly AssetGuidType[] = [],
|
|
226
|
+
): Promise<readonly ScriptablePackStagedOutput[]> {
|
|
227
|
+
if (requiredGuids.length === 0) return [];
|
|
228
|
+
const required = new Set(requiredGuids.map((guid) => AssetGuid.format(guid).toLowerCase()));
|
|
229
|
+
const outputs: ScriptablePackStagedOutput[] = [];
|
|
230
|
+
for (const declaration of declarations.values()) {
|
|
231
|
+
if (declaration.format !== 'pack.json') continue;
|
|
232
|
+
const declaredAssets = declaration.value.assets.filter((asset) =>
|
|
233
|
+
required.has(asset.guid.toLowerCase()),
|
|
234
|
+
);
|
|
235
|
+
if (declaredAssets.length === 0) continue;
|
|
236
|
+
const cooked = await readCookedAuthoredPack(declaration.value, cookers, declaration.sourcePath);
|
|
237
|
+
for (const asset of cooked?.logicalPackage.assets.filter((item) =>
|
|
238
|
+
required.has(item.guid.toLowerCase()),
|
|
239
|
+
) ?? declaredAssets) {
|
|
240
|
+
const parsed = AssetGuid.parse(asset.guid);
|
|
241
|
+
if (!parsed.ok) throw parsed.error;
|
|
242
|
+
outputs.push({ guid: parsed.value, asset: { kind: asset.kind, ...asset.payload } as Asset });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return outputs;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Normalize one authored Pack and, when needed, run its registered cooker once. */
|
|
249
|
+
export async function prepareAuthoredPackTransport(
|
|
250
|
+
authoredPack: AuthoredPackInput,
|
|
251
|
+
cookers: readonly NativeCooker[] | undefined,
|
|
252
|
+
policyFor: (guid: string) => PackageFinalizePolicy,
|
|
253
|
+
sourcePath?: string,
|
|
254
|
+
): Promise<AuthoredPackTransport> {
|
|
255
|
+
const pack = upgradeLegacyAuthoredPack(authoredPack);
|
|
256
|
+
const firstGuid = pack.assets?.[0]?.guid?.toLowerCase();
|
|
257
|
+
if (pack.schemaVersion !== '2.0.0' || firstGuid === undefined) {
|
|
258
|
+
return { pack, ...(firstGuid === undefined ? {} : { firstGuid }) };
|
|
259
|
+
}
|
|
260
|
+
const cooked = await readCookedAuthoredPack(pack, cookers, sourcePath);
|
|
261
|
+
if (cooked === undefined) return { pack, firstGuid };
|
|
262
|
+
return {
|
|
263
|
+
pack,
|
|
264
|
+
firstGuid,
|
|
265
|
+
cooked,
|
|
266
|
+
finalized: await finalizePackageTransportSource(cooked.logicalPackage, policyFor(firstGuid)),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Project one produced ScriptablePack into the publication tuple shared by dev and build. */
|
|
271
|
+
export function projectScriptablePackPublication(
|
|
272
|
+
product: ScriptableSourcePackageProduct,
|
|
273
|
+
): Result<ScriptablePackPublicationFacts, ScriptablePackDomainError> {
|
|
274
|
+
const assets = new Map(product.product.assets.map((asset) => [asset.guid.toLowerCase(), asset]));
|
|
275
|
+
const outputs: AssetPublicationOutput[] = [];
|
|
276
|
+
for (const staged of product.stagedOutputs) {
|
|
277
|
+
const guid = AssetGuid.format(staged.guid).toLowerCase();
|
|
278
|
+
const asset = assets.get(guid);
|
|
279
|
+
if (asset === undefined || staged.digest === undefined) {
|
|
280
|
+
return err({
|
|
281
|
+
code: 'pack-source-output-invalid',
|
|
282
|
+
expected: `ScriptablePack publication output ${guid} to include a product and digest`,
|
|
283
|
+
hint: 'repair the ScriptablePack producer output and rebuild',
|
|
284
|
+
detail: { stage: 'publication', guid },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
outputs.push({
|
|
288
|
+
guid,
|
|
289
|
+
sourceKey: staged.sourceKey ?? guid,
|
|
290
|
+
kind: asset.kind,
|
|
291
|
+
digest: staged.digest,
|
|
292
|
+
refs: asset.refs.map((reference) => reference.guid),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return ok({
|
|
296
|
+
outputs,
|
|
297
|
+
refs: new Map(
|
|
298
|
+
product.product.assets.map((asset) => [
|
|
299
|
+
asset.guid.toLowerCase(),
|
|
300
|
+
asset.refs.map((reference) => reference.guid),
|
|
301
|
+
]),
|
|
302
|
+
),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Project the immutable module closure into the Catalog revision contract. */
|
|
307
|
+
async function scriptablePackResourceRevision(
|
|
308
|
+
displaySourcePath: string,
|
|
309
|
+
digest: string,
|
|
310
|
+
sourceClosure: readonly ScriptablePackSourceClosureEntry[],
|
|
311
|
+
): Promise<ResourceRevision> {
|
|
312
|
+
const observedAt = Math.trunc(
|
|
313
|
+
Math.max(
|
|
314
|
+
...(await Promise.all(sourceClosure.map(async (entry) => (await stat(entry.path)).mtimeMs))),
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
return { digest, observedAt, rootId: displaySourcePath };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Produce selected ScriptablePacks through one inventory and one staged owner boundary. */
|
|
321
|
+
export async function produceScriptablePackProducts(
|
|
322
|
+
sources: readonly ScriptablePackInput[],
|
|
323
|
+
declaredExternalOutputs: readonly ScriptablePackStagedOutput[] = [],
|
|
324
|
+
): Promise<Result<ReadonlyMap<string, PreparedScriptablePack>, ScriptablePackHostError>> {
|
|
325
|
+
const entries = new Map<
|
|
326
|
+
string,
|
|
327
|
+
{
|
|
328
|
+
readonly source: ScriptablePackInput;
|
|
329
|
+
readonly definition: Readonly<ScriptablePackDefinition>;
|
|
330
|
+
readonly closure: readonly ScriptablePackSourceClosureEntry[];
|
|
331
|
+
}
|
|
332
|
+
>();
|
|
333
|
+
for (const source of sources) {
|
|
334
|
+
if (entries.has(source.displaySourcePath)) continue;
|
|
335
|
+
entries.set(source.displaySourcePath, {
|
|
336
|
+
source,
|
|
337
|
+
definition: source.definition,
|
|
338
|
+
closure: source.sourceClosure,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const products = new Map<string, ScriptableSourcePackageProduct>();
|
|
343
|
+
const owners: ScriptablePackStagedOwner[] = [...entries.entries()].map(
|
|
344
|
+
([displaySourcePath, entry]) => ({
|
|
345
|
+
id: displaySourcePath,
|
|
346
|
+
guids: Object.values(entry.definition.assets).map((asset) => asset.guid),
|
|
347
|
+
async build(source) {
|
|
348
|
+
const produced = await produceScriptableSourcePackage({
|
|
349
|
+
definition: entry.definition,
|
|
350
|
+
sourcePath: displaySourcePath,
|
|
351
|
+
assetSource: source,
|
|
352
|
+
outputs: createStandardAssetOutputProducerRegistry(
|
|
353
|
+
projectScriptablePackSceneComponents(entry.definition.sceneComponents),
|
|
354
|
+
),
|
|
355
|
+
sourceClosure: entry.closure,
|
|
356
|
+
authoringContractVersion: 'scriptable-pack-production/1',
|
|
357
|
+
});
|
|
358
|
+
if (!produced.ok) return produced;
|
|
359
|
+
const observedClosure = await inventoryScriptablePackSource(entry.source.sourcePath);
|
|
360
|
+
if (JSON.stringify(observedClosure) !== JSON.stringify(entry.closure)) {
|
|
361
|
+
return err({
|
|
362
|
+
code: 'pack-source-load-failed',
|
|
363
|
+
expected: 'the complete ScriptablePack module closure to remain fixed during one build',
|
|
364
|
+
hint: 'retry the build after source and helper writes have settled',
|
|
365
|
+
detail: {
|
|
366
|
+
sourcePath: displaySourcePath,
|
|
367
|
+
reason: 'source-changed',
|
|
368
|
+
phase: 'build',
|
|
369
|
+
diagnostic: 'module closure changed while the staged generation was building',
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
products.set(displaySourcePath, produced.value);
|
|
374
|
+
return ok(produced.value.stagedOutputs);
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
const generationDigest = createHash('sha256')
|
|
379
|
+
.update(
|
|
380
|
+
JSON.stringify(
|
|
381
|
+
[...entries.values()]
|
|
382
|
+
.flatMap((entry) => entry.closure)
|
|
383
|
+
.sort((a, b) => a.path.localeCompare(b.path)),
|
|
384
|
+
),
|
|
385
|
+
)
|
|
386
|
+
.digest();
|
|
387
|
+
const stagedSource = createScriptablePackStagedAssetSnapshotSource({
|
|
388
|
+
generation: generationDigest.readUInt32BE(0),
|
|
389
|
+
owners,
|
|
390
|
+
declaredExternalOutputs,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const prepared = new Map<string, PreparedScriptablePack>();
|
|
394
|
+
for (const source of sources) {
|
|
395
|
+
const entry = entries.get(source.displaySourcePath);
|
|
396
|
+
if (entry === undefined) {
|
|
397
|
+
return err({
|
|
398
|
+
code: 'pack-source-path-invalid',
|
|
399
|
+
expected: 'a ScriptablePack source registered in the current inventory',
|
|
400
|
+
actual: source.displaySourcePath,
|
|
401
|
+
hint: 'rebuild the source inventory before producing this path',
|
|
402
|
+
retryable: true,
|
|
403
|
+
recoveryActions: ['rebuild-source-inventory'],
|
|
404
|
+
detail: { sourcePath: source.displaySourcePath },
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const first = Object.values(entry.definition.assets)[0];
|
|
408
|
+
if (first === undefined) {
|
|
409
|
+
return err({
|
|
410
|
+
code: 'pack-source-output-invalid',
|
|
411
|
+
expected: 'at least one declared ScriptablePack output',
|
|
412
|
+
hint: 'add an output descriptor before building the package',
|
|
413
|
+
detail: { missingGuids: [], unexpectedSourceKeys: [], kindMismatches: [] },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const staged = await stagedSource.readByGuid(first.guid);
|
|
417
|
+
if (!staged.ok) return err(staged.error);
|
|
418
|
+
const produced = products.get(source.displaySourcePath);
|
|
419
|
+
if (produced === undefined) {
|
|
420
|
+
return err({
|
|
421
|
+
code: 'pack-source-output-invalid',
|
|
422
|
+
expected: 'the staged owner build to retain its source-package product',
|
|
423
|
+
hint: 'retry the ScriptablePack production attempt',
|
|
424
|
+
detail: {
|
|
425
|
+
missingGuids: [AssetGuid.format(first.guid)],
|
|
426
|
+
unexpectedSourceKeys: [],
|
|
427
|
+
kindMismatches: [],
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const transportPolicy =
|
|
432
|
+
typeof source.policy === 'function' ? source.policy(produced) : source.policy;
|
|
433
|
+
const logicalPackage = projectImportProductForBuild(produced.product);
|
|
434
|
+
const finalized = await finalizePackageTransportSource(logicalPackage, transportPolicy);
|
|
435
|
+
const facts = projectScriptablePackPublication(produced);
|
|
436
|
+
if (!facts.ok) return err(facts.error);
|
|
437
|
+
const revision = await scriptablePackResourceRevision(
|
|
438
|
+
source.displaySourcePath,
|
|
439
|
+
produced.inputFingerprint,
|
|
440
|
+
source.sourceClosure,
|
|
441
|
+
);
|
|
442
|
+
const publication = createAcceptedPublication({
|
|
443
|
+
sourcePath: source.displaySourcePath,
|
|
444
|
+
sourceRevision: produced.inputFingerprint,
|
|
445
|
+
generation: source.publicationGeneration,
|
|
446
|
+
digest: finalized.digest,
|
|
447
|
+
packageUrl: finalized.packageUrl,
|
|
448
|
+
inputFingerprint: produced.inputFingerprint,
|
|
449
|
+
outputs: facts.value.outputs,
|
|
450
|
+
externalEvidence: produced.externalEvidence,
|
|
451
|
+
});
|
|
452
|
+
prepared.set(source.displaySourcePath, {
|
|
453
|
+
product: produced,
|
|
454
|
+
finalized,
|
|
455
|
+
facts: facts.value,
|
|
456
|
+
revision,
|
|
457
|
+
publication,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return ok(prepared);
|
|
461
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { componentSchema } from '@forgeax/engine-ecs/internal';
|
|
2
1
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
2
|
+
import type { ScriptablePackSceneComponent } from '@forgeax/engine-pack/source';
|
|
3
3
|
import { externalizeSceneAsset } from '@forgeax/engine-scene';
|
|
4
4
|
import type {
|
|
5
5
|
AnimationGraph,
|
|
6
|
-
Asset,
|
|
7
6
|
AssetGuid as AssetGuidType,
|
|
8
7
|
AssetRef,
|
|
9
8
|
AudioClipAsset,
|
|
@@ -16,7 +15,6 @@ import type {
|
|
|
16
15
|
MeshAsset,
|
|
17
16
|
ParticleEffectAsset,
|
|
18
17
|
RenderPipelineAsset,
|
|
19
|
-
Result,
|
|
20
18
|
SamplerAsset,
|
|
21
19
|
SceneAsset,
|
|
22
20
|
SkeletonAsset,
|
|
@@ -154,18 +152,6 @@ function materialProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
|
154
152
|
};
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
export const materialAssetOutputProducer: AssetOutputProducer = {
|
|
158
|
-
kind: 'material',
|
|
159
|
-
version: 'material-pack/1',
|
|
160
|
-
produce(input): Result<AssetOutputProduct, ImportError> {
|
|
161
|
-
try {
|
|
162
|
-
return ok(materialProduct(input));
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return err(producerError(input, error));
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
155
|
function meshProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
170
156
|
if (input.asset.kind !== 'mesh') throw new TypeError('expected MeshAsset');
|
|
171
157
|
const mesh = input.asset as MeshAsset;
|
|
@@ -201,23 +187,19 @@ function meshProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
|
201
187
|
};
|
|
202
188
|
}
|
|
203
189
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
return ok(meshProduct(input));
|
|
210
|
-
} catch (error) {
|
|
211
|
-
return err(producerError(input, error));
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
function sceneProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
190
|
+
function sceneProduct(
|
|
191
|
+
input: AssetOutputInput,
|
|
192
|
+
components: ReadonlyMap<string, Readonly<Record<string, string>>>,
|
|
193
|
+
): AssetOutputProduct {
|
|
217
194
|
if (input.asset.kind !== 'scene') throw new TypeError('expected SceneAsset');
|
|
218
195
|
const externalized = externalizeSceneAsset(input.asset as SceneAsset, (componentName) => {
|
|
219
|
-
const
|
|
220
|
-
|
|
196
|
+
const schema = components.get(componentName);
|
|
197
|
+
if (schema === undefined) {
|
|
198
|
+
throw new TypeError(
|
|
199
|
+
`ScriptablePack scene component ${componentName} is missing from sceneComponents`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return schema;
|
|
221
203
|
});
|
|
222
204
|
if (!externalized.ok) throw new TypeError(`scene field ${externalized.error.field} is invalid`);
|
|
223
205
|
return {
|
|
@@ -227,6 +209,24 @@ function sceneProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
|
227
209
|
};
|
|
228
210
|
}
|
|
229
211
|
|
|
212
|
+
function createSafeProducer(
|
|
213
|
+
kind: AssetOutputProducer['kind'],
|
|
214
|
+
version: string,
|
|
215
|
+
product: (input: AssetOutputInput) => AssetOutputProduct,
|
|
216
|
+
): AssetOutputProducer {
|
|
217
|
+
return {
|
|
218
|
+
kind,
|
|
219
|
+
version,
|
|
220
|
+
produce(input) {
|
|
221
|
+
try {
|
|
222
|
+
return ok(product(input));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return err(producerError(input, error));
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
230
|
function jsonArtifact(value: unknown): ImportedArtifactBody {
|
|
231
231
|
return {
|
|
232
232
|
mediaType: 'application/json',
|
|
@@ -412,39 +412,29 @@ function ordinaryPodProduct(input: AssetOutputInput): AssetOutputProduct {
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
} catch (error) {
|
|
422
|
-
return err(producerError(input, error));
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
};
|
|
415
|
+
export const materialAssetOutputProducer = createSafeProducer(
|
|
416
|
+
'material',
|
|
417
|
+
'material-pack/1',
|
|
418
|
+
materialProduct,
|
|
419
|
+
);
|
|
420
|
+
export const meshAssetOutputProducer = createSafeProducer('mesh', 'mesh-binary/4', meshProduct);
|
|
426
421
|
|
|
427
|
-
function
|
|
428
|
-
|
|
422
|
+
export function createSceneAssetOutputProducer(
|
|
423
|
+
sceneComponents: readonly ScriptablePackSceneComponent[] = [],
|
|
424
|
+
): AssetOutputProducer {
|
|
425
|
+
const schemas = new Map(
|
|
426
|
+
sceneComponents.map((component) => [component.name, component.fields] as const),
|
|
427
|
+
);
|
|
428
|
+
return createSafeProducer('scene', 'scene-pack/3', (input) => sceneProduct(input, schemas));
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
export
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
produce(input): Result<AssetOutputProduct, ImportError> {
|
|
435
|
-
try {
|
|
436
|
-
return ok(sceneProduct(input));
|
|
437
|
-
} catch (error) {
|
|
438
|
-
return err(producerError(input, error));
|
|
439
|
-
}
|
|
440
|
-
},
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
export function createStandardAssetOutputProducerRegistry(): AssetOutputProducerRegistry {
|
|
431
|
+
export function createStandardAssetOutputProducerRegistry(
|
|
432
|
+
sceneComponents: readonly ScriptablePackSceneComponent[] = [],
|
|
433
|
+
): AssetOutputProducerRegistry {
|
|
444
434
|
const registry = new AssetOutputProducerRegistry();
|
|
445
435
|
registry.register(materialAssetOutputProducer);
|
|
446
436
|
registry.register(meshAssetOutputProducer);
|
|
447
|
-
registry.register(
|
|
437
|
+
registry.register(createSceneAssetOutputProducer(sceneComponents));
|
|
448
438
|
for (const kind of [
|
|
449
439
|
'texture',
|
|
450
440
|
'equirect',
|
|
@@ -460,7 +450,7 @@ export function createStandardAssetOutputProducerRegistry(): AssetOutputProducer
|
|
|
460
450
|
'audio',
|
|
461
451
|
'particle-effect',
|
|
462
452
|
] as const) {
|
|
463
|
-
registry.register(
|
|
453
|
+
registry.register(createSafeProducer(kind, 'ordinary-pod/1', ordinaryPodProduct));
|
|
464
454
|
}
|
|
465
455
|
return registry;
|
|
466
456
|
}
|
|
@@ -26,8 +26,7 @@ export interface ScriptablePackStagedOwner {
|
|
|
26
26
|
export interface ScriptablePackStagedSnapshotOptions {
|
|
27
27
|
readonly generation: number;
|
|
28
28
|
readonly owners: readonly ScriptablePackStagedOwner[];
|
|
29
|
-
|
|
30
|
-
readonly fallback?: ScriptablePackAssetSnapshotSource;
|
|
29
|
+
readonly declaredExternalOutputs?: readonly ScriptablePackStagedOutput[];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
function stable(value: unknown): string {
|
|
@@ -81,8 +80,21 @@ function missingOutputError(owner: string, guid: string): ScriptablePackError {
|
|
|
81
80
|
export function createScriptablePackStagedAssetSnapshotSource(
|
|
82
81
|
options: ScriptablePackStagedSnapshotOptions,
|
|
83
82
|
): ScriptablePackAssetSnapshotSource {
|
|
83
|
+
const owners =
|
|
84
|
+
options.declaredExternalOutputs === undefined || options.declaredExternalOutputs.length === 0
|
|
85
|
+
? options.owners
|
|
86
|
+
: [
|
|
87
|
+
{
|
|
88
|
+
id: '<declared-pack-external>',
|
|
89
|
+
guids: options.declaredExternalOutputs.map((output) => output.guid),
|
|
90
|
+
async build() {
|
|
91
|
+
return ok(options.declaredExternalOutputs ?? []);
|
|
92
|
+
},
|
|
93
|
+
} satisfies ScriptablePackStagedOwner,
|
|
94
|
+
...options.owners,
|
|
95
|
+
];
|
|
84
96
|
const ownerByGuid = new Map<string, ScriptablePackStagedOwner>();
|
|
85
|
-
for (const owner of
|
|
97
|
+
for (const owner of owners) {
|
|
86
98
|
if (owner.id.trim().length === 0) throw new TypeError('staged owner id must be non-empty');
|
|
87
99
|
for (const guid of owner.guids) {
|
|
88
100
|
const key = AssetGuid.format(guid).toLowerCase();
|
|
@@ -104,12 +116,11 @@ export function createScriptablePackStagedAssetSnapshotSource(
|
|
|
104
116
|
if (cached !== undefined) return ok(structuredClone(cached));
|
|
105
117
|
const owner = ownerByGuid.get(key);
|
|
106
118
|
if (owner === undefined) {
|
|
107
|
-
if (options.fallback !== undefined) return options.fallback.readByGuid(guid);
|
|
108
119
|
return err(
|
|
109
120
|
new AssetError({
|
|
110
121
|
code: 'asset-not-imported',
|
|
111
|
-
expected: 'a staged local owner
|
|
112
|
-
hint: 'declare the local ScriptablePack output
|
|
122
|
+
expected: 'a staged local owner for the requested GUID',
|
|
123
|
+
hint: 'declare the local ScriptablePack output before rebuilding the generation',
|
|
113
124
|
}),
|
|
114
125
|
);
|
|
115
126
|
}
|