@forgeax/engine-pack 0.1.23 → 0.1.25
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 +12 -6
- package/dist/build.d.ts +4 -4
- package/dist/build.d.ts.map +1 -1
- package/dist/build.mjs +390 -294
- package/dist/build.mjs.map +1 -1
- package/dist/cli-asset.mjs +33 -35
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/evidence/material-cook.d.ts +26 -0
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +121 -27
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.d.ts +1 -1
- package/dist/material-cook.d.ts.map +1 -1
- package/dist/material-cook.mjs +102 -4
- package/dist/material-cook.mjs.map +1 -1
- package/dist/{parameterized-pack-node.d.ts → pack-authoring-node.d.ts} +7 -7
- package/dist/pack-authoring-node.d.ts.map +1 -0
- package/dist/{parameterized-pack-node.mjs → pack-authoring-node.mjs} +59 -63
- package/dist/pack-authoring-node.mjs.map +1 -0
- package/dist/{parameterized-pack.d.ts → pack-authoring.d.ts} +28 -31
- package/dist/pack-authoring.d.ts.map +1 -0
- package/dist/pack-authoring.mjs +1137 -0
- package/dist/pack-authoring.mjs.map +1 -0
- package/dist/runtime.mjs +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/scanner.d.ts +11 -9
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.mjs +36 -34
- package/dist/scanner.mjs.map +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/schema.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts +4 -4
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +18 -20
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +3 -2
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs +19 -23
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -7
- package/schema/pack.schema.json +1 -1
- package/src/__tests__/material-cook-receipt-layer-plan.unit.test.ts +16 -0
- package/src/__tests__/material-cook-schema.unit.test.ts +62 -0
- package/src/__tests__/material-declaration-boundary.unit.test.ts +35 -0
- package/src/__tests__/material-surface-consumer-census.test.ts +24 -0
- package/src/__tests__/material-surface-publication.unit.test.ts +159 -0
- package/src/__tests__/material-surface-wire.unit.test.ts +30 -0
- package/src/__tests__/{parameterized-pack-authoring-gateway.unit.test.ts → pack-authoring-gateway.unit.test.ts} +9 -9
- package/src/__tests__/{parameterized-pack.unit.test.ts → pack-authoring.unit.test.ts} +10 -10
- package/src/__tests__/{parameterized-pack-scanner.unit.test.ts → pack-scanner.unit.test.ts} +6 -6
- package/src/__tests__/scanner-inventory.contract.test.ts +2 -4
- package/src/__tests__/scanner-inventory.test.ts +7 -6
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +1 -1
- package/src/build.ts +10 -8
- package/src/catalog-builder.ts +3 -3
- package/src/cli-asset.ts +5 -5
- package/src/evidence/material-cook.ts +170 -7
- package/src/evidence/source-inventory.ts +3 -3
- package/src/index.ts +6 -1
- package/src/material-cook.ts +3 -0
- package/src/{parameterized-pack-node.ts → pack-authoring-node.ts} +71 -73
- package/src/{parameterized-pack.ts → pack-authoring.ts} +59 -74
- package/src/scanner.ts +33 -31
- package/src/schema/material-cook.schema.json +47 -1
- package/src/scriptable-pack-node.ts +25 -25
- package/src/scriptable-pack.ts +3 -2
- package/dist/__tests__/ai-recovery.unit.test.d.ts +0 -2
- package/dist/__tests__/ai-recovery.unit.test.d.ts.map +0 -1
- package/dist/__tests__/artifact-path.test.d.ts +0 -2
- package/dist/__tests__/artifact-path.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-error-hints.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-lifecycle.test.d.ts.map +0 -1
- package/dist/__tests__/asset-evidence-schema.test.d.ts +0 -2
- package/dist/__tests__/asset-evidence-schema.test.d.ts.map +0 -1
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts +0 -2
- package/dist/__tests__/atlas-cli-region-mismatch.test.d.ts.map +0 -1
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts +0 -2
- package/dist/__tests__/atlas-error-code-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts +0 -2
- package/dist/__tests__/cli-asset-evidence.integration.test.d.ts.map +0 -1
- package/dist/__tests__/cli.unit.test.d.ts +0 -2
- package/dist/__tests__/cli.unit.test.d.ts.map +0 -1
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts +0 -2
- package/dist/__tests__/evidence-source-inventory.unit.test.d.ts.map +0 -1
- package/dist/__tests__/guid-collision.unit.test.d.ts +0 -2
- package/dist/__tests__/guid-collision.unit.test.d.ts.map +0 -1
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts +0 -2
- package/dist/__tests__/guid-validator-surface.unit.test.d.ts.map +0 -1
- package/dist/__tests__/inventory-schema.test.d.ts +0 -2
- package/dist/__tests__/inventory-schema.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-dependencies.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-receipt.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-cook-schema.unit.test.d.ts +0 -2
- package/dist/__tests__/material-cook-schema.unit.test.d.ts.map +0 -1
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts +0 -2
- package/dist/__tests__/mesh-bin-consumer-surface.unit.test.d.ts.map +0 -1
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts +0 -2
- package/dist/__tests__/mesh-bin-contract.unit.test.d.ts.map +0 -1
- package/dist/__tests__/meta-schema-optional.test.d.ts +0 -2
- package/dist/__tests__/meta-schema-optional.test.d.ts.map +0 -1
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts +0 -2
- package/dist/__tests__/meta-source-overrides.unit.test.d.ts.map +0 -1
- package/dist/__tests__/native-cooker-registry.test.d.ts +0 -2
- package/dist/__tests__/native-cooker-registry.test.d.ts.map +0 -1
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts +0 -2
- package/dist/__tests__/offline-evidence-artifact-verification-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/offline-evidence.integration.test.d.ts +0 -2
- package/dist/__tests__/offline-evidence.integration.test.d.ts.map +0 -1
- package/dist/__tests__/owner-chain.integration.test.d.ts +0 -2
- package/dist/__tests__/owner-chain.integration.test.d.ts.map +0 -1
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts +0 -2
- package/dist/__tests__/pack-v2-fixture-migration.test.d.ts.map +0 -1
- package/dist/__tests__/pack-v2-schema.test.d.ts +0 -2
- package/dist/__tests__/pack-v2-schema.test.d.ts.map +0 -1
- package/dist/__tests__/pack.unit.test.d.ts +0 -2
- package/dist/__tests__/pack.unit.test.d.ts.map +0 -1
- package/dist/__tests__/package-finalizer.contract.test.d.ts +0 -2
- package/dist/__tests__/package-finalizer.contract.test.d.ts.map +0 -1
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts +0 -2
- package/dist/__tests__/parameterized-pack-authoring-gateway.unit.test.d.ts.map +0 -1
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts +0 -2
- package/dist/__tests__/parameterized-pack-scanner.unit.test.d.ts.map +0 -1
- package/dist/__tests__/parameterized-pack.unit.test.d.ts +0 -2
- package/dist/__tests__/parameterized-pack.unit.test.d.ts.map +0 -1
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts +0 -2
- package/dist/__tests__/producer-contract-schema.unit.test.d.ts.map +0 -1
- package/dist/__tests__/resolve-asset-source.test.d.ts +0 -2
- package/dist/__tests__/resolve-asset-source.test.d.ts.map +0 -1
- package/dist/__tests__/runtime-projection.unit.test.d.ts +0 -2
- package/dist/__tests__/runtime-projection.unit.test.d.ts.map +0 -1
- package/dist/__tests__/runtime.browser.test.d.ts +0 -2
- package/dist/__tests__/runtime.browser.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-blacklist.test.d.ts +0 -2
- package/dist/__tests__/scanner-blacklist.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-inventory.contract.test.d.ts +0 -2
- package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-inventory.test.d.ts +0 -2
- package/dist/__tests__/scanner-inventory.test.d.ts.map +0 -1
- package/dist/__tests__/scanner-mount-cycle.test.d.ts +0 -2
- package/dist/__tests__/scanner-mount-cycle.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-cli.integration.test.d.ts.map +0 -1
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts +0 -2
- package/dist/__tests__/scriptable-pack-ddc-key.unit.test.d.ts.map +0 -1
- package/dist/__tests__/topology-actions.unit.test.d.ts +0 -2
- package/dist/__tests__/topology-actions.unit.test.d.ts.map +0 -1
- package/dist/__tests__/topology.unit.test.d.ts +0 -2
- package/dist/__tests__/topology.unit.test.d.ts.map +0 -1
- package/dist/parameterized-pack-node.d.ts.map +0 -1
- package/dist/parameterized-pack-node.mjs.map +0 -1
- package/dist/parameterized-pack.d.ts.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../');
|
|
8
|
+
const CONFIG_PATH = resolve(REPO_ROOT, 'packages/pack/tsconfig.json');
|
|
9
|
+
|
|
10
|
+
async function declarationProjectInputs(): Promise<readonly string[]> {
|
|
11
|
+
const source = await readFile(CONFIG_PATH, 'utf8');
|
|
12
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
13
|
+
JSON.parse(source),
|
|
14
|
+
ts.sys,
|
|
15
|
+
dirname(CONFIG_PATH),
|
|
16
|
+
undefined,
|
|
17
|
+
CONFIG_PATH,
|
|
18
|
+
);
|
|
19
|
+
return parsed.fileNames.map((fileName) => relative(REPO_ROOT, fileName));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('Pack declaration project boundary', () => {
|
|
23
|
+
it('keeps tests and emitted declarations out of production inputs', async () => {
|
|
24
|
+
const inputs = await declarationProjectInputs();
|
|
25
|
+
const config = JSON.parse(await readFile(CONFIG_PATH, 'utf8')) as {
|
|
26
|
+
readonly exclude?: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(config.exclude).toEqual(expect.arrayContaining(['src/**/__tests__/**', 'dist/**']));
|
|
30
|
+
expect(inputs.some((fileName) => fileName.startsWith('packages/pack/src/__tests__/'))).toBe(
|
|
31
|
+
false,
|
|
32
|
+
);
|
|
33
|
+
expect(inputs.some((fileName) => fileName.startsWith('packages/pack/dist/'))).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const packageRoots = ['packages/pack/src', 'packages/vite-plugin-pack/src', 'packages/render/src'];
|
|
6
|
+
|
|
7
|
+
describe('Surface consumer census', () => {
|
|
8
|
+
it('keeps the runtime and Pack consumers on the existing MaterialAsset wire', async () => {
|
|
9
|
+
const sources = await Promise.all(
|
|
10
|
+
packageRoots.map(async (root) => {
|
|
11
|
+
const path = resolve(process.cwd(), root, 'index.ts');
|
|
12
|
+
try {
|
|
13
|
+
return await readFile(path, 'utf8');
|
|
14
|
+
} catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
const source = sources.join('\n');
|
|
20
|
+
expect(source).not.toContain('SurfaceAsset');
|
|
21
|
+
expect(source).not.toContain('surfaceModule');
|
|
22
|
+
expect(source).not.toContain('shadingModel');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { Materials } from '@forgeax/engine-render';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { NativeCookerRegistry } from '../native-cooker-registry.js';
|
|
8
|
+
|
|
9
|
+
const compilerModule = pathToFileURL(
|
|
10
|
+
resolve(
|
|
11
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
12
|
+
'../../../../packages/shader-compiler/dist/index.mjs',
|
|
13
|
+
),
|
|
14
|
+
).href;
|
|
15
|
+
|
|
16
|
+
const GUID = '11111111-1111-4111-8111-111111111111';
|
|
17
|
+
const ROOT_GUID = '22222222-2222-4222-8222-222222222222';
|
|
18
|
+
const CHILD_GUID = '33333333-3333-4333-8333-333333333333';
|
|
19
|
+
const SURFACE = `#define_import_path game_3d::rusted_iron_surface
|
|
20
|
+
#import forgeax_material::surface_v1::{SurfaceData, SurfaceInput}
|
|
21
|
+
fn evaluate_surface(input: SurfaceInput) -> SurfaceData {
|
|
22
|
+
return SurfaceData(vec3<f32>(0.4, 0.45, 0.47), input.geometricNormalWS, 0.0, 0.5, vec3<f32>(0.0), 1.0, 1.0, 0.0);
|
|
23
|
+
}`;
|
|
24
|
+
|
|
25
|
+
describe('Surface Pack publication', () => {
|
|
26
|
+
it('publishes one complete cooked record with source closure and artifact', async () => {
|
|
27
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-publication-'));
|
|
28
|
+
try {
|
|
29
|
+
const sourcePath = join(root, 'rusted-iron-surface.wgsl');
|
|
30
|
+
await writeFile(sourcePath, SURFACE);
|
|
31
|
+
const material = Materials.standard({
|
|
32
|
+
surfaceModule: 'game_3d::rusted_iron_surface',
|
|
33
|
+
parameters: [],
|
|
34
|
+
values: {},
|
|
35
|
+
});
|
|
36
|
+
const { createMaterialPackCooker } = await import(compilerModule);
|
|
37
|
+
const draft = await createMaterialPackCooker([root]).cook({
|
|
38
|
+
guid: GUID,
|
|
39
|
+
source: material,
|
|
40
|
+
sourcePath,
|
|
41
|
+
});
|
|
42
|
+
const cooked = draft.payload.cooked as {
|
|
43
|
+
readonly receipt: { readonly sourceClosure: readonly string[] };
|
|
44
|
+
readonly artifact: { readonly path: string; readonly bytes: Uint8Array };
|
|
45
|
+
};
|
|
46
|
+
expect(cooked.receipt.sourceClosure).toContain(sourcePath);
|
|
47
|
+
expect(cooked.artifact.path).toContain(GUID);
|
|
48
|
+
const artifact = draft.artifacts[cooked.artifact.path];
|
|
49
|
+
expect(artifact?.bytes.byteLength).toBeGreaterThan(0);
|
|
50
|
+
expect(Object.keys(draft.artifacts)).toContain(cooked.artifact.path);
|
|
51
|
+
} finally {
|
|
52
|
+
await rm(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps the accepted generation as last known good after cook failure', async () => {
|
|
57
|
+
const registry = new NativeCookerRegistry();
|
|
58
|
+
registry.register({
|
|
59
|
+
key: 'material',
|
|
60
|
+
cook: async (input: { readonly fail?: boolean }) => {
|
|
61
|
+
if (input.fail) throw new Error('surface compose failed');
|
|
62
|
+
return {
|
|
63
|
+
guid: GUID,
|
|
64
|
+
payload: { generation: 1 },
|
|
65
|
+
refs: [],
|
|
66
|
+
artifacts: {},
|
|
67
|
+
inputFingerprint: 'sha256:surface-input',
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const first = await registry.runTransaction({ key: 'material', input: {} });
|
|
72
|
+
expect(first).toMatchObject({ ok: true, value: { generation: 1, status: 'committed' } });
|
|
73
|
+
if (!first.ok) return;
|
|
74
|
+
const failed = await registry.runTransaction({
|
|
75
|
+
key: 'material',
|
|
76
|
+
input: { fail: true },
|
|
77
|
+
previous: { draft: first.value.draft, generation: first.value.generation },
|
|
78
|
+
});
|
|
79
|
+
expect(failed).toMatchObject({
|
|
80
|
+
ok: true,
|
|
81
|
+
value: {
|
|
82
|
+
status: 'recovered',
|
|
83
|
+
lastKnownGood: { payload: { generation: 1 } },
|
|
84
|
+
lastKnownGoodGeneration: 1,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('cold-cooks a child through the Pack-owned root table and publishes its parent ref', async () => {
|
|
90
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-surface-root-child-'));
|
|
91
|
+
try {
|
|
92
|
+
const rootMaterial = Materials.standard({
|
|
93
|
+
baseColor: [1, 1, 1, 1],
|
|
94
|
+
metallic: 0,
|
|
95
|
+
roughness: 0.5,
|
|
96
|
+
});
|
|
97
|
+
const child = {
|
|
98
|
+
kind: 'material' as const,
|
|
99
|
+
parent: ROOT_GUID as never,
|
|
100
|
+
values: { baseColor: [0.4, 0.45, 0.47, 1] },
|
|
101
|
+
};
|
|
102
|
+
const { createMaterialPackCooker } = await import(compilerModule);
|
|
103
|
+
const draft = await createMaterialPackCooker([root]).cook({
|
|
104
|
+
guid: CHILD_GUID,
|
|
105
|
+
source: child,
|
|
106
|
+
table: { [ROOT_GUID]: rootMaterial, [CHILD_GUID]: child },
|
|
107
|
+
});
|
|
108
|
+
const cooked = draft.payload.cooked as {
|
|
109
|
+
readonly authored: { readonly parent?: string };
|
|
110
|
+
readonly resolved: {
|
|
111
|
+
readonly parameters: readonly { readonly name: string }[];
|
|
112
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
113
|
+
};
|
|
114
|
+
readonly refs: { readonly parent: readonly string[] };
|
|
115
|
+
};
|
|
116
|
+
expect(cooked.authored.parent).toBe(ROOT_GUID);
|
|
117
|
+
expect(cooked.refs.parent).toEqual([ROOT_GUID]);
|
|
118
|
+
expect(cooked.resolved.parameters.map((parameter) => parameter.name)).toContain('baseColor');
|
|
119
|
+
expect(cooked.resolved.values.baseColor).toEqual([0.4, 0.45, 0.47, 1]);
|
|
120
|
+
expect(draft.refs).toContain(ROOT_GUID);
|
|
121
|
+
} finally {
|
|
122
|
+
await rm(root, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not publish an artifact when a physical material requests Deferred', async () => {
|
|
127
|
+
const { createMaterialPackCooker } = await import(compilerModule);
|
|
128
|
+
const material = {
|
|
129
|
+
kind: 'material' as const,
|
|
130
|
+
passes: [
|
|
131
|
+
{
|
|
132
|
+
name: 'forward',
|
|
133
|
+
program: {
|
|
134
|
+
module: 'forgeax_material::standard',
|
|
135
|
+
moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'deferred',
|
|
140
|
+
program: {
|
|
141
|
+
module: 'forgeax_material::standard',
|
|
142
|
+
moduleSlots: { surface: 'forgeax_material::default_standard_surface' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
parameters: [
|
|
147
|
+
{ name: 'clearcoat', type: 'f32' as const },
|
|
148
|
+
{ name: 'clearcoatRoughness', type: 'f32' as const },
|
|
149
|
+
],
|
|
150
|
+
values: { clearcoat: 0, clearcoatRoughness: 0.5 },
|
|
151
|
+
};
|
|
152
|
+
await expect(
|
|
153
|
+
createMaterialPackCooker().cook({
|
|
154
|
+
guid: GUID,
|
|
155
|
+
source: material,
|
|
156
|
+
}),
|
|
157
|
+
).rejects.toThrow('material-physical-contract-invalid');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Materials } from '@forgeax/engine-render';
|
|
2
|
+
import { assertMaterialAsset } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const customOptions = {
|
|
6
|
+
surfaceModule: 'game_3d::rusted_iron_surface',
|
|
7
|
+
parameters: [{ name: 'ironColor', type: 'color' as const }],
|
|
8
|
+
values: { ironColor: [0.4, 0.45, 0.47, 1] },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('custom Surface MaterialAsset wire projection', () => {
|
|
12
|
+
it('uses Standard module and the existing Surface slot wire', () => {
|
|
13
|
+
const material = Materials.standard(customOptions as never);
|
|
14
|
+
expect(material.passes?.map((pass) => pass.program.module)).toEqual([
|
|
15
|
+
'forgeax_material::standard',
|
|
16
|
+
'forgeax_material::standard',
|
|
17
|
+
'forgeax_material::standard',
|
|
18
|
+
]);
|
|
19
|
+
expect(
|
|
20
|
+
material.passes?.every(
|
|
21
|
+
(pass) => pass.program.moduleSlots?.surface === customOptions.surfaceModule,
|
|
22
|
+
),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
const wire = JSON.parse(JSON.stringify(material)) as Record<string, unknown>;
|
|
25
|
+
assertMaterialAsset(wire);
|
|
26
|
+
expect(wire).not.toHaveProperty('surfaceModule');
|
|
27
|
+
expect(wire).not.toHaveProperty('shadingModel');
|
|
28
|
+
expect(wire).not.toHaveProperty('surface');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { AssetGuid, PackageId } from '../guid.js';
|
|
6
|
-
import {
|
|
6
|
+
import { createFileSystemPackAuthoringGateway } from '../pack-authoring-node.js';
|
|
7
7
|
|
|
8
8
|
const DIRECT_PACKAGE = '01900000-0000-7000-8000-000000000070';
|
|
9
9
|
const CLONE_PACKAGE = '01900000-0000-7000-8000-000000000071';
|
|
@@ -23,11 +23,11 @@ afterEach(async () => {
|
|
|
23
23
|
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
describe('
|
|
26
|
+
describe('Pack authoring gateway', () => {
|
|
27
27
|
it('writes guid-less direct JSON, derives identity, and makes mutations idempotent', async () => {
|
|
28
28
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-direct-'));
|
|
29
29
|
roots.push(root);
|
|
30
|
-
const gateway =
|
|
30
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
31
31
|
const operation = {
|
|
32
32
|
operation: 'asset-source.create' as const,
|
|
33
33
|
requestId: 'create-direct',
|
|
@@ -72,7 +72,7 @@ describe('parameterized Pack authoring gateway', () => {
|
|
|
72
72
|
});
|
|
73
73
|
expect(unresolved).toMatchObject({ ok: false, error: { code: 'asset-not-ready' } });
|
|
74
74
|
|
|
75
|
-
const publishedGateway =
|
|
75
|
+
const publishedGateway = createFileSystemPackAuthoringGateway({
|
|
76
76
|
gameRoot: root,
|
|
77
77
|
materialized: () => [
|
|
78
78
|
{
|
|
@@ -121,10 +121,10 @@ describe('parameterized Pack authoring gateway', () => {
|
|
|
121
121
|
expect(cloneResolved).toMatchObject({ ok: true, value: { status: 'identity' } });
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('creates
|
|
124
|
+
it('creates ScriptablePack source and Pack instance documents with CAS-protected value edits', async () => {
|
|
125
125
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-instance-'));
|
|
126
126
|
roots.push(root);
|
|
127
|
-
const gateway =
|
|
127
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
128
128
|
const source = await gateway.execute({
|
|
129
129
|
operation: 'asset-source.create',
|
|
130
130
|
requestId: 'create-source',
|
|
@@ -188,7 +188,7 @@ describe('parameterized Pack authoring gateway', () => {
|
|
|
188
188
|
it('fails closed when the current materialized projection violates derived identity', async () => {
|
|
189
189
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-projection-'));
|
|
190
190
|
roots.push(root);
|
|
191
|
-
const gateway =
|
|
191
|
+
const gateway = createFileSystemPackAuthoringGateway({
|
|
192
192
|
gameRoot: root,
|
|
193
193
|
materialized: () => [
|
|
194
194
|
{
|
|
@@ -216,7 +216,7 @@ describe('parameterized Pack authoring gateway', () => {
|
|
|
216
216
|
it('does not fall back from a missing sourcePath to another package locator', async () => {
|
|
217
217
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-locator-'));
|
|
218
218
|
roots.push(root);
|
|
219
|
-
const gateway =
|
|
219
|
+
const gateway = createFileSystemPackAuthoringGateway({ gameRoot: root });
|
|
220
220
|
await gateway.execute({
|
|
221
221
|
operation: 'asset-source.create',
|
|
222
222
|
requestId: 'create-locator-source',
|
|
@@ -237,7 +237,7 @@ describe('parameterized Pack authoring gateway', () => {
|
|
|
237
237
|
it('converts a broken materialized projection callback into a structured error', async () => {
|
|
238
238
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-gateway-projection-callback-'));
|
|
239
239
|
roots.push(root);
|
|
240
|
-
const gateway =
|
|
240
|
+
const gateway = createFileSystemPackAuthoringGateway({
|
|
241
241
|
gameRoot: root,
|
|
242
242
|
materialized: () => {
|
|
243
243
|
throw new Error('projection unavailable');
|
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
definePack,
|
|
6
6
|
definePackageId,
|
|
7
7
|
type PackParameterInheritanceSubject,
|
|
8
|
-
|
|
8
|
+
parsePackSourceJson,
|
|
9
9
|
projectDirectPackJson,
|
|
10
10
|
resolvePackParameterInheritance,
|
|
11
11
|
resolvePackParameterValues,
|
|
12
12
|
validatePackDefinition,
|
|
13
|
-
} from '../
|
|
13
|
+
} from '../pack-authoring.js';
|
|
14
14
|
|
|
15
15
|
const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
|
|
16
16
|
|
|
@@ -20,7 +20,7 @@ function packageId(value: string) {
|
|
|
20
20
|
return result.value;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
describe('
|
|
23
|
+
describe('ScriptablePack and Pack authoring', () => {
|
|
24
24
|
it('derives the RFC 4122 UUIDv5 vector and keeps PackageId branded separately', () => {
|
|
25
25
|
const namespace = packageId(NAMESPACE);
|
|
26
26
|
expect(AssetGuid.format(AssetGuid.derive(namespace, 'www.widgets.com'))).toBe(
|
|
@@ -30,7 +30,7 @@ describe('parameterized Pack authoring', () => {
|
|
|
30
30
|
expect(AssetGuid.derive(namespace, 'mesh/a')).not.toBe(namespace);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('derives an explicit context shape for zero-parameter and
|
|
33
|
+
it('derives an explicit context shape for zero-parameter and parameter-bearing ScriptablePacks', async () => {
|
|
34
34
|
const zero = definePack({
|
|
35
35
|
schemaVersion: '2.0.0',
|
|
36
36
|
packageId: definePackageId('01900000-0000-7000-8000-000000000001'),
|
|
@@ -51,7 +51,7 @@ describe('parameterized Pack authoring', () => {
|
|
|
51
51
|
});
|
|
52
52
|
expect(zeroResult.ok).toBe(true);
|
|
53
53
|
|
|
54
|
-
const
|
|
54
|
+
const withParameters = definePack({
|
|
55
55
|
schemaVersion: '2.0.0',
|
|
56
56
|
packageId: definePackageId('01900000-0000-7000-8000-000000000002'),
|
|
57
57
|
parameters: [
|
|
@@ -67,9 +67,9 @@ describe('parameterized Pack authoring', () => {
|
|
|
67
67
|
},
|
|
68
68
|
}),
|
|
69
69
|
});
|
|
70
|
-
const values = resolvePackParameterValues(
|
|
70
|
+
const values = resolvePackParameterValues(withParameters, { segments: 24 });
|
|
71
71
|
expect(values).toMatchObject({ ok: true, value: { segments: 24, style: 'oak' } });
|
|
72
|
-
expect(resolvePackParameterValues(
|
|
72
|
+
expect(resolvePackParameterValues(withParameters, { unknown: true })).toMatchObject({
|
|
73
73
|
ok: false,
|
|
74
74
|
error: { code: 'pack-parameter-invalid' },
|
|
75
75
|
});
|
|
@@ -148,7 +148,7 @@ describe('parameterized Pack authoring', () => {
|
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it('parses direct and instance JSON without accepting GUID fields', () => {
|
|
151
|
-
const direct =
|
|
151
|
+
const direct = parsePackSourceJson({
|
|
152
152
|
schemaVersion: '3.0.0',
|
|
153
153
|
packageId: '01900000-0000-7000-8000-000000000010',
|
|
154
154
|
assets: {
|
|
@@ -167,14 +167,14 @@ describe('parameterized Pack authoring', () => {
|
|
|
167
167
|
});
|
|
168
168
|
}
|
|
169
169
|
expect(
|
|
170
|
-
|
|
170
|
+
parsePackSourceJson({
|
|
171
171
|
schemaVersion: '3.0.0',
|
|
172
172
|
packageId: '01900000-0000-7000-8000-000000000010',
|
|
173
173
|
assets: { 'scene/main': { guid: NAMESPACE, kind: 'scene', payload: {}, refs: [] } },
|
|
174
174
|
}),
|
|
175
175
|
).toMatchObject({ ok: false, error: { code: 'pack-parameter-invalid' } });
|
|
176
176
|
expect(
|
|
177
|
-
|
|
177
|
+
parsePackSourceJson({
|
|
178
178
|
schemaVersion: '3.0.0',
|
|
179
179
|
packageId: '01900000-0000-7000-8000-000000000020',
|
|
180
180
|
parent: '01900000-0000-7000-8000-000000000002',
|
|
@@ -16,9 +16,9 @@ afterEach(async () => {
|
|
|
16
16
|
if (root !== undefined) await rm(root, { recursive: true, force: true });
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
describe('
|
|
19
|
+
describe('ScriptablePack and Pack source scanner', () => {
|
|
20
20
|
it('indexes direct v3 output keys as derived GUIDs without requiring entry GUIDs', async () => {
|
|
21
|
-
const root = await mkdtemp(join(tmpdir(), 'forgeax-
|
|
21
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-scan-'));
|
|
22
22
|
roots.push(root);
|
|
23
23
|
const path = join(root, 'direct.pack.json');
|
|
24
24
|
await writeFile(
|
|
@@ -65,8 +65,8 @@ describe('parameterized Pack scanner', () => {
|
|
|
65
65
|
expect(stderr).toEqual([]);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('captures a
|
|
69
|
-
const root = await mkdtemp(join(tmpdir(), 'forgeax-
|
|
68
|
+
it('captures a ScriptablePack source as metadata and resolves an instance parent', async () => {
|
|
69
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-source-scan-'));
|
|
70
70
|
roots.push(root);
|
|
71
71
|
await writeFile(
|
|
72
72
|
join(root, 'source.pack.ts'),
|
|
@@ -96,12 +96,12 @@ describe('parameterized Pack scanner', () => {
|
|
|
96
96
|
expect(result.ok).toBe(true);
|
|
97
97
|
if (!result.ok) return;
|
|
98
98
|
const declaration = result.value.declarations.get(join(root, 'source.pack.ts'));
|
|
99
|
-
expect(declaration?.format).toBe('pack.ts
|
|
99
|
+
expect(declaration?.format).toBe('pack.ts');
|
|
100
100
|
expect(result.value.inventory).toEqual([]);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
it('fails closed when an instance parent is missing', async () => {
|
|
104
|
-
const root = await mkdtemp(join(tmpdir(), 'forgeax-
|
|
104
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-parent-scan-'));
|
|
105
105
|
roots.push(root);
|
|
106
106
|
await writeFile(
|
|
107
107
|
join(root, 'missing.pack.json'),
|
|
@@ -117,16 +117,14 @@ describe('engine-pack scanner inventory contract', () => {
|
|
|
117
117
|
});
|
|
118
118
|
const scriptableDeclaration = result.value.declarations.get(scriptablePath);
|
|
119
119
|
expect(scriptableDeclaration).toMatchObject({
|
|
120
|
-
format: 'pack.ts
|
|
120
|
+
format: 'pack.ts',
|
|
121
121
|
sourcePath: scriptablePath,
|
|
122
122
|
value: {
|
|
123
123
|
packageId: '019e3969-1d48-7c3b-ac24-6d68f4570662',
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
expect(
|
|
127
|
-
scriptableDeclaration?.format === 'pack.ts
|
|
128
|
-
? scriptableDeclaration.value
|
|
129
|
-
: undefined,
|
|
127
|
+
scriptableDeclaration?.format === 'pack.ts' ? scriptableDeclaration.value : undefined,
|
|
130
128
|
).not.toHaveProperty('subAssets');
|
|
131
129
|
});
|
|
132
130
|
|
|
@@ -44,11 +44,11 @@ describe('AssetInventory', () => {
|
|
|
44
44
|
const { inventory, sourcePath } = getFixture();
|
|
45
45
|
const declaration = inventory.declarations.get(sourcePath);
|
|
46
46
|
expect(declaration).toMatchObject({
|
|
47
|
-
format: 'pack.ts
|
|
47
|
+
format: 'pack.ts',
|
|
48
48
|
sourcePath,
|
|
49
49
|
value: {
|
|
50
50
|
schemaVersion: '2.0.0',
|
|
51
|
-
kind: '
|
|
51
|
+
kind: 'scriptable-pack-source',
|
|
52
52
|
packageId: '00000000-0000-0000-0000-000000000001',
|
|
53
53
|
source: sourcePath,
|
|
54
54
|
},
|
|
@@ -59,7 +59,7 @@ describe('AssetInventory', () => {
|
|
|
59
59
|
const { inventory, sourcePath } = getFixture();
|
|
60
60
|
const declaration = inventory.declarations.get(sourcePath);
|
|
61
61
|
expect(
|
|
62
|
-
declaration?.format === 'pack.ts
|
|
62
|
+
declaration?.format === 'pack.ts'
|
|
63
63
|
? declaration.sourceClosure.map((entry) => entry.path)
|
|
64
64
|
: undefined,
|
|
65
65
|
).toEqual([await realpath(sourcePath)]);
|
|
@@ -69,9 +69,10 @@ describe('AssetInventory', () => {
|
|
|
69
69
|
const { inventory, sourcePath } = getFixture();
|
|
70
70
|
const declaration = inventory.declarations.get(sourcePath);
|
|
71
71
|
expect(inventory.inventory).toEqual([]);
|
|
72
|
-
expect(
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
expect(declaration?.format === 'pack.ts' ? declaration.definition : undefined).toMatchObject({
|
|
73
|
+
packageId: expect.any(Uint8Array),
|
|
74
|
+
schemaVersion: '2.0.0',
|
|
75
|
+
});
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
it('keeps semantic identity stable when the source path is renamed', () => {
|
|
@@ -31,7 +31,7 @@ describe('ScriptablePack CLI Meta inspection', () => {
|
|
|
31
31
|
expect(code).toBe(0);
|
|
32
32
|
expect(stderr).toEqual([]);
|
|
33
33
|
expect(JSON.parse(stdout[0] ?? '{}')).toMatchObject({
|
|
34
|
-
kind: '
|
|
34
|
+
kind: 'scriptable-pack-source',
|
|
35
35
|
source,
|
|
36
36
|
packageId: '019ffa97-8b39-7ad2-84cc-273268591ab4',
|
|
37
37
|
});
|
package/src/build.ts
CHANGED
|
@@ -60,6 +60,8 @@ export {
|
|
|
60
60
|
type MaterialCookReceipt,
|
|
61
61
|
type MaterialCookRecordError,
|
|
62
62
|
type MaterialCookRefs,
|
|
63
|
+
type MaterialCookVariant,
|
|
64
|
+
type MaterialCookVariantContext,
|
|
63
65
|
projectCookedMaterialRecord,
|
|
64
66
|
serializeCookedMaterialRecord,
|
|
65
67
|
serializeMaterialCookReceipt,
|
|
@@ -94,6 +96,13 @@ export {
|
|
|
94
96
|
type MeshBinHeaderV4,
|
|
95
97
|
writeMeshBinHeader,
|
|
96
98
|
} from './mesh-bin-contract.js';
|
|
99
|
+
export * from './pack-authoring.js';
|
|
100
|
+
export {
|
|
101
|
+
createFileSystemPackAuthoringGateway,
|
|
102
|
+
createFileSystemPackAuthoringPort,
|
|
103
|
+
type FileSystemPackAuthoringOptions,
|
|
104
|
+
type PackAuthoringMaterializedAsset,
|
|
105
|
+
} from './pack-authoring-node.js';
|
|
97
106
|
export {
|
|
98
107
|
type FinalizedPackageProduct,
|
|
99
108
|
finalizePackageProduct,
|
|
@@ -106,13 +115,6 @@ export {
|
|
|
106
115
|
type PackageProductAsset,
|
|
107
116
|
packageTransportRevision,
|
|
108
117
|
} from './package-finalizer.js';
|
|
109
|
-
export * from './parameterized-pack.js';
|
|
110
|
-
export {
|
|
111
|
-
createFileSystemParameterizedPackAuthoringGateway,
|
|
112
|
-
createFileSystemParameterizedPackAuthoringPort,
|
|
113
|
-
type FileSystemParameterizedPackAuthoringOptions,
|
|
114
|
-
type ParameterizedPackMaterializedAsset,
|
|
115
|
-
} from './parameterized-pack-node.js';
|
|
116
118
|
export { validateProducerContract, validateProducerOutputs } from './producer-contract.js';
|
|
117
119
|
export { resolveAssetSource } from './resolve-asset-source.js';
|
|
118
120
|
export { parsePackV2, validateMeta, validatePack, validatePackV2 } from './runtime.js';
|
|
@@ -129,7 +131,7 @@ export {
|
|
|
129
131
|
type LegacyPackInventoryDocument,
|
|
130
132
|
type PackInventoryAsset,
|
|
131
133
|
type PackInventoryDocument,
|
|
132
|
-
type
|
|
134
|
+
type PackSourceInventoryDocument,
|
|
133
135
|
type ScanInventory,
|
|
134
136
|
type ScanOptions,
|
|
135
137
|
type ScanSourceDeclaration,
|
package/src/catalog-builder.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
projectPackageCatalog,
|
|
7
7
|
} from './catalog-projection.js';
|
|
8
8
|
import { deriveAssetName } from './deriveAssetName.js';
|
|
9
|
-
import {
|
|
9
|
+
import { parsePackSourceJson, projectDirectPackJson } from './pack-authoring.js';
|
|
10
10
|
import { resolveAssetSource } from './resolve-asset-source.js';
|
|
11
11
|
import {
|
|
12
12
|
type MetaInventoryDocument,
|
|
@@ -201,7 +201,7 @@ function projectSource(
|
|
|
201
201
|
declarations: Map<string, CatalogImportMeta>,
|
|
202
202
|
sourceIdentityFor: CatalogBuildProjectionOptions['sourceIdentityFor'],
|
|
203
203
|
): { readonly entries: PackIndexEntry[]; readonly error?: CatalogBuildError } {
|
|
204
|
-
if (sourceDeclaration.format === 'pack.ts
|
|
204
|
+
if (sourceDeclaration.format === 'pack.ts') {
|
|
205
205
|
// A source declaration is an authoring identity and parameter contract,
|
|
206
206
|
// not a materialized output set. Dynamic build owns publication after its
|
|
207
207
|
// fixed-point worklist converges; Catalog scanning must not invent rows.
|
|
@@ -233,7 +233,7 @@ function projectSource(
|
|
|
233
233
|
const parsed = sourceDeclaration.value;
|
|
234
234
|
const sourcePath = catalogSourcePathFor(cwd, path, sourceIdentityFor);
|
|
235
235
|
if (parsed.schemaVersion === '3.0.0') {
|
|
236
|
-
const authoring =
|
|
236
|
+
const authoring = parsePackSourceJson(parsed);
|
|
237
237
|
if (!authoring.ok) {
|
|
238
238
|
return {
|
|
239
239
|
entries: [],
|
package/src/cli-asset.ts
CHANGED
|
@@ -26,9 +26,9 @@ import {
|
|
|
26
26
|
import { readSourceInventory } from './evidence/source-inventory.js';
|
|
27
27
|
import { isValidAssetGuidString } from './guid.js';
|
|
28
28
|
import { parsePackV2 } from './index.js';
|
|
29
|
-
import {
|
|
29
|
+
import { projectScriptablePackMeta } from './pack-authoring.js';
|
|
30
30
|
import { type ScanSourceDeclaration, scanInventory } from './scanner.js';
|
|
31
|
-
import {
|
|
31
|
+
import { loadScriptablePack } from './scriptable-pack-node.js';
|
|
32
32
|
|
|
33
33
|
export interface PackEntry {
|
|
34
34
|
readonly guid: string;
|
|
@@ -81,7 +81,7 @@ export async function scanEntries(
|
|
|
81
81
|
return { ok: false };
|
|
82
82
|
}
|
|
83
83
|
const entries: PackEntry[] = [];
|
|
84
|
-
// The scanner is the single identity projection for Pack JSON.
|
|
84
|
+
// The scanner is the single identity projection for Pack JSON. ScriptablePack
|
|
85
85
|
// source modules deliberately contribute no rows until a build generation.
|
|
86
86
|
for (const declaration of result.value.declarations.values()) {
|
|
87
87
|
if (declaration.format !== 'meta.json') continue;
|
|
@@ -233,11 +233,11 @@ async function runMeta(rest: string[], ctx: AssetCtx): Promise<number> {
|
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
const cwd = ctx.cwd ?? process.cwd();
|
|
236
|
-
const loaded = await
|
|
236
|
+
const loaded = await loadScriptablePack(absolutePath(source, cwd), {
|
|
237
237
|
metadataOnly: true,
|
|
238
238
|
});
|
|
239
239
|
if (!loaded.ok) return emitError(ctx, loaded.error);
|
|
240
|
-
ctx.stdoutWrite(JSON.stringify(
|
|
240
|
+
ctx.stdoutWrite(JSON.stringify(projectScriptablePackMeta(loaded.value, source)));
|
|
241
241
|
return 0;
|
|
242
242
|
}
|
|
243
243
|
|