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