@forgeax/engine-import 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -3
- package/dist/__tests__/ies-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts +2 -0
- package/dist/__tests__/ies-producer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts +2 -0
- package/dist/__tests__/mesh-lod-reimport.integration.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-scriptable-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-file-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts +2 -0
- package/dist/__tests__/source-package-publication-lod.integration.test.d.ts.map +1 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.mjs +230 -3
- package/dist/browser.mjs.map +1 -1
- package/dist/build-production.d.ts +1 -8
- package/dist/build-production.d.ts.map +1 -1
- package/dist/ies/ies-importer.d.ts +4 -0
- package/dist/ies/ies-importer.d.ts.map +1 -0
- package/dist/ies/parse-lm63.d.ts +13 -0
- package/dist/ies/parse-lm63.d.ts.map +1 -0
- package/dist/ies/resample-type-c.d.ts +4 -0
- package/dist/ies/resample-type-c.d.ts.map +1 -0
- package/dist/import-runner.d.ts.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +2547 -1028
- package/dist/index.mjs.map +1 -1
- package/dist/mesh-bin.d.ts.map +1 -1
- package/dist/mesh-bin.mjs +29 -1
- package/dist/mesh-bin.mjs.map +1 -1
- package/dist/mesh-lod.d.ts +60 -0
- package/dist/mesh-lod.d.ts.map +1 -0
- package/dist/parameterized-scriptable-pack.d.ts +68 -0
- package/dist/parameterized-scriptable-pack.d.ts.map +1 -0
- package/dist/scriptable-pack-file-snapshot.d.ts +71 -0
- package/dist/scriptable-pack-file-snapshot.d.ts.map +1 -0
- package/dist/scriptable-pack-host.d.ts +70 -39
- package/dist/scriptable-pack-host.d.ts.map +1 -1
- package/dist/scriptable-pack-output-producers.d.ts +1 -0
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -1
- package/dist/scriptable-pack-staged-snapshot.d.ts +2 -2
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -1
- package/dist/scriptable-pack.d.ts +13 -26
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/source-package-errors.d.ts.map +1 -1
- package/dist/source-package-publication.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/ies-contract.unit.test.ts +31 -0
- package/src/__tests__/ies-producer.unit.test.ts +104 -0
- package/src/__tests__/mesh-bin.test.ts +69 -0
- package/src/__tests__/mesh-lod-contract.unit.test.ts +92 -0
- package/src/__tests__/mesh-lod-reimport.integration.test.ts +15 -0
- package/src/__tests__/parameterized-scriptable-pack.unit.test.ts +270 -0
- package/src/__tests__/scriptable-pack-file-snapshot.unit.test.ts +177 -0
- package/src/__tests__/scriptable-pack-host.unit.test.ts +196 -4
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +2 -0
- package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +43 -12
- package/src/__tests__/source-package-publication-lod.integration.test.ts +29 -0
- package/src/__tests__/source-package-publication.integration.test.ts +26 -1
- package/src/browser.ts +5 -0
- package/src/build-production.ts +498 -141
- package/src/ies/ies-importer.ts +126 -0
- package/src/ies/parse-lm63.ts +98 -0
- package/src/ies/resample-type-c.ts +108 -0
- package/src/import-runner.ts +58 -0
- package/src/index.ts +49 -19
- package/src/mesh-bin.ts +38 -0
- package/src/mesh-lod.ts +265 -0
- package/src/parameterized-scriptable-pack.ts +725 -0
- package/src/scriptable-pack-file-snapshot.ts +346 -0
- package/src/scriptable-pack-host.ts +790 -343
- package/src/scriptable-pack-output-producers.ts +55 -0
- package/src/scriptable-pack-staged-snapshot.ts +12 -11
- package/src/scriptable-pack.ts +16 -485
- package/src/source-package-errors.ts +6 -0
- package/src/source-package-publication.ts +32 -2
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.integration.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +0 -1
- package/dist/scriptable-source-package.d.ts +0 -14
- package/dist/scriptable-source-package.d.ts.map +0 -1
- package/src/__tests__/scriptable-pack-product.contract.test.ts +0 -46
- package/src/__tests__/scriptable-pack.integration.test.ts +0 -435
- package/src/scriptable-source-package.ts +0 -88
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { AssetGuid, definePack, definePackageId, PackageId } from '@forgeax/engine-pack/source';
|
|
2
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
buildParameterizedScriptablePack,
|
|
6
|
+
buildParameterizedScriptablePackWorklist,
|
|
7
|
+
} from '../parameterized-scriptable-pack.js';
|
|
8
|
+
import type { ScriptablePackAssetSnapshotSource } from '../scriptable-pack.js';
|
|
9
|
+
import { AssetOutputProducerRegistry } from '../scriptable-pack.js';
|
|
10
|
+
import { produceParameterizedScriptablePackProducts } from '../scriptable-pack-host.js';
|
|
11
|
+
|
|
12
|
+
function id(value: string) {
|
|
13
|
+
const result = PackageId.parse(value);
|
|
14
|
+
if (!result.ok) throw result.error;
|
|
15
|
+
return result.value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function scene() {
|
|
19
|
+
return { kind: 'scene' as const, entities: [], mounts: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function producers() {
|
|
23
|
+
const registry = new AssetOutputProducerRegistry();
|
|
24
|
+
registry.register({
|
|
25
|
+
kind: 'scene',
|
|
26
|
+
version: 'test',
|
|
27
|
+
produce: ({ asset }) => ok({ payload: asset as never, refs: [], artifacts: {} }),
|
|
28
|
+
});
|
|
29
|
+
return registry;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('parameterized ScriptablePack import bridge', () => {
|
|
33
|
+
it('uses the subject packageId and dynamic sourceKey output map', async () => {
|
|
34
|
+
const packageId = definePackageId('01900000-0000-7000-8000-000000000040');
|
|
35
|
+
const definition = definePack({
|
|
36
|
+
schemaVersion: '2.0.0',
|
|
37
|
+
packageId,
|
|
38
|
+
parameters: [{ name: 'count', type: 'u32', default: 1, minimum: 1 }] as const,
|
|
39
|
+
build: ({ values }) =>
|
|
40
|
+
ok(
|
|
41
|
+
Object.fromEntries(
|
|
42
|
+
Array.from({ length: values.count }, (_, index) => [`scene/${index}`, scene()]),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
});
|
|
46
|
+
const result = await buildParameterizedScriptablePack({
|
|
47
|
+
definition,
|
|
48
|
+
sourcePath: 'assets/param.pack.ts',
|
|
49
|
+
values: { count: 2 },
|
|
50
|
+
outputs: producers(),
|
|
51
|
+
});
|
|
52
|
+
expect(result.ok).toBe(true);
|
|
53
|
+
if (!result.ok) return;
|
|
54
|
+
expect(result.value.product.assets.map((asset) => asset.guid)).toEqual([
|
|
55
|
+
AssetGuid.format(AssetGuid.derive(packageId, 'scene/0')),
|
|
56
|
+
AssetGuid.format(AssetGuid.derive(packageId, 'scene/1')),
|
|
57
|
+
]);
|
|
58
|
+
expect(result.value.stagedOutputs.map((output) => output.sourceKey)).toEqual([
|
|
59
|
+
'scene/0',
|
|
60
|
+
'scene/1',
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('retries forward content reads until the internal worklist makes progress', async () => {
|
|
65
|
+
const firstId = id('01900000-0000-7000-8000-000000000050');
|
|
66
|
+
const secondId = id('01900000-0000-7000-8000-000000000051');
|
|
67
|
+
const first = definePack({
|
|
68
|
+
schemaVersion: '2.0.0',
|
|
69
|
+
packageId: firstId,
|
|
70
|
+
build: () => ok({ 'scene/first': scene() }),
|
|
71
|
+
});
|
|
72
|
+
const dependencyGuid = AssetGuid.derive(firstId, 'scene/first');
|
|
73
|
+
const second = definePack({
|
|
74
|
+
schemaVersion: '2.0.0',
|
|
75
|
+
packageId: secondId,
|
|
76
|
+
build: async ({ readByGuid }) => {
|
|
77
|
+
const dependency = await readByGuid(dependencyGuid);
|
|
78
|
+
if (!dependency.ok) return err(dependency.error);
|
|
79
|
+
return ok({ 'scene/second': scene() });
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const result = await buildParameterizedScriptablePackWorklist({
|
|
83
|
+
subjects: [
|
|
84
|
+
// Keep the dependency consumer first even after the worklist applies
|
|
85
|
+
// its stable source-path ordering, so this proves a real retry pass.
|
|
86
|
+
{ definition: second, sourcePath: 'assets/a-second.pack.ts' },
|
|
87
|
+
{ definition: first, sourcePath: 'assets/z-first.pack.ts' },
|
|
88
|
+
],
|
|
89
|
+
outputs: producers(),
|
|
90
|
+
});
|
|
91
|
+
expect(result).toMatchObject({ ok: true, value: { iterations: 2 } });
|
|
92
|
+
if (result.ok) expect(result.value.products).toHaveLength(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns a structured stall when no subject can satisfy a content read', async () => {
|
|
96
|
+
const missing = AssetGuid.derive(id('01900000-0000-7000-8000-000000000060'), 'scene/missing');
|
|
97
|
+
const definition = definePack({
|
|
98
|
+
schemaVersion: '2.0.0',
|
|
99
|
+
packageId: id('01900000-0000-7000-8000-000000000061'),
|
|
100
|
+
build: async ({ readByGuid }) => {
|
|
101
|
+
const dependency = await readByGuid(missing);
|
|
102
|
+
if (!dependency.ok) return err(dependency.error);
|
|
103
|
+
return ok({ 'scene/main': scene() });
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const result = await buildParameterizedScriptablePackWorklist({
|
|
107
|
+
subjects: [{ definition, sourcePath: 'assets/missing.pack.ts' }],
|
|
108
|
+
outputs: producers(),
|
|
109
|
+
});
|
|
110
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'pack-content-dependency-stalled' } });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('uses the published asset source when a dynamic build reads a non-staged dependency', async () => {
|
|
114
|
+
const packageId = id('01900000-0000-7000-8000-000000000062');
|
|
115
|
+
const dependencyPackageId = id('01900000-0000-7000-8000-000000000063');
|
|
116
|
+
const dependencyGuid = AssetGuid.derive(dependencyPackageId, 'sampler/main');
|
|
117
|
+
const dependency = {
|
|
118
|
+
kind: 'sampler' as const,
|
|
119
|
+
magFilter: 'linear' as const,
|
|
120
|
+
minFilter: 'linear' as const,
|
|
121
|
+
mipmapFilter: 'linear' as const,
|
|
122
|
+
addressModeU: 'repeat' as const,
|
|
123
|
+
addressModeV: 'repeat' as const,
|
|
124
|
+
addressModeW: 'repeat' as const,
|
|
125
|
+
};
|
|
126
|
+
const assetSource: ScriptablePackAssetSnapshotSource = {
|
|
127
|
+
async readByGuid(guid) {
|
|
128
|
+
return AssetGuid.equals(guid, dependencyGuid)
|
|
129
|
+
? ok({ asset: dependency, generation: 7, digest: 'sha256:published' })
|
|
130
|
+
: err({
|
|
131
|
+
code: 'asset-not-found',
|
|
132
|
+
expected: 'the published dependency',
|
|
133
|
+
hint: 'publish the dependency before rebuilding',
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
const definition = definePack({
|
|
138
|
+
schemaVersion: '2.0.0',
|
|
139
|
+
packageId,
|
|
140
|
+
build: async ({ readByGuid }) => {
|
|
141
|
+
const read = await readByGuid(dependencyGuid);
|
|
142
|
+
if (!read.ok) return err(read.error);
|
|
143
|
+
return ok({ 'scene/main': scene() });
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const result = await produceParameterizedScriptablePackProducts({
|
|
147
|
+
sources: [
|
|
148
|
+
{
|
|
149
|
+
sourcePath: 'assets/consumer.pack.ts',
|
|
150
|
+
displaySourcePath: 'assets/consumer.pack.ts',
|
|
151
|
+
definition,
|
|
152
|
+
sourceClosure: [],
|
|
153
|
+
publicationGeneration: 1,
|
|
154
|
+
policy: {
|
|
155
|
+
base: '/',
|
|
156
|
+
packagePath: 'assets/consumer.pack.json',
|
|
157
|
+
artifactPath: (guid, key) => `artifacts/${guid}/${key}`,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
assetSource,
|
|
162
|
+
});
|
|
163
|
+
expect(result).toMatchObject({ ok: true });
|
|
164
|
+
if (!result.ok) return;
|
|
165
|
+
expect(result.value.get('assets/consumer.pack.ts')?.product.externalEvidence).toEqual([
|
|
166
|
+
{
|
|
167
|
+
guid: AssetGuid.format(dependencyGuid),
|
|
168
|
+
usage: 'content',
|
|
169
|
+
generation: 7,
|
|
170
|
+
digest: 'sha256:published',
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
const revision = result.value.get('assets/consumer.pack.ts')?.revision;
|
|
174
|
+
expect(revision?.observedAt).toBe(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not replace a staged read failure with a published value', async () => {
|
|
178
|
+
const packageId = id('01900000-0000-7000-8000-000000000064');
|
|
179
|
+
const dependencyGuid = AssetGuid.derive(packageId, 'scene/dependency');
|
|
180
|
+
const assetSource: ScriptablePackAssetSnapshotSource = {
|
|
181
|
+
async readByGuid() {
|
|
182
|
+
return err({
|
|
183
|
+
code: 'asset-fetch-failed',
|
|
184
|
+
expected: 'the staged dependency read to succeed',
|
|
185
|
+
hint: 'repair the staged generation before retrying',
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
const definition = definePack({
|
|
190
|
+
schemaVersion: '2.0.0',
|
|
191
|
+
packageId,
|
|
192
|
+
build: async ({ readByGuid }) => {
|
|
193
|
+
const read = await readByGuid(dependencyGuid);
|
|
194
|
+
if (!read.ok) return err(read.error);
|
|
195
|
+
return ok({ 'scene/main': scene() });
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const result = await produceParameterizedScriptablePackProducts({
|
|
199
|
+
sources: [
|
|
200
|
+
{
|
|
201
|
+
sourcePath: 'assets/failing-consumer.pack.ts',
|
|
202
|
+
displaySourcePath: 'assets/failing-consumer.pack.ts',
|
|
203
|
+
definition,
|
|
204
|
+
sourceClosure: [],
|
|
205
|
+
publicationGeneration: 1,
|
|
206
|
+
policy: {
|
|
207
|
+
base: '/',
|
|
208
|
+
packagePath: 'assets/failing-consumer.pack.json',
|
|
209
|
+
artifactPath: (guid, key) => `artifacts/${guid}/${key}`,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
assetSource,
|
|
214
|
+
});
|
|
215
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'asset-fetch-failed' } });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('converts a throwing output producer into a structured import failure', async () => {
|
|
219
|
+
const outputs = new AssetOutputProducerRegistry();
|
|
220
|
+
outputs.register({
|
|
221
|
+
kind: 'scene',
|
|
222
|
+
version: 'test',
|
|
223
|
+
produce: () => {
|
|
224
|
+
throw new Error('producer exploded');
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const result = await buildParameterizedScriptablePack({
|
|
228
|
+
definition: definePack({
|
|
229
|
+
schemaVersion: '2.0.0',
|
|
230
|
+
packageId: id('01900000-0000-7000-8000-000000000065'),
|
|
231
|
+
build: () => ok({ 'scene/main': scene() }),
|
|
232
|
+
}),
|
|
233
|
+
sourcePath: 'assets/throwing-producer.pack.ts',
|
|
234
|
+
outputs,
|
|
235
|
+
});
|
|
236
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'import-internal-error' } });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('rejects malformed output producer products at the import boundary', async () => {
|
|
240
|
+
const outputs = new AssetOutputProducerRegistry();
|
|
241
|
+
outputs.register({
|
|
242
|
+
kind: 'scene',
|
|
243
|
+
version: 'test',
|
|
244
|
+
produce: () => ({ ok: true, value: null }) as never,
|
|
245
|
+
});
|
|
246
|
+
const result = await buildParameterizedScriptablePack({
|
|
247
|
+
definition: definePack({
|
|
248
|
+
schemaVersion: '2.0.0',
|
|
249
|
+
packageId: id('01900000-0000-7000-8000-000000000066'),
|
|
250
|
+
build: () => ok({ 'scene/main': scene() }),
|
|
251
|
+
}),
|
|
252
|
+
sourcePath: 'assets/malformed-producer.pack.ts',
|
|
253
|
+
outputs,
|
|
254
|
+
});
|
|
255
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('converts a malformed Pack build result into a structured import failure', async () => {
|
|
259
|
+
const result = await buildParameterizedScriptablePack({
|
|
260
|
+
definition: definePack({
|
|
261
|
+
schemaVersion: '2.0.0',
|
|
262
|
+
packageId: id('01900000-0000-7000-8000-000000000067'),
|
|
263
|
+
build: () => null as never,
|
|
264
|
+
}),
|
|
265
|
+
sourcePath: 'assets/malformed-build.pack.ts',
|
|
266
|
+
outputs: producers(),
|
|
267
|
+
});
|
|
268
|
+
expect(result).toMatchObject({ ok: false, error: { code: 'import-internal-error' } });
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
5
|
+
import type { AssetGuid as AssetGuidType } from '@forgeax/engine-types';
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { createScriptablePackFileAssetSnapshotSource } from '../index.js';
|
|
8
|
+
|
|
9
|
+
function guid(value: string): AssetGuidType {
|
|
10
|
+
const parsed = AssetGuid.parse(value);
|
|
11
|
+
if (!parsed.ok) throw parsed.error;
|
|
12
|
+
return parsed.value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pack(
|
|
16
|
+
assets: readonly {
|
|
17
|
+
readonly guid: string;
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
readonly payload: Record<string, unknown>;
|
|
20
|
+
}[],
|
|
21
|
+
): string {
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
schemaVersion: '1.0.0',
|
|
24
|
+
kind: 'internal-text-package',
|
|
25
|
+
assets: assets.map((asset) => ({ ...asset, refs: [] })),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ROOT_GUID = '019ffa97-2000-7000-8000-000000000011';
|
|
30
|
+
const SECOND_GUID = '019ffa97-2000-7000-8000-000000000012';
|
|
31
|
+
const MISSING_GUID = guid('019ffa97-2000-7000-8000-000000000099');
|
|
32
|
+
const temporaryRoots: string[] = [];
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await Promise.all(
|
|
36
|
+
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
async function temporaryRoot(): Promise<string> {
|
|
41
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-file-source-'));
|
|
42
|
+
temporaryRoots.push(root);
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('ScriptablePack file asset snapshot source', () => {
|
|
47
|
+
it('indexes ordinary Pack JSON by GUID and returns deterministic private snapshots', async () => {
|
|
48
|
+
const root = await temporaryRoot();
|
|
49
|
+
const nested = join(root, 'nested');
|
|
50
|
+
await mkdir(nested);
|
|
51
|
+
await writeFile(
|
|
52
|
+
join(root, 'root.pack.json'),
|
|
53
|
+
pack([{ guid: ROOT_GUID, kind: 'video', payload: { url: '/root.webm' } }]),
|
|
54
|
+
);
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(nested, 'second.pack.json'),
|
|
57
|
+
pack([{ guid: SECOND_GUID, kind: 'video', payload: { url: '/second.webm' } }]),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const source = createScriptablePackFileAssetSnapshotSource({ assetRoots: [root] });
|
|
61
|
+
const first = await source.readByGuid(guid(ROOT_GUID));
|
|
62
|
+
const second = await source.readByGuid(guid(ROOT_GUID));
|
|
63
|
+
|
|
64
|
+
expect(first).toMatchObject({
|
|
65
|
+
ok: true,
|
|
66
|
+
value: {
|
|
67
|
+
asset: { kind: 'video', url: '/root.webm' },
|
|
68
|
+
generation: expect.any(Number),
|
|
69
|
+
digest: expect.stringMatching(/^sha256:[0-9a-f]{64}$/),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(second).toEqual(first);
|
|
73
|
+
if (!first.ok) return;
|
|
74
|
+
expect(first.value.generation).toBeGreaterThan(0);
|
|
75
|
+
(first.value.asset as { url: string }).url = '/mutated.webm';
|
|
76
|
+
|
|
77
|
+
const reread = await source.readByGuid(guid(ROOT_GUID));
|
|
78
|
+
expect(reread).toMatchObject({ ok: true, value: { asset: { url: '/root.webm' } } });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('keeps generation and digest stable when roots and JSON key order change', async () => {
|
|
82
|
+
const firstRoot = await temporaryRoot();
|
|
83
|
+
const secondRoot = await temporaryRoot();
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(firstRoot, 'first.pack.json'),
|
|
86
|
+
pack([{ guid: ROOT_GUID, kind: 'video', payload: { url: '/root.webm', loop: true } }]),
|
|
87
|
+
);
|
|
88
|
+
await writeFile(
|
|
89
|
+
join(secondRoot, 'second.pack.json'),
|
|
90
|
+
pack([{ guid: SECOND_GUID, kind: 'video', payload: { loop: false, url: '/second.webm' } }]),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const first = await createScriptablePackFileAssetSnapshotSource({
|
|
94
|
+
assetRoots: [firstRoot, secondRoot],
|
|
95
|
+
}).readByGuid(guid(ROOT_GUID));
|
|
96
|
+
const second = await createScriptablePackFileAssetSnapshotSource({
|
|
97
|
+
assetRoots: [secondRoot, firstRoot],
|
|
98
|
+
}).readByGuid(guid(ROOT_GUID));
|
|
99
|
+
|
|
100
|
+
expect(second).toEqual(first);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns structured failures for malformed packages, collisions, and missing GUIDs', async () => {
|
|
104
|
+
const malformedRoot = await temporaryRoot();
|
|
105
|
+
await writeFile(join(malformedRoot, 'broken.pack.json'), '{not-json');
|
|
106
|
+
const malformed = await createScriptablePackFileAssetSnapshotSource({
|
|
107
|
+
assetRoots: [malformedRoot],
|
|
108
|
+
}).readByGuid(MISSING_GUID);
|
|
109
|
+
expect(malformed).toMatchObject({
|
|
110
|
+
ok: false,
|
|
111
|
+
error: {
|
|
112
|
+
code: 'scriptable-pack-file-json-invalid',
|
|
113
|
+
detail: { path: join(malformedRoot, 'broken.pack.json') },
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const invalidRoot = await temporaryRoot();
|
|
118
|
+
await writeFile(
|
|
119
|
+
join(invalidRoot, 'invalid.pack.json'),
|
|
120
|
+
JSON.stringify({ schemaVersion: '1.0.0', kind: 'internal-text-package' }),
|
|
121
|
+
);
|
|
122
|
+
const invalid = await createScriptablePackFileAssetSnapshotSource({
|
|
123
|
+
assetRoots: [invalidRoot],
|
|
124
|
+
}).readByGuid(MISSING_GUID);
|
|
125
|
+
expect(invalid).toMatchObject({
|
|
126
|
+
ok: false,
|
|
127
|
+
error: {
|
|
128
|
+
code: 'scriptable-pack-file-invalid',
|
|
129
|
+
detail: { path: join(invalidRoot, 'invalid.pack.json') },
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const collisionRoot = await temporaryRoot();
|
|
134
|
+
await writeFile(
|
|
135
|
+
join(collisionRoot, 'a.pack.json'),
|
|
136
|
+
pack([{ guid: ROOT_GUID, kind: 'video', payload: { url: '/a.webm' } }]),
|
|
137
|
+
);
|
|
138
|
+
await writeFile(
|
|
139
|
+
join(collisionRoot, 'b.pack.json'),
|
|
140
|
+
pack([{ guid: ROOT_GUID, kind: 'video', payload: { url: '/b.webm' } }]),
|
|
141
|
+
);
|
|
142
|
+
const collision = await createScriptablePackFileAssetSnapshotSource({
|
|
143
|
+
assetRoots: [collisionRoot],
|
|
144
|
+
}).readByGuid(guid(ROOT_GUID));
|
|
145
|
+
expect(collision).toMatchObject({
|
|
146
|
+
ok: false,
|
|
147
|
+
error: {
|
|
148
|
+
code: 'scriptable-pack-file-guid-collision',
|
|
149
|
+
detail: {
|
|
150
|
+
guid: ROOT_GUID,
|
|
151
|
+
paths: [join(collisionRoot, 'a.pack.json'), join(collisionRoot, 'b.pack.json')],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const emptyRoot = await temporaryRoot();
|
|
157
|
+
const missing = await createScriptablePackFileAssetSnapshotSource({
|
|
158
|
+
assetRoots: [emptyRoot],
|
|
159
|
+
}).readByGuid(MISSING_GUID);
|
|
160
|
+
expect(missing).toMatchObject({
|
|
161
|
+
ok: false,
|
|
162
|
+
error: { code: 'asset-not-found', detail: { guid: AssetGuid.format(MISSING_GUID) } },
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('fails closed when an asset root cannot be read', async () => {
|
|
167
|
+
const root = join(await temporaryRoot(), 'missing');
|
|
168
|
+
const result = await createScriptablePackFileAssetSnapshotSource({
|
|
169
|
+
assetRoots: [root],
|
|
170
|
+
}).readByGuid(MISSING_GUID);
|
|
171
|
+
|
|
172
|
+
expect(result).toMatchObject({
|
|
173
|
+
ok: false,
|
|
174
|
+
error: { code: 'scriptable-pack-file-root-unreadable', detail: { root } },
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { AssetGuid, PackageId } from '@forgeax/engine-pack/guid';
|
|
3
|
+
import type { NativeCooker } from '@forgeax/engine-pack/native-cooker';
|
|
4
|
+
import type {
|
|
5
|
+
ParameterizedPackInventoryDocument,
|
|
6
|
+
ScanSourceDeclaration,
|
|
7
|
+
} from '@forgeax/engine-pack/scanner';
|
|
8
|
+
import { parseParameterizedPackJson, projectDirectPackJson } from '@forgeax/engine-pack/source';
|
|
3
9
|
import type {
|
|
4
10
|
Asset,
|
|
5
11
|
AssetGuid as AssetGuidType,
|
|
@@ -8,7 +14,10 @@ import type {
|
|
|
8
14
|
} from '@forgeax/engine-types';
|
|
9
15
|
import { describe, expect, it } from 'vitest';
|
|
10
16
|
import { ImporterRegistry } from '../importer-registry.js';
|
|
11
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
declaredPackExternalOutputs,
|
|
19
|
+
prepareDirectPackTransport,
|
|
20
|
+
} from '../scriptable-pack-host.js';
|
|
12
21
|
|
|
13
22
|
const GUID = '019ffa97-0000-7000-8000-000000000001';
|
|
14
23
|
|
|
@@ -69,7 +78,6 @@ describe('ScriptablePack external Meta staging', () => {
|
|
|
69
78
|
fsForImport: {
|
|
70
79
|
readSource: async () => ({ ok: true as const, value: new Uint8Array([1, 2, 3]) }),
|
|
71
80
|
},
|
|
72
|
-
assetPaths: {},
|
|
73
81
|
},
|
|
74
82
|
);
|
|
75
83
|
|
|
@@ -83,4 +91,188 @@ describe('ScriptablePack external Meta staging', () => {
|
|
|
83
91
|
0, 1, 2,
|
|
84
92
|
]);
|
|
85
93
|
});
|
|
94
|
+
|
|
95
|
+
it('cooks direct producer-backed outputs before exposing them to a dynamic Pack', async () => {
|
|
96
|
+
const packageId = PackageId.parse('019ffa97-0000-7000-8000-000000000010');
|
|
97
|
+
if (!packageId.ok) throw packageId.error;
|
|
98
|
+
const outputGuid = AssetGuid.format(AssetGuid.derive(packageId.value, 'vfx/main'));
|
|
99
|
+
const materialGuid = '019ffa97-0000-7000-8000-000000000099';
|
|
100
|
+
const value: ParameterizedPackInventoryDocument = {
|
|
101
|
+
schemaVersion: '3.0.0',
|
|
102
|
+
packageId: PackageId.format(packageId.value),
|
|
103
|
+
assets: {
|
|
104
|
+
'vfx/main': {
|
|
105
|
+
kind: 'particle-effect',
|
|
106
|
+
payload: {
|
|
107
|
+
schemaVersion: 2,
|
|
108
|
+
emitters: [
|
|
109
|
+
{
|
|
110
|
+
id: 'main',
|
|
111
|
+
program: { module: 'main.vfx.wgsl' },
|
|
112
|
+
renderers: [],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
refs: [materialGuid],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
let cookCalls = 0;
|
|
121
|
+
const cooker: NativeCooker = {
|
|
122
|
+
key: 'particle-effect',
|
|
123
|
+
cook(input) {
|
|
124
|
+
cookCalls += 1;
|
|
125
|
+
const source = input as { readonly guid: string };
|
|
126
|
+
return {
|
|
127
|
+
guid: source.guid,
|
|
128
|
+
payload: {
|
|
129
|
+
kind: 'particle-effect',
|
|
130
|
+
schemaVersion: 2,
|
|
131
|
+
programFingerprint: 'sha256:cooked',
|
|
132
|
+
emitters: [],
|
|
133
|
+
program: {
|
|
134
|
+
format: 'forgeax-vfx-program-2',
|
|
135
|
+
fingerprint: 'sha256:cooked',
|
|
136
|
+
emitters: [],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
refs: [materialGuid],
|
|
140
|
+
artifacts: {
|
|
141
|
+
'particle-effect/program.json': {
|
|
142
|
+
mediaType: 'application/json',
|
|
143
|
+
bytes: new TextEncoder().encode('{"program":"cooked"}'),
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
inputFingerprint: 'sha256:cooked',
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const declaration = {
|
|
151
|
+
format: 'pack.json',
|
|
152
|
+
sourcePath: resolve(process.cwd(), 'src/__tests__/scriptable-pack-host.unit.test.ts'),
|
|
153
|
+
sourceRevision: 'sha256:direct',
|
|
154
|
+
sourceText: JSON.stringify(value),
|
|
155
|
+
value,
|
|
156
|
+
} satisfies Extract<ScanSourceDeclaration, { readonly format: 'pack.json' }>;
|
|
157
|
+
|
|
158
|
+
const outputs = await declaredPackExternalOutputs(
|
|
159
|
+
new Map([[declaration.sourcePath, declaration]]),
|
|
160
|
+
[cooker],
|
|
161
|
+
[parseGuid(outputGuid), parseGuid(materialGuid)],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(cookCalls).toBe(1);
|
|
165
|
+
expect(outputs).toHaveLength(1);
|
|
166
|
+
expect(outputs[0]).toMatchObject({
|
|
167
|
+
sourceKey: 'vfx/main',
|
|
168
|
+
asset: {
|
|
169
|
+
kind: 'particle-effect',
|
|
170
|
+
programFingerprint: 'sha256:cooked',
|
|
171
|
+
program: { format: 'forgeax-vfx-program-2' },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('keeps a direct non-Engine POD as direct transport', async () => {
|
|
177
|
+
const packageId = PackageId.parse('019ffa97-0000-0000-8000-000000000010');
|
|
178
|
+
if (!packageId.ok) throw packageId.error;
|
|
179
|
+
const outputGuid = AssetGuid.format(AssetGuid.derive(packageId.value, 'ui/main'));
|
|
180
|
+
const value: ParameterizedPackInventoryDocument = {
|
|
181
|
+
schemaVersion: '3.0.0',
|
|
182
|
+
packageId: PackageId.format(packageId.value),
|
|
183
|
+
assets: {
|
|
184
|
+
'ui/main': {
|
|
185
|
+
kind: 'ui',
|
|
186
|
+
payload: { html: '<main>direct</main>', css: ':host{display:block}' },
|
|
187
|
+
refs: [],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const declaration = {
|
|
192
|
+
format: 'pack.json',
|
|
193
|
+
sourcePath: resolve(process.cwd(), 'src/__tests__/scriptable-pack-host.unit.test.ts'),
|
|
194
|
+
sourceRevision: 'sha256:ui',
|
|
195
|
+
sourceText: JSON.stringify(value),
|
|
196
|
+
value,
|
|
197
|
+
} satisfies Extract<ScanSourceDeclaration, { readonly format: 'pack.json' }>;
|
|
198
|
+
|
|
199
|
+
const outputs = await declaredPackExternalOutputs(
|
|
200
|
+
new Map([[declaration.sourcePath, declaration]]),
|
|
201
|
+
[],
|
|
202
|
+
[parseGuid(outputGuid)],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(outputs).toHaveLength(1);
|
|
206
|
+
expect(outputs[0]?.asset).toEqual({
|
|
207
|
+
kind: 'ui',
|
|
208
|
+
guid: outputGuid,
|
|
209
|
+
html: '<main>direct</main>',
|
|
210
|
+
css: ':host{display:block}',
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('leaves Engine-owned material modules on the runtime registry path', async () => {
|
|
215
|
+
const packageId = PackageId.parse('019ffa97-0000-0000-8000-000000000011');
|
|
216
|
+
if (!packageId.ok) throw packageId.error;
|
|
217
|
+
const value = {
|
|
218
|
+
schemaVersion: '3.0.0' as const,
|
|
219
|
+
packageId: PackageId.format(packageId.value),
|
|
220
|
+
assets: {
|
|
221
|
+
'material/base': {
|
|
222
|
+
kind: 'material',
|
|
223
|
+
payload: {
|
|
224
|
+
passes: [
|
|
225
|
+
{ name: 'Forward', program: { module: 'forgeax::default-unlit' } },
|
|
226
|
+
{ name: 'ShadowCaster', program: { module: 'forgeax::default-shadow-caster' } },
|
|
227
|
+
],
|
|
228
|
+
values: { baseColor: [0.6, 0.6, 0.6, 1] },
|
|
229
|
+
},
|
|
230
|
+
refs: [],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
const parsed = parseParameterizedPackJson(value);
|
|
235
|
+
expect(parsed.ok).toBe(true);
|
|
236
|
+
if (!parsed.ok || parsed.value.format !== 'direct') return;
|
|
237
|
+
const projected = projectDirectPackJson(parsed.value);
|
|
238
|
+
expect(projected.ok).toBe(true);
|
|
239
|
+
if (!projected.ok) return;
|
|
240
|
+
let cookCalls = 0;
|
|
241
|
+
const sourcePath = resolve(process.cwd(), 'src/__tests__/scriptable-pack-host.unit.test.ts');
|
|
242
|
+
const result = await prepareDirectPackTransport({
|
|
243
|
+
projected: projected.value,
|
|
244
|
+
sourcePath,
|
|
245
|
+
sourceRevision: 'sha256:direct-engine-material',
|
|
246
|
+
cookers: [
|
|
247
|
+
{
|
|
248
|
+
key: 'material',
|
|
249
|
+
cook() {
|
|
250
|
+
cookCalls += 1;
|
|
251
|
+
throw new Error('Engine-owned material must not enter the project cooker');
|
|
252
|
+
},
|
|
253
|
+
} satisfies NativeCooker,
|
|
254
|
+
],
|
|
255
|
+
policy: {
|
|
256
|
+
base: '/',
|
|
257
|
+
packagePath: 'assets/base-material.pack.json',
|
|
258
|
+
artifactPath: (guid, key) => `${guid}/${key}.bin`,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(result.ok).toBe(true);
|
|
263
|
+
expect(cookCalls).toBe(0);
|
|
264
|
+
if (!result.ok) return;
|
|
265
|
+
expect(result.value.finalized.pack.assets).toMatchObject([
|
|
266
|
+
{
|
|
267
|
+
kind: 'material',
|
|
268
|
+
payload: {
|
|
269
|
+
passes: [
|
|
270
|
+
{ program: { module: 'forgeax::default-unlit' } },
|
|
271
|
+
{ program: { module: 'forgeax::default-shadow-caster' } },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
artifacts: {},
|
|
275
|
+
},
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
86
278
|
});
|
|
@@ -174,6 +174,7 @@ describe('standard ScriptablePack output producers', () => {
|
|
|
174
174
|
audio: 'ordinary-pod/1',
|
|
175
175
|
equirect: 'ordinary-pod/1',
|
|
176
176
|
font: 'ordinary-pod/1',
|
|
177
|
+
'ies-profile': 'ordinary-pod/1',
|
|
177
178
|
material: 'material-pack/1',
|
|
178
179
|
mesh: 'mesh-binary/4',
|
|
179
180
|
'particle-effect': 'ordinary-pod/1',
|
|
@@ -206,6 +207,7 @@ describe('standard ScriptablePack output producers', () => {
|
|
|
206
207
|
'animation-clip',
|
|
207
208
|
'animation-graph',
|
|
208
209
|
'audio',
|
|
210
|
+
'ies-profile',
|
|
209
211
|
'particle-effect',
|
|
210
212
|
] as const;
|
|
211
213
|
|