@forgeax/engine-pack 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 +159 -46
- package/dist/__tests__/inventory-schema.test.d.ts +2 -0
- package/dist/__tests__/inventory-schema.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parameterized-pack.unit.test.d.ts +2 -0
- package/dist/__tests__/parameterized-pack.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts +2 -0
- package/dist/__tests__/scanner-blacklist.test.d.ts.map +1 -0
- package/dist/build.d.ts +9 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +3314 -803
- package/dist/build.mjs.map +1 -1
- package/dist/builtin.mjs +37 -4
- package/dist/builtin.mjs.map +1 -1
- package/dist/catalog-builder.d.ts.map +1 -1
- package/dist/cli-asset.d.ts.map +1 -1
- package/dist/cli-asset.mjs +1368 -850
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/deriveAssetName.d.ts +11 -7
- package/dist/deriveAssetName.d.ts.map +1 -1
- package/dist/evidence/source-inventory.d.ts.map +1 -1
- package/dist/guid.d.ts +15 -0
- package/dist/guid.d.ts.map +1 -1
- package/dist/guid.mjs +53 -5
- package/dist/guid.mjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1252 -560
- package/dist/index.mjs.map +1 -1
- package/dist/inventory/binding.d.ts +6 -0
- package/dist/inventory/binding.d.ts.map +1 -0
- package/dist/inventory/declaration.d.ts +24 -0
- package/dist/inventory/declaration.d.ts.map +1 -0
- package/dist/inventory/index.d.ts +3 -0
- package/dist/inventory/index.d.ts.map +1 -0
- package/dist/inventory/sync.d.ts +5 -0
- package/dist/inventory/sync.d.ts.map +1 -0
- package/dist/name.mjs +4 -7
- package/dist/name.mjs.map +1 -1
- package/dist/package-finalizer.d.ts +2 -20
- package/dist/package-finalizer.d.ts.map +1 -1
- package/dist/parameterized-pack-node.d.ts +26 -0
- package/dist/parameterized-pack-node.d.ts.map +1 -0
- package/dist/parameterized-pack-node.mjs +11093 -0
- package/dist/parameterized-pack-node.mjs.map +1 -0
- package/dist/parameterized-pack.d.ts +233 -0
- package/dist/parameterized-pack.d.ts.map +1 -0
- package/dist/resolve-asset-source.d.ts +1 -3
- package/dist/resolve-asset-source.d.ts.map +1 -1
- package/dist/resolve-asset-source.mjs +6 -81
- package/dist/resolve-asset-source.mjs.map +1 -1
- package/dist/runtime.mjs +77 -7
- package/dist/runtime.mjs.map +1 -1
- package/dist/scanner.d.ts +19 -8
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +1169 -690
- package/dist/scanner.mjs.map +1 -1
- package/dist/schema-compiled.d.ts.map +1 -1
- package/dist/schema.mjs +79 -7
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +8 -32
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +576 -1021
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +29 -11
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +19 -532
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +1042 -527
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +7 -7
- package/schema/meta.schema.json +1 -1
- package/schema/pack.schema.json +76 -6
- package/src/__tests__/guid-collision.unit.test.ts +18 -9
- package/src/__tests__/inventory-schema.test.ts +109 -0
- package/src/__tests__/pack.unit.test.ts +1 -238
- package/src/__tests__/package-finalizer.contract.test.ts +55 -0
- package/src/__tests__/parameterized-pack-authoring-gateway.unit.test.ts +249 -0
- package/src/__tests__/parameterized-pack-scanner.unit.test.ts +123 -0
- package/src/__tests__/parameterized-pack.unit.test.ts +266 -0
- package/src/__tests__/resolve-asset-source.test.ts +15 -98
- package/src/__tests__/scanner-blacklist.test.ts +55 -0
- package/src/__tests__/scanner-inventory.contract.test.ts +14 -15
- package/src/__tests__/scanner-inventory.test.ts +24 -12
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +4 -9
- package/src/__tests__/topology.unit.test.ts +23 -0
- package/src/build.ts +28 -18
- package/src/catalog-builder.ts +61 -34
- package/src/cli-asset.ts +70 -85
- package/src/deriveAssetName.ts +14 -13
- package/src/evidence/source-inventory.ts +34 -21
- package/src/guid.ts +65 -4
- package/src/index.ts +12 -11
- package/src/inventory/binding.ts +25 -0
- package/src/inventory/declaration.ts +168 -0
- package/src/inventory/index.ts +18 -0
- package/src/inventory/sync.ts +22 -0
- package/src/package-finalizer.ts +2 -42
- package/src/parameterized-pack-node.ts +1968 -0
- package/src/parameterized-pack.ts +1493 -0
- package/src/resolve-asset-source.ts +4 -76
- package/src/scanner.ts +241 -59
- package/src/schema-compiled.ts +2 -0
- package/src/scriptable-pack-node.ts +111 -722
- package/src/scriptable-pack-worker.ts +48 -16
- package/src/scriptable-pack.ts +27 -939
- package/dist/.tsbuildinfo +0 -1
- package/dist/__tests__/load-asset-config.test.d.ts +0 -2
- package/dist/__tests__/load-asset-config.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.test-d.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.test-d.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack.unit.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack.unit.test.d.ts.map +0 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +0 -1
- package/dist/config.mjs +0 -29
- package/dist/config.mjs.map +0 -1
- package/src/__tests__/load-asset-config.test.ts +0 -121
- package/src/__tests__/scriptable-pack.test-d.ts +0 -131
- package/src/__tests__/scriptable-pack.unit.test.ts +0 -789
- package/src/config.ts +0 -36
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { scanEntries } from '../cli-asset.js';
|
|
6
|
+
import { scanInventory } from '../scanner.js';
|
|
7
|
+
|
|
8
|
+
const SOURCE_PACKAGE = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
|
|
9
|
+
const INSTANCE_PACKAGE = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
|
|
10
|
+
const DIRECT_PACKAGE = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
|
|
11
|
+
|
|
12
|
+
const roots: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
const root = roots.pop();
|
|
16
|
+
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('parameterized Pack scanner', () => {
|
|
20
|
+
it('indexes direct v3 output keys as derived GUIDs without requiring entry GUIDs', async () => {
|
|
21
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-parameterized-scan-'));
|
|
22
|
+
roots.push(root);
|
|
23
|
+
const path = join(root, 'direct.pack.json');
|
|
24
|
+
await writeFile(
|
|
25
|
+
path,
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
schemaVersion: '3.0.0',
|
|
28
|
+
packageId: DIRECT_PACKAGE,
|
|
29
|
+
assets: {
|
|
30
|
+
'scene/main': {
|
|
31
|
+
kind: 'scene',
|
|
32
|
+
payload: { kind: 'scene', nodes: [] },
|
|
33
|
+
refs: [],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const result = await scanInventory([root]);
|
|
40
|
+
|
|
41
|
+
expect(result.ok).toBe(true);
|
|
42
|
+
if (!result.ok) return;
|
|
43
|
+
expect(result.value.inventory).toMatchObject([
|
|
44
|
+
{
|
|
45
|
+
kind: 'scene',
|
|
46
|
+
sourcePath: path,
|
|
47
|
+
sourceKey: 'scene/main',
|
|
48
|
+
sourceIndex: 0,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
expect(result.value.inventory[0]?.guid).toMatch(
|
|
52
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const stdout: string[] = [];
|
|
56
|
+
const stderr: string[] = [];
|
|
57
|
+
const listed = await scanEntries([root], {
|
|
58
|
+
stdoutWrite: (line) => stdout.push(line),
|
|
59
|
+
stderrWrite: (line) => stderr.push(line),
|
|
60
|
+
});
|
|
61
|
+
expect(listed).toMatchObject({
|
|
62
|
+
ok: true,
|
|
63
|
+
value: [{ guid: result.value.inventory[0]?.guid, name: 'scene/main' }],
|
|
64
|
+
});
|
|
65
|
+
expect(stderr).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('captures a parameterized source as metadata and resolves an instance parent', async () => {
|
|
69
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-parameterized-source-scan-'));
|
|
70
|
+
roots.push(root);
|
|
71
|
+
await writeFile(
|
|
72
|
+
join(root, 'source.pack.ts'),
|
|
73
|
+
[
|
|
74
|
+
'const packageId = new Uint8Array(16);',
|
|
75
|
+
'packageId.set([170, 170, 170, 170, 170, 170, 74, 170, 138, 170, 170, 170, 170, 170, 170, 170]);',
|
|
76
|
+
'export default {',
|
|
77
|
+
" schemaVersion: '2.0.0',",
|
|
78
|
+
' packageId,',
|
|
79
|
+
" parameters: [{ name: 'count', type: 'u32', default: 1 }],",
|
|
80
|
+
" build: ({ values }) => ({ ok: true, value: { ['mesh/' + values.count]: { kind: 'mesh', materialSlots: [] } } }),",
|
|
81
|
+
'};',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
);
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(root, 'instance.pack.json'),
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
schemaVersion: '3.0.0',
|
|
88
|
+
packageId: INSTANCE_PACKAGE,
|
|
89
|
+
parent: SOURCE_PACKAGE,
|
|
90
|
+
values: { count: 2 },
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = await scanInventory([root]);
|
|
95
|
+
|
|
96
|
+
expect(result.ok).toBe(true);
|
|
97
|
+
if (!result.ok) return;
|
|
98
|
+
const declaration = result.value.declarations.get(join(root, 'source.pack.ts'));
|
|
99
|
+
expect(declaration?.format).toBe('pack.ts-parameterized');
|
|
100
|
+
expect(result.value.inventory).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('fails closed when an instance parent is missing', async () => {
|
|
104
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-parameterized-parent-scan-'));
|
|
105
|
+
roots.push(root);
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(root, 'missing.pack.json'),
|
|
108
|
+
JSON.stringify({
|
|
109
|
+
schemaVersion: '3.0.0',
|
|
110
|
+
packageId: INSTANCE_PACKAGE,
|
|
111
|
+
parent: SOURCE_PACKAGE,
|
|
112
|
+
values: { count: 2 },
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const result = await scanInventory([root]);
|
|
117
|
+
|
|
118
|
+
expect(result.ok).toBe(false);
|
|
119
|
+
if (result.ok) return;
|
|
120
|
+
expect(result.error.code).toBe('pack-malformed-pack');
|
|
121
|
+
expect(result.error.detail).toMatchObject({ reason: 'pack-parent-not-found' });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { ok } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { AssetGuid, PackageId } from '../guid.js';
|
|
4
|
+
import {
|
|
5
|
+
definePack,
|
|
6
|
+
definePackageId,
|
|
7
|
+
type PackParameterInheritanceSubject,
|
|
8
|
+
parseParameterizedPackJson,
|
|
9
|
+
projectDirectPackJson,
|
|
10
|
+
resolvePackParameterInheritance,
|
|
11
|
+
resolvePackParameterValues,
|
|
12
|
+
validatePackDefinition,
|
|
13
|
+
} from '../parameterized-pack.js';
|
|
14
|
+
|
|
15
|
+
const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
16
|
+
|
|
17
|
+
function packageId(value: string) {
|
|
18
|
+
const result = PackageId.parse(value);
|
|
19
|
+
if (!result.ok) throw result.error;
|
|
20
|
+
return result.value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('parameterized Pack authoring', () => {
|
|
24
|
+
it('derives the RFC 4122 UUIDv5 vector and keeps PackageId branded separately', () => {
|
|
25
|
+
const namespace = packageId(NAMESPACE);
|
|
26
|
+
expect(AssetGuid.format(AssetGuid.derive(namespace, 'www.widgets.com'))).toBe(
|
|
27
|
+
'21f7f8de-8051-5b89-8680-0195ef798b6a',
|
|
28
|
+
);
|
|
29
|
+
expect(PackageId.format(namespace)).toBe(NAMESPACE);
|
|
30
|
+
expect(AssetGuid.derive(namespace, 'mesh/a')).not.toBe(namespace);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('derives an explicit context shape for zero-parameter and parameterized Packs', async () => {
|
|
34
|
+
const zero = definePack({
|
|
35
|
+
schemaVersion: '2.0.0',
|
|
36
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000001'),
|
|
37
|
+
build: ({ packageId: subjectId }) =>
|
|
38
|
+
ok({
|
|
39
|
+
'scene/main': {
|
|
40
|
+
kind: 'scene',
|
|
41
|
+
entities: [],
|
|
42
|
+
mounts: [],
|
|
43
|
+
source: AssetGuid.format(AssetGuid.derive(subjectId, 'mesh/main')),
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
expect('parameters' in zero).toBe(false);
|
|
48
|
+
const zeroResult = await zero.build({
|
|
49
|
+
packageId: zero.packageId,
|
|
50
|
+
readByGuid: async () => ok(null as never),
|
|
51
|
+
});
|
|
52
|
+
expect(zeroResult.ok).toBe(true);
|
|
53
|
+
|
|
54
|
+
const parameterized = definePack({
|
|
55
|
+
schemaVersion: '2.0.0',
|
|
56
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000002'),
|
|
57
|
+
parameters: [
|
|
58
|
+
{ name: 'segments', type: 'u32', default: 12, minimum: 3, maximum: 128 },
|
|
59
|
+
{ name: 'style', type: 'enum', values: ['oak', 'pine'], default: 'oak' },
|
|
60
|
+
] as const,
|
|
61
|
+
build: ({ values }) =>
|
|
62
|
+
ok({
|
|
63
|
+
'scene/main': {
|
|
64
|
+
kind: 'scene',
|
|
65
|
+
entities: values.segments >= 0 ? [] : [],
|
|
66
|
+
mounts: [],
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
const values = resolvePackParameterValues(parameterized, { segments: 24 });
|
|
71
|
+
expect(values).toMatchObject({ ok: true, value: { segments: 24, style: 'oak' } });
|
|
72
|
+
expect(resolvePackParameterValues(parameterized, { unknown: true })).toMatchObject({
|
|
73
|
+
ok: false,
|
|
74
|
+
error: { code: 'pack-parameter-invalid' },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const bounded = definePack({
|
|
78
|
+
schemaVersion: '2.0.0',
|
|
79
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000003'),
|
|
80
|
+
parameters: [
|
|
81
|
+
{ name: 'unsigned', type: 'u32', default: 0 },
|
|
82
|
+
{ name: 'signed', type: 'i32', default: 0 },
|
|
83
|
+
{ name: 'float', type: 'f32', default: 0 },
|
|
84
|
+
] as const,
|
|
85
|
+
build: () => ok({}),
|
|
86
|
+
});
|
|
87
|
+
expect(resolvePackParameterValues(bounded, { unsigned: 0x1_0000_0000 })).toMatchObject({
|
|
88
|
+
ok: false,
|
|
89
|
+
error: { code: 'pack-parameter-invalid' },
|
|
90
|
+
});
|
|
91
|
+
expect(resolvePackParameterValues(bounded, { signed: 0x8000_0000 })).toMatchObject({
|
|
92
|
+
ok: false,
|
|
93
|
+
error: { code: 'pack-parameter-invalid' },
|
|
94
|
+
});
|
|
95
|
+
expect(resolvePackParameterValues(bounded, { float: Number.MAX_VALUE })).toMatchObject({
|
|
96
|
+
ok: false,
|
|
97
|
+
error: { code: 'pack-parameter-invalid' },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('validates and preserves optional scene component schemas without exposing output identity', () => {
|
|
102
|
+
const definition = definePack({
|
|
103
|
+
schemaVersion: '2.0.0',
|
|
104
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000004'),
|
|
105
|
+
sceneComponents: [
|
|
106
|
+
{ name: 'Transform', fields: { pos: { type: 'vec3' }, quat: 'quat' } },
|
|
107
|
+
{ name: 'Name', fields: { value: 'string' } },
|
|
108
|
+
],
|
|
109
|
+
build: () => ok({}),
|
|
110
|
+
});
|
|
111
|
+
expect(definition.sceneComponents).toEqual([
|
|
112
|
+
{ name: 'Transform', fields: { pos: { type: 'vec3' }, quat: 'quat' } },
|
|
113
|
+
{ name: 'Name', fields: { value: 'string' } },
|
|
114
|
+
]);
|
|
115
|
+
expect(Object.isFrozen(definition.sceneComponents)).toBe(true);
|
|
116
|
+
expect(Object.isFrozen(definition.sceneComponents?.[0])).toBe(true);
|
|
117
|
+
expect(Object.isFrozen(definition.sceneComponents?.[0]?.fields)).toBe(true);
|
|
118
|
+
|
|
119
|
+
const duplicate = validatePackDefinition({
|
|
120
|
+
schemaVersion: '2.0.0',
|
|
121
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000005'),
|
|
122
|
+
sceneComponents: [
|
|
123
|
+
{ name: 'Transform', fields: {} },
|
|
124
|
+
{ name: 'Transform', fields: {} },
|
|
125
|
+
],
|
|
126
|
+
build: () => ok({}),
|
|
127
|
+
});
|
|
128
|
+
expect(duplicate).toMatchObject({
|
|
129
|
+
ok: false,
|
|
130
|
+
error: {
|
|
131
|
+
code: 'pack-parameter-invalid',
|
|
132
|
+
detail: { reason: 'scene component names must be unique' },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const malformed = validatePackDefinition({
|
|
136
|
+
schemaVersion: '2.0.0',
|
|
137
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000006'),
|
|
138
|
+
sceneComponents: [{ name: 'Transform', fields: { pos: { type: '' } } }],
|
|
139
|
+
build: () => ok({}),
|
|
140
|
+
});
|
|
141
|
+
expect(malformed).toMatchObject({
|
|
142
|
+
ok: false,
|
|
143
|
+
error: {
|
|
144
|
+
code: 'pack-parameter-invalid',
|
|
145
|
+
detail: { reason: 'scene component field schema is malformed' },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('parses direct and instance JSON without accepting GUID fields', () => {
|
|
151
|
+
const direct = parseParameterizedPackJson({
|
|
152
|
+
schemaVersion: '3.0.0',
|
|
153
|
+
packageId: '01900000-0000-7000-8000-000000000010',
|
|
154
|
+
assets: {
|
|
155
|
+
'scene/main': { kind: 'scene', payload: {}, refs: [] },
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
expect(direct.ok).toBe(true);
|
|
159
|
+
if (direct.ok && direct.value.format === 'direct') {
|
|
160
|
+
const projected = projectDirectPackJson(direct.value);
|
|
161
|
+
expect(projected).toMatchObject({
|
|
162
|
+
ok: true,
|
|
163
|
+
value: {
|
|
164
|
+
packageId: '01900000-0000-7000-8000-000000000010',
|
|
165
|
+
assets: [{ sourceKey: 'scene/main', guid: expect.any(String) }],
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
expect(
|
|
170
|
+
parseParameterizedPackJson({
|
|
171
|
+
schemaVersion: '3.0.0',
|
|
172
|
+
packageId: '01900000-0000-7000-8000-000000000010',
|
|
173
|
+
assets: { 'scene/main': { guid: NAMESPACE, kind: 'scene', payload: {}, refs: [] } },
|
|
174
|
+
}),
|
|
175
|
+
).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
|
|
176
|
+
expect(
|
|
177
|
+
parseParameterizedPackJson({
|
|
178
|
+
schemaVersion: '3.0.0',
|
|
179
|
+
packageId: '01900000-0000-7000-8000-000000000020',
|
|
180
|
+
parent: '01900000-0000-7000-8000-000000000002',
|
|
181
|
+
values: { segments: 24 },
|
|
182
|
+
}),
|
|
183
|
+
).toMatchObject({ ok: true, value: { format: 'instance' } });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('rejects legacy static output fields in the v2 source definition', () => {
|
|
187
|
+
expect(() =>
|
|
188
|
+
definePack({
|
|
189
|
+
schemaVersion: '2.0.0',
|
|
190
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000021'),
|
|
191
|
+
assets: [],
|
|
192
|
+
build: () => ok({}),
|
|
193
|
+
} as never),
|
|
194
|
+
).toThrow(/repair the parameter declaration|authoring/);
|
|
195
|
+
expect(() =>
|
|
196
|
+
definePack({
|
|
197
|
+
schemaVersion: '2.0.0',
|
|
198
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000022'),
|
|
199
|
+
externalAssets: [],
|
|
200
|
+
build: () => ok({}),
|
|
201
|
+
} as never),
|
|
202
|
+
).toThrow();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('rejects unknown parameter descriptor fields instead of carrying a second schema', () => {
|
|
206
|
+
expect(() =>
|
|
207
|
+
definePack({
|
|
208
|
+
schemaVersion: '2.0.0',
|
|
209
|
+
packageId: definePackageId('01900000-0000-7000-8000-000000000023'),
|
|
210
|
+
parameters: [{ name: 'count', type: 'u32', default: 1, editor: 'slider' }] as never,
|
|
211
|
+
build: () => ok({}),
|
|
212
|
+
}),
|
|
213
|
+
).toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('resolves finite shallow inheritance and reports missing parent and cycle', async () => {
|
|
217
|
+
const rootId = packageId('01900000-0000-7000-8000-000000000030');
|
|
218
|
+
const middleId = packageId('01900000-0000-7000-8000-000000000031');
|
|
219
|
+
const leafId = packageId('01900000-0000-7000-8000-000000000032');
|
|
220
|
+
const root: PackParameterInheritanceSubject = {
|
|
221
|
+
format: 'source',
|
|
222
|
+
packageId: rootId,
|
|
223
|
+
parameters: [{ name: 'segments', type: 'u32', default: 12, minimum: 3 }],
|
|
224
|
+
};
|
|
225
|
+
const middle: PackParameterInheritanceSubject = {
|
|
226
|
+
format: 'instance',
|
|
227
|
+
packageId: middleId,
|
|
228
|
+
parent: rootId,
|
|
229
|
+
values: { segments: 24 },
|
|
230
|
+
};
|
|
231
|
+
const leaf: PackParameterInheritanceSubject = {
|
|
232
|
+
format: 'instance',
|
|
233
|
+
packageId: leafId,
|
|
234
|
+
parent: middleId,
|
|
235
|
+
values: {},
|
|
236
|
+
};
|
|
237
|
+
const subjects = new Map<string, PackParameterInheritanceSubject>([
|
|
238
|
+
[PackageId.format(rootId), root],
|
|
239
|
+
[PackageId.format(middleId), middle],
|
|
240
|
+
]);
|
|
241
|
+
const resolved = await resolvePackParameterInheritance(leaf, (id) =>
|
|
242
|
+
subjects.get(PackageId.format(id)),
|
|
243
|
+
);
|
|
244
|
+
expect(resolved).toMatchObject({
|
|
245
|
+
ok: true,
|
|
246
|
+
value: {
|
|
247
|
+
values: { segments: 24 },
|
|
248
|
+
parentChain: [PackageId.format(middleId), PackageId.format(rootId)],
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const missing = await resolvePackParameterInheritance(leaf, () => undefined);
|
|
253
|
+
expect(missing).toMatchObject({ ok: false, error: { code: 'pack-parent-not-found' } });
|
|
254
|
+
|
|
255
|
+
const cycleA: PackParameterInheritanceSubject = {
|
|
256
|
+
format: 'instance',
|
|
257
|
+
packageId: middleId,
|
|
258
|
+
parent: leafId,
|
|
259
|
+
values: {},
|
|
260
|
+
};
|
|
261
|
+
const cycle = await resolvePackParameterInheritance(cycleA, (id) =>
|
|
262
|
+
PackageId.format(id) === PackageId.format(leafId) ? leaf : cycleA,
|
|
263
|
+
);
|
|
264
|
+
expect(cycle).toMatchObject({ ok: false, error: { code: 'pack-parent-cycle' } });
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
// @ts-nocheck — PackErrorDetail is a discriminated union narrowed by runtime .code
|
|
2
|
-
// checks; TS cannot follow the narrowing through PackError.detail.
|
|
3
1
|
import { resolve } from 'node:path';
|
|
4
2
|
import { describe, expect, it } from 'vitest';
|
|
5
3
|
import { resolveAssetSource } from '../resolve-asset-source.js';
|
|
6
4
|
|
|
7
|
-
describe('resolveAssetSource —
|
|
5
|
+
describe('resolveAssetSource — companion and sidecar-relative sources', () => {
|
|
8
6
|
const metaDir = '/project/assets';
|
|
9
7
|
|
|
10
|
-
it('omitted source
|
|
8
|
+
it('derives an omitted source from the companion filename', () => {
|
|
11
9
|
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
12
|
-
|
|
13
|
-
expect(result.ok).toBe(true);
|
|
14
|
-
if (result.ok) {
|
|
15
|
-
expect(result.value).toBe(resolve(metaDir, 'foo.png'));
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('renamed audio meta derives correct companion (AC-3)', () => {
|
|
20
|
-
const metaPath = resolve(metaDir, 'bleep.wav.meta.json');
|
|
21
|
-
const result = resolveAssetSource(metaPath, undefined, {});
|
|
22
|
-
expect(result.ok).toBe(true);
|
|
23
|
-
if (result.ok) {
|
|
24
|
-
expect(result.value).toBe(resolve(metaDir, 'bleep.wav'));
|
|
25
|
-
}
|
|
10
|
+
expect(resolveAssetSource(metaPath, undefined)).toBe(resolve(metaDir, 'foo.png'));
|
|
26
11
|
});
|
|
27
12
|
|
|
28
|
-
it('
|
|
13
|
+
it('supports every ordinary companion extension without a special table', () => {
|
|
29
14
|
const cases = [
|
|
30
15
|
{ meta: 'model.glb.meta.json', expected: 'model.glb' },
|
|
31
16
|
{ meta: 'sprite.png.meta.json', expected: 'sprite.png' },
|
|
@@ -33,89 +18,21 @@ describe('resolveAssetSource — omit derivation (w4)', () => {
|
|
|
33
18
|
{ meta: 'bgm.mp3.meta.json', expected: 'bgm.mp3' },
|
|
34
19
|
];
|
|
35
20
|
for (const { meta, expected } of cases) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (result.ok) {
|
|
40
|
-
expect(result.value).toBe(resolve(metaDir, expected));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('resolveAssetSource — @name/ path + boundary + legacy (w5)', () => {
|
|
47
|
-
const metaDir = '/project/assets';
|
|
48
|
-
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
49
|
-
|
|
50
|
-
it('resolves @name/rest to absolute path via paths table (AC-5)', () => {
|
|
51
|
-
const paths = { shared: '/lib/assets' };
|
|
52
|
-
const result = resolveAssetSource(metaPath, '@shared/foo.png', paths);
|
|
53
|
-
expect(result.ok).toBe(true);
|
|
54
|
-
if (result.ok) {
|
|
55
|
-
expect(result.value).toBe(resolve('/lib/assets', 'foo.png'));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('returns pack-unknown-path for undeclared @name (AC-8)', () => {
|
|
60
|
-
const paths = { shared: '/lib/assets' };
|
|
61
|
-
const result = resolveAssetSource(metaPath, '@nope/x.png', paths);
|
|
62
|
-
expect(result.ok).toBe(false);
|
|
63
|
-
if (!result.ok) {
|
|
64
|
-
expect(result.error.code).toBe('pack-unknown-path');
|
|
65
|
-
expect(result.error.detail.pathName).toBe('nope');
|
|
66
|
-
expect(result.error.detail.knownNames).toEqual(['shared']);
|
|
21
|
+
expect(resolveAssetSource(resolve(metaDir, meta), undefined)).toBe(
|
|
22
|
+
resolve(metaDir, expected),
|
|
23
|
+
);
|
|
67
24
|
}
|
|
68
25
|
});
|
|
69
26
|
|
|
70
|
-
it('
|
|
71
|
-
const
|
|
72
|
-
expect(
|
|
73
|
-
if (!result.ok) {
|
|
74
|
-
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
75
|
-
expect(result.error.detail.rawSource).toBe('@');
|
|
76
|
-
if (result.error.detail.code === 'pack-malformed-path-ref') {
|
|
77
|
-
expect(result.error.detail.reason).toBe('format');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('returns pack-malformed-path-ref for @onlyname with no /rest (AC-9)', () => {
|
|
83
|
-
const result = resolveAssetSource(metaPath, '@onlyname', { shared: '/lib/assets' });
|
|
84
|
-
expect(result.ok).toBe(false);
|
|
85
|
-
if (!result.ok) {
|
|
86
|
-
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
87
|
-
expect(result.error.detail.rawSource).toBe('@onlyname');
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('returns pack-malformed-path-ref for ../ escape (AC-11)', () => {
|
|
92
|
-
const paths = { shared: '/data/shared' };
|
|
93
|
-
const result = resolveAssetSource(metaPath, '@shared/../escape.png', paths);
|
|
94
|
-
expect(result.ok).toBe(false);
|
|
95
|
-
if (!result.ok) {
|
|
96
|
-
expect(result.error.code).toBe('pack-malformed-path-ref');
|
|
97
|
-
expect(result.error.detail.rawSource).toBe('@shared/../escape.png');
|
|
98
|
-
if (result.error.detail.code === 'pack-malformed-path-ref') {
|
|
99
|
-
expect(result.error.detail.reason).toBe('escape');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('resolves legacy relative path unchanged', () => {
|
|
105
|
-
const result = resolveAssetSource(metaPath, 'sub/foo.png', {});
|
|
106
|
-
expect(result.ok).toBe(true);
|
|
107
|
-
if (result.ok) {
|
|
108
|
-
expect(result.value).toBe(resolve(metaDir, 'sub/foo.png'));
|
|
109
|
-
}
|
|
27
|
+
it('resolves an explicit source relative to its sidecar', () => {
|
|
28
|
+
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
29
|
+
expect(resolveAssetSource(metaPath, 'sub/foo.png')).toBe(resolve(metaDir, 'sub/foo.png'));
|
|
110
30
|
});
|
|
111
31
|
|
|
112
|
-
it('
|
|
113
|
-
const
|
|
114
|
-
expect(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(result.error.detail.pathName).toBe('any');
|
|
118
|
-
expect(result.error.detail.knownNames).toEqual([]);
|
|
119
|
-
}
|
|
32
|
+
it('keeps source resolution independent of package.json and path aliases', () => {
|
|
33
|
+
const metaPath = resolve(metaDir, 'foo.png.meta.json');
|
|
34
|
+
expect(resolveAssetSource(metaPath, '@shared/foo.png')).toBe(
|
|
35
|
+
resolve(metaDir, '@shared/foo.png'),
|
|
36
|
+
);
|
|
120
37
|
});
|
|
121
38
|
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { scan } from '../scanner.js';
|
|
6
|
+
|
|
7
|
+
const EMPTY_PACK = JSON.stringify({
|
|
8
|
+
schemaVersion: '1.0.0',
|
|
9
|
+
kind: 'internal-text-package',
|
|
10
|
+
assets: [],
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('asset scanner test-directory boundary', () => {
|
|
14
|
+
const temporaryRoots: string[] = [];
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await Promise.all(
|
|
18
|
+
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('skips arbitrarily nested __tests__ declarations while keeping normal assets', async () => {
|
|
23
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-scanner-tests-'));
|
|
24
|
+
temporaryRoots.push(root);
|
|
25
|
+
const visible = join(root, 'world', 'scene.pack.json');
|
|
26
|
+
const nestedFixture = join(root, 'world', '__tests__', 'fixture.pack.json');
|
|
27
|
+
const rootFixture = join(root, '__tests__', 'nested', 'fixture.pack.json');
|
|
28
|
+
|
|
29
|
+
await mkdir(join(root, 'world', '__tests__'), { recursive: true });
|
|
30
|
+
await mkdir(join(root, '__tests__', 'nested'), { recursive: true });
|
|
31
|
+
await writeFile(visible, EMPTY_PACK);
|
|
32
|
+
// Deliberately malformed: a test fixture must not be able to degrade the
|
|
33
|
+
// production scan simply because it lives below the asset root.
|
|
34
|
+
await writeFile(nestedFixture, '{}');
|
|
35
|
+
await writeFile(rootFixture, '{}');
|
|
36
|
+
|
|
37
|
+
const result = await scan([root]);
|
|
38
|
+
|
|
39
|
+
expect(result).toMatchObject({ ok: true, value: [visible] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('allows an explicit __tests__ root when a test intentionally opts in', async () => {
|
|
43
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-scanner-tests-'));
|
|
44
|
+
temporaryRoots.push(root);
|
|
45
|
+
const testRoot = join(root, '__tests__');
|
|
46
|
+
const fixture = join(testRoot, 'fixture.pack.json');
|
|
47
|
+
|
|
48
|
+
await mkdir(testRoot, { recursive: true });
|
|
49
|
+
await writeFile(fixture, EMPTY_PACK);
|
|
50
|
+
|
|
51
|
+
const result = await scan([testRoot]);
|
|
52
|
+
|
|
53
|
+
expect(result).toMatchObject({ ok: true, value: [fixture] });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -88,17 +88,14 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
88
88
|
await writeFile(scriptablePath, '// scanner fixture');
|
|
89
89
|
|
|
90
90
|
const scriptablePackageId = guid('019e3969-1d48-7c3b-ac24-6d68f4570662');
|
|
91
|
-
const scriptableAssetGuid = guid('019e3969-1d48-7c3b-ac24-6d68f4570663');
|
|
92
91
|
const result = await scanInventory([root], {
|
|
93
92
|
scriptablePack: {
|
|
94
93
|
executor: {
|
|
95
94
|
load: async () => ({
|
|
96
95
|
default: {
|
|
97
|
-
schemaVersion: '
|
|
96
|
+
schemaVersion: '2.0.0',
|
|
98
97
|
packageId: scriptablePackageId,
|
|
99
|
-
|
|
100
|
-
externalAssets: {},
|
|
101
|
-
build: () => ({ ok: true, value: {} }),
|
|
98
|
+
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities: [] } } }),
|
|
102
99
|
},
|
|
103
100
|
}),
|
|
104
101
|
},
|
|
@@ -120,17 +117,17 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
120
117
|
});
|
|
121
118
|
const scriptableDeclaration = result.value.declarations.get(scriptablePath);
|
|
122
119
|
expect(scriptableDeclaration).toMatchObject({
|
|
123
|
-
format: 'pack.ts',
|
|
120
|
+
format: 'pack.ts-parameterized',
|
|
124
121
|
sourcePath: scriptablePath,
|
|
125
122
|
value: {
|
|
126
|
-
importer: 'pack-ts',
|
|
127
123
|
packageId: '019e3969-1d48-7c3b-ac24-6d68f4570662',
|
|
128
|
-
subAssets: [{ sourceKey: 'effect', kind: 'scene' }],
|
|
129
124
|
},
|
|
130
125
|
});
|
|
131
126
|
expect(
|
|
132
|
-
scriptableDeclaration?.format === 'pack.ts'
|
|
133
|
-
|
|
127
|
+
scriptableDeclaration?.format === 'pack.ts-parameterized'
|
|
128
|
+
? scriptableDeclaration.value
|
|
129
|
+
: undefined,
|
|
130
|
+
).not.toHaveProperty('subAssets');
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
it('releases the ScriptablePack loader for the path-only scan contract', async () => {
|
|
@@ -140,18 +137,15 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
140
137
|
await writeFile(scriptablePath, '// scanner fixture');
|
|
141
138
|
|
|
142
139
|
const packageId = guid('019e3969-1d48-7c3b-ac24-6d68f4570664');
|
|
143
|
-
const assetGuid = guid('019e3969-1d48-7c3b-ac24-6d68f4570665');
|
|
144
140
|
let disposals = 0;
|
|
145
141
|
const result = await scan([root], {
|
|
146
142
|
scriptablePack: {
|
|
147
143
|
executor: {
|
|
148
144
|
load: async () => ({
|
|
149
145
|
default: {
|
|
150
|
-
schemaVersion: '
|
|
146
|
+
schemaVersion: '2.0.0',
|
|
151
147
|
packageId,
|
|
152
|
-
|
|
153
|
-
externalAssets: {},
|
|
154
|
-
build: () => ({ ok: true, value: {} }),
|
|
148
|
+
build: () => ({ ok: true, value: { effect: { kind: 'scene', entities: [] } } }),
|
|
155
149
|
},
|
|
156
150
|
}),
|
|
157
151
|
dispose: async () => {
|
|
@@ -164,4 +158,9 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
164
158
|
expect(result.ok).toBe(true);
|
|
165
159
|
expect(disposals).toBe(1);
|
|
166
160
|
});
|
|
161
|
+
|
|
162
|
+
it('characterizes the public scanner inventory vocabulary', () => {
|
|
163
|
+
expect(typeof scan).toBe('function');
|
|
164
|
+
expect(typeof scanInventory).toBe('function');
|
|
165
|
+
});
|
|
167
166
|
});
|