@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,295 @@
|
|
|
1
|
+
import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
type DdcEntry,
|
|
5
|
+
type DdcGenerationEntryCandidate,
|
|
6
|
+
DdcGenerationSession,
|
|
7
|
+
type DdcHead,
|
|
8
|
+
DdcLifecycle,
|
|
9
|
+
ddcOutputDigest,
|
|
10
|
+
} from '@forgeax/engine-ddc';
|
|
11
|
+
import {
|
|
12
|
+
type CatalogEntry,
|
|
13
|
+
err,
|
|
14
|
+
ok,
|
|
15
|
+
type ResourceRevision,
|
|
16
|
+
type Result,
|
|
17
|
+
} from '@forgeax/engine-types';
|
|
18
|
+
import {
|
|
19
|
+
type SourcePackageError,
|
|
20
|
+
type SourcePackageErrorContext,
|
|
21
|
+
sourcePackageError,
|
|
22
|
+
} from './source-package-errors.js';
|
|
23
|
+
|
|
24
|
+
interface SourcePackageDdcEntryInput {
|
|
25
|
+
readonly root: string;
|
|
26
|
+
readonly entry: DdcEntry;
|
|
27
|
+
readonly context: SourcePackageErrorContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StagedSourcePackageDdc extends DdcGenerationEntryCandidate {
|
|
31
|
+
readonly root: string;
|
|
32
|
+
readonly session: DdcGenerationSession;
|
|
33
|
+
readonly context: SourcePackageErrorContext;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ImportPublicationInput {
|
|
37
|
+
readonly root: string;
|
|
38
|
+
readonly guid: string;
|
|
39
|
+
readonly desiredKey: string;
|
|
40
|
+
readonly pack: unknown;
|
|
41
|
+
readonly previousCatalog: readonly CatalogEntry[];
|
|
42
|
+
readonly nextCatalog: readonly CatalogEntry[];
|
|
43
|
+
readonly publishedGuids: readonly string[];
|
|
44
|
+
readonly transport?: {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly body: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ImportPublicationError {
|
|
51
|
+
readonly code: SourcePackageError['code'];
|
|
52
|
+
readonly expected: string;
|
|
53
|
+
readonly hint: string;
|
|
54
|
+
readonly detail: string;
|
|
55
|
+
readonly diagnostic: SourcePackageError;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ImportPublicationResult =
|
|
59
|
+
| {
|
|
60
|
+
readonly ok: true;
|
|
61
|
+
readonly key: string;
|
|
62
|
+
readonly head: DdcHead;
|
|
63
|
+
readonly catalog: readonly CatalogEntry[];
|
|
64
|
+
readonly revision: ResourceRevision;
|
|
65
|
+
readonly transportPersisted: boolean;
|
|
66
|
+
}
|
|
67
|
+
| { readonly ok: false; readonly error: ImportPublicationError; readonly head: DdcHead };
|
|
68
|
+
|
|
69
|
+
export interface StagedImportPublication {
|
|
70
|
+
readonly input: ImportPublicationInput;
|
|
71
|
+
readonly ddc: StagedSourcePackageDdc;
|
|
72
|
+
readonly key: string;
|
|
73
|
+
readonly head: DdcHead;
|
|
74
|
+
readonly catalog: readonly CatalogEntry[];
|
|
75
|
+
readonly revision: ResourceRevision;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type StagedImportPublicationResult =
|
|
79
|
+
| { readonly ok: true; readonly candidate: StagedImportPublication }
|
|
80
|
+
| { readonly ok: false; readonly error: ImportPublicationError; readonly head: DdcHead };
|
|
81
|
+
|
|
82
|
+
function inspectHead(root: string, guid: string, desiredKey: string): Promise<DdcHead> {
|
|
83
|
+
return new DdcLifecycle(root).inspect(guid, desiredKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function stageSourcePackageDdc(
|
|
87
|
+
input: SourcePackageDdcEntryInput,
|
|
88
|
+
): Promise<Result<StagedSourcePackageDdc, SourcePackageError>> {
|
|
89
|
+
const session = new DdcGenerationSession(input.root, { generation: 1 });
|
|
90
|
+
try {
|
|
91
|
+
const candidate = await session.stageEntry(input.entry);
|
|
92
|
+
return ok({ ...candidate, root: input.root, session, context: input.context });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return err(
|
|
95
|
+
sourcePackageError('source-package-ddc-failed', input.context, {
|
|
96
|
+
stage: 'ddc',
|
|
97
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function commitSourcePackageDdc(
|
|
104
|
+
candidate: StagedSourcePackageDdc,
|
|
105
|
+
): Promise<Result<DdcHead, SourcePackageError>> {
|
|
106
|
+
try {
|
|
107
|
+
const commit = await candidate.session.commitEntry(candidate, candidate.lease.desiredKey);
|
|
108
|
+
if (commit.result !== 'current') {
|
|
109
|
+
return err(
|
|
110
|
+
sourcePackageError('source-package-ddc-failed', candidate.context, {
|
|
111
|
+
stage: 'ddc',
|
|
112
|
+
reason: `DDC lifecycle commit returned ${commit.result}`,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return ok(await inspectHead(candidate.root, candidate.lease.guid, candidate.lease.desiredKey));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return err(
|
|
119
|
+
sourcePackageError('source-package-ddc-failed', candidate.context, {
|
|
120
|
+
stage: 'ddc',
|
|
121
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function persistTransport(transport: ImportPublicationInput['transport']): Promise<boolean> {
|
|
128
|
+
if (transport === undefined) return true;
|
|
129
|
+
const directory = dirname(transport.path);
|
|
130
|
+
const temporary = `${transport.path}.tmp`;
|
|
131
|
+
try {
|
|
132
|
+
await mkdir(directory, { recursive: true });
|
|
133
|
+
if (((await stat(directory)).mode & 0o222) === 0) {
|
|
134
|
+
throw new Error('transport directory is read-only');
|
|
135
|
+
}
|
|
136
|
+
await writeFile(temporary, transport.body);
|
|
137
|
+
await rename(temporary, transport.path);
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function importPublicationFailure(error: SourcePackageError): ImportPublicationError {
|
|
146
|
+
return {
|
|
147
|
+
code: error.code,
|
|
148
|
+
expected: error.expected,
|
|
149
|
+
hint: error.hint,
|
|
150
|
+
detail: error.detail.reason ?? error.detail.stage,
|
|
151
|
+
diagnostic: error,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function projectImportPublication(
|
|
156
|
+
input: ImportPublicationInput,
|
|
157
|
+
head: DdcHead,
|
|
158
|
+
observedAt: number,
|
|
159
|
+
): { readonly catalog: readonly CatalogEntry[]; readonly revision: ResourceRevision } {
|
|
160
|
+
const digest = head.currentKey ?? input.desiredKey;
|
|
161
|
+
const revision: ResourceRevision = { digest, observedAt, rootId: input.root };
|
|
162
|
+
const published = new Set(input.publishedGuids.map((guid) => guid.toLowerCase()));
|
|
163
|
+
const catalog = input.nextCatalog.map((row) => {
|
|
164
|
+
if (!published.has(row.guid.toLowerCase())) return row;
|
|
165
|
+
const previousLkg =
|
|
166
|
+
head.lastKnownGoodKey === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: input.previousCatalog.find(
|
|
169
|
+
(candidate) =>
|
|
170
|
+
candidate.guid.toLowerCase() === row.guid.toLowerCase() &&
|
|
171
|
+
candidate.revision?.digest === head.lastKnownGoodKey,
|
|
172
|
+
);
|
|
173
|
+
const projection =
|
|
174
|
+
row.projection === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: (() => {
|
|
177
|
+
const { lastKnownGood: _staleLastKnownGood, ...withoutLastKnownGood } = row.projection;
|
|
178
|
+
return previousLkg?.packageUrl === undefined
|
|
179
|
+
? withoutLastKnownGood
|
|
180
|
+
: { ...withoutLastKnownGood, lastKnownGood: { packageUrl: previousLkg.packageUrl } };
|
|
181
|
+
})();
|
|
182
|
+
return {
|
|
183
|
+
...row,
|
|
184
|
+
revision,
|
|
185
|
+
...(projection === undefined ? {} : { projection }),
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
return { catalog, revision };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function publishImportPublication(
|
|
192
|
+
input: ImportPublicationInput,
|
|
193
|
+
): Promise<ImportPublicationResult> {
|
|
194
|
+
const staged = await stageImportPublication(input);
|
|
195
|
+
if (!staged.ok) return staged;
|
|
196
|
+
return commitImportPublication(staged.candidate);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function stageImportPublication(
|
|
200
|
+
input: ImportPublicationInput,
|
|
201
|
+
): Promise<StagedImportPublicationResult> {
|
|
202
|
+
const staged = await stageSourcePackageDdc({
|
|
203
|
+
root: input.root,
|
|
204
|
+
entry: {
|
|
205
|
+
key: input.desiredKey,
|
|
206
|
+
guid: input.guid,
|
|
207
|
+
payload: input.pack,
|
|
208
|
+
refs: [],
|
|
209
|
+
artifacts: {},
|
|
210
|
+
receipt: {
|
|
211
|
+
guid: input.guid,
|
|
212
|
+
key: input.desiredKey,
|
|
213
|
+
producer: 'engine-import/source-package-publication',
|
|
214
|
+
inputFingerprint: input.desiredKey,
|
|
215
|
+
outputDigest: ddcOutputDigest({
|
|
216
|
+
guid: input.guid,
|
|
217
|
+
payload: input.pack,
|
|
218
|
+
refs: [],
|
|
219
|
+
artifacts: {},
|
|
220
|
+
}),
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
context: {
|
|
224
|
+
sourceMeta: '<import-publication>',
|
|
225
|
+
anchorGuid: input.guid,
|
|
226
|
+
affectedGuids: input.publishedGuids,
|
|
227
|
+
producer: 'source-package/import-publication',
|
|
228
|
+
importer: 'import-publication',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
if (!staged.ok) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
error: importPublicationFailure(staged.error),
|
|
235
|
+
head: await inspectHead(input.root, input.guid, input.desiredKey),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const lastKnownGoodKey =
|
|
239
|
+
staged.value.previousHead.currentKey !== undefined &&
|
|
240
|
+
staged.value.previousHead.currentKey !== input.desiredKey
|
|
241
|
+
? staged.value.previousHead.currentKey
|
|
242
|
+
: staged.value.previousHead.lastKnownGoodKey;
|
|
243
|
+
const projectedHead: DdcHead = {
|
|
244
|
+
...staged.value.previousHead,
|
|
245
|
+
state: 'cooking',
|
|
246
|
+
currentKey: input.desiredKey,
|
|
247
|
+
...(lastKnownGoodKey === undefined ? {} : { lastKnownGoodKey }),
|
|
248
|
+
};
|
|
249
|
+
const projected = projectImportPublication(input, projectedHead, Date.now());
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
candidate: {
|
|
253
|
+
input,
|
|
254
|
+
ddc: staged.value,
|
|
255
|
+
key: input.desiredKey,
|
|
256
|
+
head: projectedHead,
|
|
257
|
+
catalog: projected.catalog,
|
|
258
|
+
revision: projected.revision,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function commitImportPublication(
|
|
264
|
+
candidate: StagedImportPublication,
|
|
265
|
+
): Promise<ImportPublicationResult> {
|
|
266
|
+
const publication = await commitSourcePackageDdc(candidate.ddc);
|
|
267
|
+
if (!publication.ok) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: importPublicationFailure(publication.error),
|
|
271
|
+
head: await inspectHead(
|
|
272
|
+
candidate.input.root,
|
|
273
|
+
candidate.input.guid,
|
|
274
|
+
candidate.input.desiredKey,
|
|
275
|
+
),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const projected = projectImportPublication(candidate.input, publication.value, Date.now());
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
key: candidate.input.desiredKey,
|
|
282
|
+
head: publication.value,
|
|
283
|
+
catalog: projected.catalog,
|
|
284
|
+
revision: projected.revision,
|
|
285
|
+
transportPersisted: await persistTransport(candidate.input.transport),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function discardImportPublication(candidate: StagedImportPublication): Promise<void> {
|
|
290
|
+
await candidate.ddc.session.discardEntry(candidate.ddc);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function restoreImportPublication(candidate: StagedImportPublication): Promise<void> {
|
|
294
|
+
await candidate.ddc.session.restoreEntry(candidate.ddc);
|
|
295
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ImportErrorDetail,
|
|
3
|
+
ImportedAsset,
|
|
4
|
+
ImportProduct,
|
|
5
|
+
ImportProductFinalizeOptions,
|
|
6
|
+
ImportProductFinalizeResult,
|
|
7
|
+
Result,
|
|
8
|
+
} from '@forgeax/engine-types';
|
|
9
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
10
|
+
import type { ImportRunnerFs, RunImportMeta } from './import-runner.js';
|
|
11
|
+
import { runImport } from './import-runner.js';
|
|
12
|
+
import type { ImporterRegistry } from './importer-registry.js';
|
|
13
|
+
|
|
14
|
+
export type ProducerReadiness = 'before-consume' | 'on-demand';
|
|
15
|
+
|
|
16
|
+
export interface ProducerReadinessError {
|
|
17
|
+
readonly code: 'producer-readiness-invalid';
|
|
18
|
+
readonly expected: "'before-consume' or 'on-demand'";
|
|
19
|
+
readonly hint: 'set producerReadiness to before-consume or on-demand';
|
|
20
|
+
readonly detail: { readonly value: unknown };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ProducerReadinessResult =
|
|
24
|
+
| { readonly ok: true; readonly value: ProducerReadiness }
|
|
25
|
+
| { readonly ok: false; readonly error: ProducerReadinessError };
|
|
26
|
+
|
|
27
|
+
export function parseProducerReadiness(value: unknown): ProducerReadinessResult {
|
|
28
|
+
if (value === undefined || value === 'before-consume' || value === 'on-demand') {
|
|
29
|
+
return { ok: true, value: value ?? 'before-consume' };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: {
|
|
34
|
+
code: 'producer-readiness-invalid',
|
|
35
|
+
expected: "'before-consume' or 'on-demand'",
|
|
36
|
+
hint: 'set producerReadiness to before-consume or on-demand',
|
|
37
|
+
detail: { value },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SourcePackageProducerInput {
|
|
43
|
+
readonly meta: RunImportMeta;
|
|
44
|
+
readonly registry: ImporterRegistry;
|
|
45
|
+
readonly fs: ImportRunnerFs;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SourcePackageClosureDetail {
|
|
49
|
+
readonly stage: 'closure';
|
|
50
|
+
readonly declaredGuids: readonly string[];
|
|
51
|
+
readonly producedGuids: readonly string[];
|
|
52
|
+
readonly missingGuids: readonly string[];
|
|
53
|
+
readonly unexpectedGuids: readonly string[];
|
|
54
|
+
readonly duplicateGuids: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SourcePackageClosureError {
|
|
58
|
+
readonly code: 'source-package-guid-closure-mismatch';
|
|
59
|
+
readonly expected: string;
|
|
60
|
+
readonly hint: string;
|
|
61
|
+
readonly detail: SourcePackageClosureDetail;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SourcePackageProduct {
|
|
65
|
+
readonly anchorGuid: string;
|
|
66
|
+
readonly declaredGuids: readonly string[];
|
|
67
|
+
readonly product: ImportProduct<unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type SourcePackageFinalizeError = Extract<
|
|
71
|
+
ImportProductFinalizeResult,
|
|
72
|
+
{ readonly ok: false }
|
|
73
|
+
>['error'];
|
|
74
|
+
|
|
75
|
+
/** Apply an importer-owned transport finalizer without teaching the adapter its kind. */
|
|
76
|
+
export function finalizeSourcePackage(
|
|
77
|
+
product: ImportProduct<unknown>,
|
|
78
|
+
finalizer:
|
|
79
|
+
| ((
|
|
80
|
+
product: ImportProduct<unknown>,
|
|
81
|
+
options: ImportProductFinalizeOptions,
|
|
82
|
+
) => ImportProductFinalizeResult)
|
|
83
|
+
| undefined,
|
|
84
|
+
artifactUrl: ImportProductFinalizeOptions['artifactUrl'],
|
|
85
|
+
): Result<ImportProduct<unknown>, SourcePackageFinalizeError> {
|
|
86
|
+
if (finalizer === undefined) return ok(product);
|
|
87
|
+
const result = finalizer(product, { artifactUrl });
|
|
88
|
+
if (!result.ok) return err(result.error);
|
|
89
|
+
return ok({
|
|
90
|
+
...product,
|
|
91
|
+
assets: product.assets.map((asset, index) =>
|
|
92
|
+
index === 0 ? { ...asset, payload: result.value.asset } : asset,
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SourcePackageProducerResult =
|
|
98
|
+
| { readonly ok: true; readonly value: SourcePackageProduct }
|
|
99
|
+
| { readonly ok: false; readonly error: SourcePackageClosureError };
|
|
100
|
+
|
|
101
|
+
function closureError(
|
|
102
|
+
declaredGuids: readonly string[],
|
|
103
|
+
producedGuids: readonly string[],
|
|
104
|
+
): SourcePackageClosureError {
|
|
105
|
+
const declared = new Set(declaredGuids.map((guid) => guid.toLowerCase()));
|
|
106
|
+
const produced = new Set(producedGuids.map((guid) => guid.toLowerCase()));
|
|
107
|
+
const counts = new Map<string, number>();
|
|
108
|
+
for (const guid of producedGuids) {
|
|
109
|
+
const key = guid.toLowerCase();
|
|
110
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
const duplicateGuids = [...counts.entries()]
|
|
113
|
+
.filter(([, count]) => count > 1)
|
|
114
|
+
.map(([guid]) => guid);
|
|
115
|
+
return {
|
|
116
|
+
code: 'source-package-guid-closure-mismatch',
|
|
117
|
+
expected: 'the importer to return exactly one asset for every declared Meta GUID',
|
|
118
|
+
hint: 'repair the Meta declaration or importer output, then rebuild the whole source package',
|
|
119
|
+
detail: {
|
|
120
|
+
stage: 'closure',
|
|
121
|
+
declaredGuids,
|
|
122
|
+
producedGuids,
|
|
123
|
+
missingGuids: declaredGuids.filter((guid) => !produced.has(guid.toLowerCase())),
|
|
124
|
+
unexpectedGuids: producedGuids.filter((guid) => !declared.has(guid.toLowerCase())),
|
|
125
|
+
duplicateGuids,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function producedGuidsFromImportFailure(
|
|
131
|
+
declaredGuids: readonly string[],
|
|
132
|
+
detail: ImportErrorDetail,
|
|
133
|
+
): readonly string[] {
|
|
134
|
+
if ('missingGuids' in detail) {
|
|
135
|
+
return declaredGuids.filter((guid) => !detail.missingGuids.includes(guid));
|
|
136
|
+
}
|
|
137
|
+
if ('unexpectedGuids' in detail) return [...declaredGuids, ...detail.unexpectedGuids];
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function produceSourcePackage(
|
|
142
|
+
input: SourcePackageProducerInput,
|
|
143
|
+
): Promise<SourcePackageProducerResult> {
|
|
144
|
+
const declaredGuids = input.meta.subAssets.map((asset) => asset.guid);
|
|
145
|
+
const result = await runImport(input.meta, input.registry, input.fs);
|
|
146
|
+
if (!result.ok) {
|
|
147
|
+
const producedGuids = producedGuidsFromImportFailure(declaredGuids, result.error.detail);
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: closureError(declaredGuids, producedGuids),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if ('skipped' in result.value) {
|
|
154
|
+
return { ok: false, error: closureError(declaredGuids, []) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const producedGuids = result.value.product.assets.map((asset) => asset.guid);
|
|
158
|
+
const declaredSet = new Set(declaredGuids.map((guid) => guid.toLowerCase()));
|
|
159
|
+
const producedSet = new Set(producedGuids.map((guid) => guid.toLowerCase()));
|
|
160
|
+
const hasDuplicate =
|
|
161
|
+
new Set(producedGuids.map((guid) => guid.toLowerCase())).size !== producedGuids.length;
|
|
162
|
+
const complete =
|
|
163
|
+
!hasDuplicate &&
|
|
164
|
+
declaredGuids.length === producedGuids.length &&
|
|
165
|
+
declaredGuids.every((guid) => producedSet.has(guid.toLowerCase())) &&
|
|
166
|
+
producedGuids.every((guid) => declaredSet.has(guid.toLowerCase()));
|
|
167
|
+
if (!complete) return { ok: false, error: closureError(declaredGuids, producedGuids) };
|
|
168
|
+
|
|
169
|
+
const product = result.value.product;
|
|
170
|
+
const anchorGuid = [...declaredGuids].sort((left, right) => left.localeCompare(right))[0];
|
|
171
|
+
if (anchorGuid === undefined)
|
|
172
|
+
return { ok: false, error: closureError(declaredGuids, producedGuids) };
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
value: {
|
|
176
|
+
anchorGuid,
|
|
177
|
+
declaredGuids,
|
|
178
|
+
product,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function sourcePackageAssetsByGuid(
|
|
184
|
+
source: SourcePackageProduct,
|
|
185
|
+
): ReadonlyMap<string, ImportedAsset<unknown>> {
|
|
186
|
+
return new Map(source.product.assets.map((asset) => [asset.guid.toLowerCase(), asset]));
|
|
187
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"material-import-product.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/material-import-product.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { AssetGuid } from '@forgeax/engine-types';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import {
|
|
4
|
-
createMaterialImportProduct,
|
|
5
|
-
type MaterialImportProductInput,
|
|
6
|
-
materialImportProductReady,
|
|
7
|
-
} from '../import-product.js';
|
|
8
|
-
|
|
9
|
-
const input: MaterialImportProductInput = {
|
|
10
|
-
guid: 'mat-child',
|
|
11
|
-
sourcePath: 'materials/child.material.json',
|
|
12
|
-
material: {
|
|
13
|
-
kind: 'material',
|
|
14
|
-
parent: 'mat-parent' as unknown as AssetGuid,
|
|
15
|
-
values: {
|
|
16
|
-
baseColor: {
|
|
17
|
-
texture: 'texture/albedo' as unknown as AssetGuid,
|
|
18
|
-
sampler: 'sampler/linear' as unknown as AssetGuid,
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
refs: {
|
|
23
|
-
parent: ['mat-parent'],
|
|
24
|
-
textures: ['texture/albedo'],
|
|
25
|
-
samplers: ['sampler/linear'],
|
|
26
|
-
modules: ['module/pbr'],
|
|
27
|
-
},
|
|
28
|
-
sourceEvidence: { inputFingerprint: 'sha256:source', importerVersion: 'material/1' },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
describe('material import product', () => {
|
|
32
|
-
it('keeps authored material and source evidence without runtime handles', () => {
|
|
33
|
-
const result = createMaterialImportProduct(input);
|
|
34
|
-
|
|
35
|
-
expect(result.ok).toBe(true);
|
|
36
|
-
if (!result.ok) return;
|
|
37
|
-
expect(result.value.asset.payload).toEqual(input.material);
|
|
38
|
-
expect(result.value.asset.refs.map((ref) => ref.guid)).toEqual([
|
|
39
|
-
'mat-parent',
|
|
40
|
-
'texture/albedo',
|
|
41
|
-
'sampler/linear',
|
|
42
|
-
'module/pbr',
|
|
43
|
-
]);
|
|
44
|
-
expect(result.value.sourceEvidence).toEqual(input.sourceEvidence);
|
|
45
|
-
expect(result.value).not.toHaveProperty('compiler');
|
|
46
|
-
expect(result.value).not.toHaveProperty('handle');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('does not claim Ready when a declared dependency is absent', () => {
|
|
50
|
-
const result = createMaterialImportProduct({ ...input, refs: { ...input.refs, parent: [] } });
|
|
51
|
-
|
|
52
|
-
expect(result.ok).toBe(true);
|
|
53
|
-
if (!result.ok) return;
|
|
54
|
-
expect(materialImportProductReady(result.value, new Set(['texture/albedo']))).toBe(false);
|
|
55
|
-
});
|
|
56
|
-
});
|