@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.
Files changed (76) hide show
  1. package/README.md +6 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  4. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +2 -0
  6. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
  7. package/dist/browser.d.ts +5 -0
  8. package/dist/browser.d.ts.map +1 -0
  9. package/dist/browser.mjs +782 -0
  10. package/dist/browser.mjs.map +1 -0
  11. package/dist/build-production.d.ts +48 -0
  12. package/dist/build-production.d.ts.map +1 -0
  13. package/dist/catalog-importer-policy.d.ts +13 -0
  14. package/dist/catalog-importer-policy.d.ts.map +1 -0
  15. package/dist/catalog-inventory.d.ts +4 -0
  16. package/dist/catalog-inventory.d.ts.map +1 -0
  17. package/dist/import-product.d.ts +16 -25
  18. package/dist/import-product.d.ts.map +1 -1
  19. package/dist/import-runner.d.ts +6 -3
  20. package/dist/import-runner.d.ts.map +1 -1
  21. package/dist/importer-registry.d.ts +9 -1
  22. package/dist/importer-registry.d.ts.map +1 -1
  23. package/dist/index.d.ts +11 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.mjs +1315 -155
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/mesh-bin.d.ts +1 -1
  28. package/dist/mesh-bin.d.ts.map +1 -1
  29. package/dist/mesh-bin.mjs +182 -0
  30. package/dist/mesh-bin.mjs.map +1 -0
  31. package/dist/pack-projection.d.ts +5 -0
  32. package/dist/pack-projection.d.ts.map +1 -0
  33. package/dist/scriptable-pack-host.d.ts +64 -0
  34. package/dist/scriptable-pack-host.d.ts.map +1 -0
  35. package/dist/scriptable-pack-output-producers.d.ts +3 -2
  36. package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
  37. package/dist/scriptable-pack-staged-snapshot.d.ts +1 -2
  38. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
  39. package/dist/scriptable-pack.d.ts +6 -18
  40. package/dist/scriptable-pack.d.ts.map +1 -1
  41. package/dist/scriptable-source-package.d.ts +14 -0
  42. package/dist/scriptable-source-package.d.ts.map +1 -0
  43. package/dist/source-package-errors.d.ts +52 -0
  44. package/dist/source-package-errors.d.ts.map +1 -0
  45. package/dist/source-package-publication.d.ts +63 -0
  46. package/dist/source-package-publication.d.ts.map +1 -0
  47. package/dist/source-package.d.ts +60 -0
  48. package/dist/source-package.d.ts.map +1 -0
  49. package/package.json +13 -7
  50. package/src/__tests__/import-contract-migration.test.ts +33 -0
  51. package/src/__tests__/import-local-artifacts.test.ts +3 -1
  52. package/src/__tests__/owner-chain.integration.test.ts +37 -0
  53. package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
  54. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +78 -19
  55. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +52 -20
  56. package/src/browser.ts +14 -0
  57. package/src/build-production.ts +487 -0
  58. package/src/catalog-importer-policy.ts +58 -0
  59. package/src/catalog-inventory.ts +24 -0
  60. package/src/import-product.ts +37 -69
  61. package/src/import-runner.ts +36 -7
  62. package/src/importer-registry.ts +24 -1
  63. package/src/index.ts +79 -8
  64. package/src/mesh-bin.ts +2 -2
  65. package/src/pack-projection.ts +21 -0
  66. package/src/scriptable-pack-host.ts +461 -0
  67. package/src/scriptable-pack-output-producers.ts +48 -58
  68. package/src/scriptable-pack-staged-snapshot.ts +17 -6
  69. package/src/scriptable-pack.ts +37 -26
  70. package/src/scriptable-source-package.ts +88 -0
  71. package/src/source-package-errors.ts +201 -0
  72. package/src/source-package-publication.ts +295 -0
  73. package/src/source-package.ts +187 -0
  74. package/dist/__tests__/material-import-product.unit.test.d.ts +0 -2
  75. package/dist/__tests__/material-import-product.unit.test.d.ts.map +0 -1
  76. package/src/__tests__/material-import-product.unit.test.ts +0 -56
@@ -0,0 +1,487 @@
1
+ import { resolve } from 'node:path';
2
+ import {
3
+ type CatalogBuildResult,
4
+ createRuntimePackPublication,
5
+ finalizePackageTransportSource,
6
+ metaPathForGuid,
7
+ projectCookedPackageEntry,
8
+ } from '@forgeax/engine-pack/build';
9
+ import type { NativeCooker } from '@forgeax/engine-pack/native-cooker';
10
+ import type {
11
+ AssetPublicationEnvelope,
12
+ Importer,
13
+ PackIndexEntry,
14
+ ResourceRevision,
15
+ RuntimeAssetBinding,
16
+ } from '@forgeax/engine-types';
17
+ import type { ImportRunnerFs } from './import-runner.js';
18
+ import type { ImporterRegistry } from './importer-registry.js';
19
+ import { projectImportProductForBuild } from './pack-projection.js';
20
+ import {
21
+ canonicalScriptableSourcePath,
22
+ declaredPackExternalOutputs,
23
+ materializePreparedScriptablePack,
24
+ prepareAuthoredPackTransport,
25
+ produceScriptablePackProducts,
26
+ scriptablePackInputs,
27
+ } from './scriptable-pack-host.js';
28
+ import {
29
+ finalizeSourcePackage,
30
+ produceSourcePackage,
31
+ sourcePackageAssetsByGuid,
32
+ } from './source-package.js';
33
+
34
+ export interface BuildProductionFailure {
35
+ readonly code: string;
36
+ readonly expected: string;
37
+ readonly hint: string;
38
+ readonly detail?: unknown;
39
+ readonly cause?: unknown;
40
+ }
41
+
42
+ export interface BuildProductionFile {
43
+ readonly type: 'asset';
44
+ readonly fileName?: string;
45
+ readonly name?: string;
46
+ readonly originalFileName?: string;
47
+ readonly source: string | Uint8Array;
48
+ }
49
+
50
+ export interface BuildProductionSink {
51
+ emitFile(file: BuildProductionFile): string;
52
+ getFileName(referenceId: string): string;
53
+ fileUrl(fileName: string): string;
54
+ }
55
+
56
+ export interface BuildProductionOptions {
57
+ readonly inventory: CatalogBuildResult;
58
+ readonly cwd: string;
59
+ readonly basePrefix: string;
60
+ readonly generation: number;
61
+ readonly cookers: readonly NativeCooker[];
62
+ readonly importerRegistry: ImporterRegistry;
63
+ readonly fsForImport: ImportRunnerFs;
64
+ readonly cookedCurrentProjection: Record<string, unknown>;
65
+ readonly directCurrentProjection: Record<string, unknown>;
66
+ readonly authoredCookedCurrentProjection: Record<string, unknown>;
67
+ readonly runtimeBinding?: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'>;
68
+ readonly sink: BuildProductionSink;
69
+ readonly fail: (failure: BuildProductionFailure) => Error;
70
+ }
71
+
72
+ export interface ScriptableBuildPackage {
73
+ readonly packageUrl: string;
74
+ readonly receiptUrls: ReadonlyMap<string, string>;
75
+ readonly refs: ReadonlyMap<string, readonly string[]>;
76
+ readonly revision: ResourceRevision;
77
+ readonly publication: AssetPublicationEnvelope;
78
+ }
79
+
80
+ function scriptableArtifactPath(guid: string, key: string): string {
81
+ return `${guid.toLowerCase()}/${key.includes('.') ? key : `${key}.bin`}`;
82
+ }
83
+
84
+ function runtimePublicationFor(
85
+ context: BuildProductionOptions,
86
+ input: {
87
+ readonly pack: Parameters<typeof createRuntimePackPublication>[0]['pack'];
88
+ readonly sourcePath: string;
89
+ readonly sourceRevision: string;
90
+ readonly packageUrl: string;
91
+ readonly digest?: string;
92
+ },
93
+ ) {
94
+ return createRuntimePackPublication({
95
+ pack: input.pack,
96
+ scopeId: context.runtimeBinding?.scopeId ?? 'asset-runtime',
97
+ sourcePath: input.sourcePath,
98
+ sourceRevision: input.sourceRevision,
99
+ packageUrl: input.packageUrl,
100
+ ...(input.digest === undefined ? {} : { digest: input.digest }),
101
+ generation: context.runtimeBinding?.generation ?? context.generation,
102
+ });
103
+ }
104
+
105
+ async function buildAuthoredPackages(
106
+ context: BuildProductionOptions,
107
+ ): Promise<ReadonlyMap<string, ScriptableBuildPackage>> {
108
+ const scriptableSources = [
109
+ ...new Set(
110
+ context.inventory.entries
111
+ .filter((entry) => entry.sourcePath.endsWith('.pack.ts'))
112
+ .map((entry) => entry.sourcePath),
113
+ ),
114
+ ];
115
+ if (scriptableSources.length === 0) return new Map();
116
+ const inputs = scriptablePackInputs(
117
+ scriptableSources,
118
+ context.inventory.sourceDeclarations,
119
+ context.cwd,
120
+ () => context.generation,
121
+ () => (product) => ({
122
+ base: context.basePrefix === '' ? '/' : context.basePrefix,
123
+ packagePath: `assets/${product.anchorGuid}.pack.json`,
124
+ artifactPath: scriptableArtifactPath,
125
+ }),
126
+ );
127
+ const preparedResult = await produceScriptablePackProducts(
128
+ inputs,
129
+ await declaredPackExternalOutputs(
130
+ context.inventory.sourceDeclarations,
131
+ context.cookers,
132
+ inputs.flatMap((input) => Object.values(input.definition.externalAssets)),
133
+ ),
134
+ );
135
+ if (!preparedResult.ok) throw context.fail(preparedResult.error);
136
+
137
+ const result = new Map<string, ScriptableBuildPackage>();
138
+ for (const entry of context.inventory.entries) {
139
+ if (!entry.sourcePath.endsWith('.pack.ts') || result.has(entry.sourcePath)) continue;
140
+ const prepared = preparedResult.value.get(canonicalScriptableSourcePath(entry.sourcePath));
141
+ if (prepared === undefined) {
142
+ throw context.fail({
143
+ code: 'catalog-declaration-missing',
144
+ expected: 'the ScriptablePack inventory to retain every authored Pack source',
145
+ hint: 'rerun the source inventory and rebuild from the accepted declaration set',
146
+ detail: { stage: 'scan', sourcePath: entry.sourcePath },
147
+ });
148
+ }
149
+ const { product, finalized, facts, revision, publication } = prepared;
150
+ const packagePath = `assets/${product.anchorGuid}.pack.json`;
151
+ const runtimePublication = createRuntimePackPublication({
152
+ pack: { assets: finalized.pack.assets },
153
+ scopeId: context.runtimeBinding?.scopeId ?? 'asset-runtime',
154
+ sourcePath: publication.sourcePath,
155
+ sourceRevision: publication.sourceRevision,
156
+ packageUrl: finalized.packageUrl,
157
+ inputFingerprint: publication.receipt.inputFingerprint,
158
+ digest: finalized.digest,
159
+ generation: context.runtimeBinding?.generation ?? publication.generation,
160
+ outputs: facts.outputs,
161
+ externalEvidence: publication.externalEvidence,
162
+ });
163
+ const receiptPaths = await materializePreparedScriptablePack(
164
+ prepared,
165
+ {
166
+ packagePath,
167
+ artifactPath: (path) => `assets/${path}`,
168
+ receiptPath: (guid) => `assets/${guid}.receipt.json`,
169
+ },
170
+ {
171
+ writePackage: (path) => {
172
+ context.sink.emitFile({
173
+ type: 'asset',
174
+ fileName: path,
175
+ originalFileName: `${context.cwd}/${entry.sourcePath}`,
176
+ source: JSON.stringify(runtimePublication.pack),
177
+ });
178
+ },
179
+ writeArtifact: (path, bytes) => {
180
+ context.sink.emitFile({ type: 'asset', fileName: path, source: bytes });
181
+ },
182
+ writeReceipt: (path, source) => {
183
+ context.sink.emitFile({ type: 'asset', fileName: path, source });
184
+ },
185
+ },
186
+ );
187
+ result.set(entry.sourcePath, {
188
+ packageUrl: finalized.packageUrl,
189
+ receiptUrls: new Map(
190
+ [...receiptPaths].map(([guid, path]) => [guid, context.sink.fileUrl(path)]),
191
+ ),
192
+ revision,
193
+ publication: runtimePublication.publication,
194
+ refs: facts.refs,
195
+ });
196
+ }
197
+ return result;
198
+ }
199
+
200
+ function updateImportedEntries(
201
+ entries: PackIndexEntry[],
202
+ guids: readonly string[],
203
+ packageUrl: string,
204
+ projection: Record<string, unknown> = {},
205
+ refsByGuid?: ReadonlyMap<string, readonly string[]>,
206
+ publication?: AssetPublicationEnvelope,
207
+ ): void {
208
+ const selected = new Set(guids.map((guid) => guid.toLowerCase()));
209
+ for (let index = 0; index < entries.length; index += 1) {
210
+ const entry = entries[index];
211
+ if (entry === undefined || !selected.has(entry.guid.toLowerCase())) continue;
212
+ const refs = refsByGuid?.get(entry.guid.toLowerCase());
213
+ entries[index] = {
214
+ ...entry,
215
+ packageUrl,
216
+ ...projection,
217
+ ...(publication === undefined ? {} : { publication }),
218
+ ...(refs === undefined ? {} : { refs }),
219
+ };
220
+ }
221
+ }
222
+
223
+ interface BuildEmissionWork {
224
+ readonly importedEntries: PackIndexEntry[];
225
+ readonly context: BuildProductionOptions;
226
+ }
227
+
228
+ interface PackEmissionOptions {
229
+ readonly pack: unknown;
230
+ readonly artifacts: readonly { readonly path: string; readonly bytes: Uint8Array }[];
231
+ readonly packageName: string;
232
+ readonly originalFileName: string;
233
+ readonly guids: readonly string[];
234
+ readonly projection?: Record<string, unknown>;
235
+ readonly refsByGuid?: ReadonlyMap<string, readonly string[]>;
236
+ readonly publication?: AssetPublicationEnvelope;
237
+ }
238
+
239
+ async function emitPackDocument(
240
+ work: BuildEmissionWork,
241
+ options: PackEmissionOptions,
242
+ ): Promise<string> {
243
+ for (const artifact of options.artifacts) {
244
+ work.context.sink.emitFile({
245
+ type: 'asset',
246
+ fileName: `assets/${artifact.path}`,
247
+ source: artifact.bytes,
248
+ });
249
+ }
250
+ const referenceId = work.context.sink.emitFile({
251
+ type: 'asset',
252
+ name: options.packageName,
253
+ originalFileName: options.originalFileName,
254
+ source: JSON.stringify(options.pack),
255
+ });
256
+ const packageUrl = work.context.sink.fileUrl(work.context.sink.getFileName(referenceId));
257
+ updateImportedEntries(
258
+ work.importedEntries,
259
+ options.guids,
260
+ packageUrl,
261
+ options.projection,
262
+ options.refsByGuid,
263
+ options.publication,
264
+ );
265
+ return packageUrl;
266
+ }
267
+
268
+ async function emitAuthoredPack(
269
+ work: BuildEmissionWork,
270
+ guidSeen: Set<string>,
271
+ entry: PackIndexEntry,
272
+ ): Promise<void> {
273
+ const packPath = resolve(work.context.cwd, entry.sourcePath);
274
+ const declaration = work.context.inventory.sourceDeclarations.get(packPath);
275
+ if (declaration?.format !== 'pack.json') {
276
+ throw work.context.fail({
277
+ code: 'catalog-declaration-missing',
278
+ expected: 'the inventory to retain the authored Pack declaration',
279
+ hint: 'rerun Pack inventory before emitting the authored Pack',
280
+ detail: { stage: 'scan', sourcePath: packPath },
281
+ });
282
+ }
283
+ const prepared = await prepareAuthoredPackTransport(
284
+ declaration.value,
285
+ work.context.cookers,
286
+ (guid) => ({
287
+ base: work.context.basePrefix === '' ? '/' : work.context.basePrefix,
288
+ packagePath: `assets/${guid}.pack.json`,
289
+ artifactPath: (artifactGuid, key) => `${artifactGuid}/${key}.bin`,
290
+ }),
291
+ packPath,
292
+ );
293
+ const outputGuid = prepared.firstGuid ?? entry.guid;
294
+ const authoredPack = prepared.finalized?.pack ?? prepared.pack;
295
+ const runtimePublication =
296
+ authoredPack.assets === undefined || authoredPack.assets.length === 0
297
+ ? undefined
298
+ : runtimePublicationFor(work.context, {
299
+ pack: { assets: authoredPack.assets },
300
+ sourcePath: packPath,
301
+ sourceRevision: prepared.finalized?.sourceRevision ?? entry.sourcePath,
302
+ packageUrl:
303
+ prepared.finalized?.packageUrl ??
304
+ `${work.context.basePrefix === '/' ? '' : work.context.basePrefix}/assets/${outputGuid}.pack.json`,
305
+ ...(prepared.finalized?.digest === undefined
306
+ ? {}
307
+ : { digest: prepared.finalized.digest }),
308
+ });
309
+ await emitPackDocument(work, {
310
+ pack: runtimePublication?.pack ?? authoredPack,
311
+ artifacts: prepared.finalized?.artifacts ?? [],
312
+ packageName: `${outputGuid}.pack.json`,
313
+ originalFileName: packPath,
314
+ guids: prepared.pack.assets?.map((asset) => asset.guid) ?? [outputGuid],
315
+ projection:
316
+ prepared.finalized === undefined
317
+ ? work.context.directCurrentProjection
318
+ : work.context.authoredCookedCurrentProjection,
319
+ ...(prepared.cooked === undefined ? {} : { refsByGuid: prepared.cooked.refsByGuid }),
320
+ ...(runtimePublication === undefined ? {} : { publication: runtimePublication.publication }),
321
+ });
322
+ for (const guid of prepared.pack.assets?.map((asset) => asset.guid) ?? [outputGuid]) {
323
+ guidSeen.add(guid.toLowerCase());
324
+ }
325
+ }
326
+
327
+ async function emitFinalizedOwner(
328
+ work: BuildEmissionWork,
329
+ metaPath: string,
330
+ subAssets: readonly { readonly guid: string }[],
331
+ sourcePackage: Extract<
332
+ Awaited<ReturnType<typeof produceSourcePackage>>,
333
+ { readonly ok: true }
334
+ >['value'],
335
+ ownerFinalizer: NonNullable<Importer['finalize']>,
336
+ ): Promise<void> {
337
+ const ownerGuid = subAssets[0]?.guid;
338
+ if (ownerGuid === undefined) return;
339
+ const artifactPaths = new Map<string, string>();
340
+ const sourceAsset = sourcePackage.product.assets[0];
341
+ for (const [path, artifact] of Object.entries(sourceAsset?.artifacts ?? {})) {
342
+ const ref = work.context.sink.emitFile({
343
+ type: 'asset',
344
+ name: path,
345
+ originalFileName: path,
346
+ source: artifact.bytes,
347
+ });
348
+ artifactPaths.set(path, work.context.sink.getFileName(ref));
349
+ }
350
+ const ownerProduct = finalizeSourcePackage(sourcePackage.product, ownerFinalizer, (artifact) =>
351
+ work.context.sink.fileUrl(artifactPaths.get(artifact.path) ?? artifact.path),
352
+ );
353
+ if (!ownerProduct.ok) throw work.context.fail(ownerProduct.error);
354
+ const ownerPackage = await finalizePackageTransportSource(
355
+ projectImportProductForBuild(ownerProduct.value),
356
+ {
357
+ base: work.context.basePrefix,
358
+ packagePath: `assets/${ownerGuid}.pack.json`,
359
+ artifactPath: (_guid, key) => {
360
+ const emittedPath = artifactPaths.get(key);
361
+ if (emittedPath === undefined) {
362
+ throw work.context.fail({
363
+ code: 'producer-artifact-missing',
364
+ expected: `producer artifact ${key} to be emitted before Pack finalization`,
365
+ hint: 'repair the importer finalizer artifact closure and rebuild',
366
+ detail: { stage: 'finalize', metaPath, artifact: key },
367
+ });
368
+ }
369
+ return emittedPath.replace(/^assets\//, '');
370
+ },
371
+ sink: () => {},
372
+ },
373
+ );
374
+ const runtimePublication = runtimePublicationFor(work.context, {
375
+ pack: { assets: ownerPackage.pack.assets },
376
+ sourcePath: metaPath,
377
+ sourceRevision: ownerPackage.sourceRevision,
378
+ packageUrl: ownerPackage.packageUrl,
379
+ digest: ownerPackage.digest,
380
+ });
381
+ await emitPackDocument(work, {
382
+ pack: runtimePublication.pack,
383
+ artifacts: ownerPackage.artifacts,
384
+ packageName: `${ownerGuid}.pack.json`,
385
+ originalFileName: metaPath,
386
+ guids: subAssets.map((sub) => sub.guid),
387
+ projection: work.context.cookedCurrentProjection,
388
+ refsByGuid: new Map(
389
+ ownerProduct.value.assets.map((asset) => [
390
+ asset.guid.toLowerCase(),
391
+ asset.refs.map((ref) => ref.guid),
392
+ ]),
393
+ ),
394
+ publication: runtimePublication.publication,
395
+ });
396
+ }
397
+
398
+ async function emitBuildEntry(
399
+ work: BuildEmissionWork,
400
+ guidSeen: Set<string>,
401
+ entry: PackIndexEntry,
402
+ ): Promise<void> {
403
+ const guid = entry.guid.toLowerCase();
404
+ const metaPath = metaPathForGuid(work.context.inventory.declarations, guid);
405
+ if (metaPath === undefined) {
406
+ if (entry.sourcePath.endsWith('.pack.json')) await emitAuthoredPack(work, guidSeen, entry);
407
+ else guidSeen.add(guid);
408
+ return;
409
+ }
410
+ const declaration = work.context.inventory.declarations.get(metaPath);
411
+ if (declaration === undefined) {
412
+ throw work.context.fail({
413
+ code: 'catalog-declaration-missing',
414
+ expected: 'every indexed Meta path to have one validated producer declaration',
415
+ hint: 'rerun the inventory and repair the Catalog declaration index',
416
+ detail: { metaPath },
417
+ });
418
+ }
419
+ const runMeta = { ...declaration, buildPack: false };
420
+ const subAssets: readonly { readonly guid: string }[] = runMeta.subAssets;
421
+ for (const sub of subAssets) guidSeen.add(sub.guid.toLowerCase());
422
+ const sourcePackage = await produceSourcePackage({
423
+ meta: runMeta,
424
+ registry: work.context.importerRegistry,
425
+ fs: work.context.fsForImport,
426
+ });
427
+ if (!sourcePackage.ok) throw work.context.fail(sourcePackage.error);
428
+ const ownerFinalizer = work.context.importerRegistry.get(runMeta.importer)?.finalize;
429
+ if (ownerFinalizer !== undefined) {
430
+ await emitFinalizedOwner(work, metaPath, subAssets, sourcePackage.value, ownerFinalizer);
431
+ return;
432
+ }
433
+ const finalized = await finalizePackageTransportSource(
434
+ projectImportProductForBuild(sourcePackage.value.product),
435
+ {
436
+ base: work.context.basePrefix,
437
+ packagePath: `assets/${subAssets[0]?.guid ?? 'pack'}.pack.json`,
438
+ artifactPath: (assetGuid, key) => `${assetGuid}-${key}.bin`,
439
+ sink: () => {},
440
+ },
441
+ );
442
+ const productByGuid = sourcePackageAssetsByGuid(sourcePackage.value);
443
+ const runtimePublication = runtimePublicationFor(work.context, {
444
+ pack: { assets: finalized.pack.assets },
445
+ sourcePath: metaPath,
446
+ sourceRevision: finalized.sourceRevision,
447
+ packageUrl: finalized.packageUrl,
448
+ digest: finalized.digest,
449
+ });
450
+ await emitPackDocument(work, {
451
+ pack: runtimePublication.pack,
452
+ artifacts: finalized.artifacts,
453
+ packageName: `${subAssets[0]?.guid ?? 'pack'}.pack.json`,
454
+ originalFileName: metaPath,
455
+ guids: subAssets.map((sub) => sub.guid),
456
+ projection: work.context.cookedCurrentProjection,
457
+ refsByGuid: new Map(
458
+ [...productByGuid].map(([guid, asset]) => [guid, asset.refs.map((ref) => ref.guid)]),
459
+ ),
460
+ publication: runtimePublication.publication,
461
+ });
462
+ }
463
+
464
+ export async function produceBuildAssets(
465
+ context: BuildProductionOptions,
466
+ ): Promise<PackIndexEntry[]> {
467
+ const entries = [...context.inventory.entries];
468
+ const authored = await buildAuthoredPackages(context);
469
+ const importedEntries = entries.map((entry) => {
470
+ const scriptable = authored.get(entry.sourcePath);
471
+ if (scriptable === undefined) return entry;
472
+ const cookReceiptUrl = scriptable.receiptUrls.get(entry.guid.toLowerCase());
473
+ return projectCookedPackageEntry(entry, {
474
+ packageUrl: scriptable.packageUrl,
475
+ revision: scriptable.revision,
476
+ refs: scriptable.refs.get(entry.guid.toLowerCase()) ?? [],
477
+ ...(cookReceiptUrl === undefined ? {} : { cookReceiptUrl }),
478
+ publication: scriptable.publication,
479
+ });
480
+ });
481
+ const guidSeen = new Set<string>();
482
+ const work: BuildEmissionWork = { importedEntries, context };
483
+ for (const entry of importedEntries) {
484
+ if (!guidSeen.has(entry.guid.toLowerCase())) await emitBuildEntry(work, guidSeen, entry);
485
+ }
486
+ return importedEntries;
487
+ }
@@ -0,0 +1,58 @@
1
+ import type { CatalogImporterPolicy } from '@forgeax/engine-pack/build';
2
+ import { SHADER_RESERVED_IMPORTER_KEY } from './import-runner.js';
3
+
4
+ export type { CatalogImporterPolicy } from '@forgeax/engine-pack/build';
5
+
6
+ /** Provider keys whose external declarations have a standard Catalog route. */
7
+ export const DEFAULT_CATALOG_IMPORTER_KEYS = [
8
+ 'image',
9
+ 'gltf',
10
+ 'fbx',
11
+ 'audio',
12
+ 'font',
13
+ 'ui',
14
+ ] as const;
15
+
16
+ export type CatalogImporterDisposition = 'publish' | 'exclude' | 'missing';
17
+
18
+ /**
19
+ * Resolve Catalog visibility from provider registration, not from a Vite
20
+ * adapter's importer list. The shader transform is deliberately excluded;
21
+ * every other provider must either be a standard provider or be registered by
22
+ * the host before its declarations can become authoritative.
23
+ */
24
+ export function catalogImporterPolicy(
25
+ importer: string,
26
+ hostImporterKeys: ReadonlySet<string>,
27
+ ): CatalogImporterPolicy {
28
+ if (importer === SHADER_RESERVED_IMPORTER_KEY) {
29
+ return {
30
+ disposition: 'exclude',
31
+ hostProvided: false,
32
+ expected: 'the provider must publish cooked runtime output before Catalog projection',
33
+ hint: 'remove the source declaration from Pack roots or publish its cooked runtime module',
34
+ };
35
+ }
36
+ if ((DEFAULT_CATALOG_IMPORTER_KEYS as readonly string[]).includes(importer)) {
37
+ return {
38
+ disposition: 'publish',
39
+ hostProvided: false,
40
+ expected: 'a standard Catalog provider declaration',
41
+ hint: 'let the provider producer materialize the declared output',
42
+ };
43
+ }
44
+ if (hostImporterKeys.has(importer)) {
45
+ return {
46
+ disposition: 'publish',
47
+ hostProvided: true,
48
+ expected: 'a registered host Catalog provider declaration',
49
+ hint: 'let the registered provider materialize the declared output',
50
+ };
51
+ }
52
+ return {
53
+ disposition: 'missing',
54
+ hostProvided: false,
55
+ expected: 'the sidecar importer must be present in the registered provider set',
56
+ hint: 'wire the provider through the host importer registry before building the Catalog',
57
+ };
58
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ buildCatalogProjection,
3
+ type CatalogBuildProjectionOptions,
4
+ type CatalogBuildResult,
5
+ type CatalogProducerVisibility,
6
+ } from '@forgeax/engine-pack/build';
7
+ import type { ScanOptions } from '@forgeax/engine-pack/scanner';
8
+ import { catalogImporterPolicy } from './catalog-importer-policy.js';
9
+
10
+ export async function buildCatalogResult(
11
+ roots: readonly string[],
12
+ base = '/',
13
+ registeredImporterKeys: ReadonlySet<string> = new Set(),
14
+ scanOptions: ScanOptions = {},
15
+ catalogVisibility: CatalogProducerVisibility = () => true,
16
+ ): Promise<CatalogBuildResult> {
17
+ const options: CatalogBuildProjectionOptions = {
18
+ base,
19
+ scanOptions,
20
+ importerPolicy: (importer) => catalogImporterPolicy(importer, registeredImporterKeys),
21
+ visibility: catalogVisibility,
22
+ };
23
+ return buildCatalogProjection(roots, options);
24
+ }