@forgeax/engine-pack 0.1.29 → 0.1.31
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 +16 -0
- package/dist/build.mjs +65 -9
- package/dist/build.mjs.map +1 -1
- package/dist/catalog-projection.d.ts +3 -3
- package/dist/cli-asset.mjs +1 -1
- package/dist/cli-asset.mjs.map +1 -1
- package/dist/evidence/material-cook.d.ts +7 -1
- package/dist/evidence/material-cook.d.ts.map +1 -1
- package/dist/index.mjs +61 -5
- package/dist/index.mjs.map +1 -1
- package/dist/material-cook.mjs +61 -5
- package/dist/material-cook.mjs.map +1 -1
- package/dist/pack-authoring-node.mjs +1 -1
- package/dist/pack-authoring-node.mjs.map +1 -1
- package/dist/pack-authoring.d.ts +8 -2
- package/dist/pack-authoring.d.ts.map +1 -1
- package/dist/pack-authoring.mjs.map +1 -1
- package/dist/runtime-publication.d.ts +2 -0
- package/dist/runtime-publication.d.ts.map +1 -1
- package/dist/scanner.mjs +1 -1
- package/dist/scanner.mjs.map +1 -1
- package/dist/scriptable-pack-node.d.ts.map +1 -1
- package/dist/scriptable-pack-node.mjs +1 -1
- package/dist/scriptable-pack-node.mjs.map +1 -1
- package/dist/scriptable-pack-worker.mjs +7 -4
- package/dist/scriptable-pack-worker.mjs.map +1 -1
- package/dist/scriptable-pack.d.ts +2 -1
- package/dist/scriptable-pack.d.ts.map +1 -1
- package/dist/scriptable-pack.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/material-cook-schema.unit.test.ts +92 -1
- package/src/__tests__/runtime-publication.unit.test.ts +36 -0
- package/src/__tests__/scriptable-pack-cli.integration.test.ts +35 -1
- package/src/evidence/material-cook.ts +79 -5
- package/src/pack-authoring.ts +11 -2
- package/src/runtime-publication.ts +7 -3
- package/src/schema/material-cook.schema.json +107 -0
- package/src/scriptable-pack-node.ts +5 -1
- package/src/scriptable-pack-worker.ts +15 -4
- package/src/scriptable-pack.ts +2 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, it } from 'vitest';
|
|
2
|
+
import { createRuntimePackPublication } from '../runtime-publication.js';
|
|
3
|
+
|
|
4
|
+
it('preserves declared source keys in derived publication rows and their digest', () => {
|
|
5
|
+
const input = {
|
|
6
|
+
pack: {
|
|
7
|
+
assets: [
|
|
8
|
+
{ guid: 'WALL-GUID', kind: 'volume', payload: {} },
|
|
9
|
+
{ guid: 'CONTROL-GUID', kind: 'mesh', payload: {} },
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
scopeId: 'lab',
|
|
13
|
+
sourcePath: 'wall.pack.json',
|
|
14
|
+
sourceRevision: 'source-1',
|
|
15
|
+
packageUrl: '/wall.pack.json',
|
|
16
|
+
};
|
|
17
|
+
const before = createRuntimePackPublication(input);
|
|
18
|
+
const after = createRuntimePackPublication({
|
|
19
|
+
...input,
|
|
20
|
+
sourceKeys: new Map([['wall-guid', 'wall/main']]),
|
|
21
|
+
});
|
|
22
|
+
expect(after.publication.outputs.map(({ guid, sourceKey }) => [guid, sourceKey])).toEqual([
|
|
23
|
+
['wall-guid', 'wall/main'],
|
|
24
|
+
['control-guid', 'control-guid'],
|
|
25
|
+
]);
|
|
26
|
+
expect(after.pack.digest).toBe(before.pack.digest);
|
|
27
|
+
expect(after.pack.outputSetDigest).not.toBe(before.pack.outputSetDigest);
|
|
28
|
+
expect(after.publication.receipt.outputSetDigest).toBe(after.pack.outputSetDigest);
|
|
29
|
+
// Explicit producer output rows remain authoritative when provided.
|
|
30
|
+
const explicit = createRuntimePackPublication({
|
|
31
|
+
...input,
|
|
32
|
+
sourceKeys: new Map([['wall-guid', 'ignored']]),
|
|
33
|
+
outputs: after.publication.outputs,
|
|
34
|
+
});
|
|
35
|
+
expect(explicit.publication.outputs).toEqual(after.publication.outputs);
|
|
36
|
+
});
|
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
// @perf-budget-skip: intentional ScriptablePack CLI integration gate.
|
|
2
2
|
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { err } from '@forgeax/engine-types';
|
|
6
7
|
import { describe, expect, it } from 'vitest';
|
|
7
8
|
import { runCliAsset } from '../cli-asset.js';
|
|
9
|
+
import { loadScriptablePack } from '../scriptable-pack-node.js';
|
|
8
10
|
|
|
9
11
|
describe('ScriptablePack CLI Meta inspection', () => {
|
|
12
|
+
it('executes TypeScript re-export closures using JavaScript specifiers and directory indexes', async () => {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), 'forgeax-pack-ts-closure-'));
|
|
14
|
+
try {
|
|
15
|
+
await mkdir(join(root, 'nested'));
|
|
16
|
+
await writeFile(join(root, 'nested/index.ts'), 'export const count: number = 7;');
|
|
17
|
+
await writeFile(join(root, 'barrel.ts'), "export * from './nested';");
|
|
18
|
+
const source = join(root, 'shape.pack.ts');
|
|
19
|
+
await writeFile(
|
|
20
|
+
source,
|
|
21
|
+
`import { count } from './barrel.js';
|
|
22
|
+
export default { schemaVersion: '2.0.0',
|
|
23
|
+
packageId: new Uint8Array([1,159,250,151,139,57,122,210,132,204,39,50,104,89,26,180]),
|
|
24
|
+
build() { return { ok: true, value: { 'scene/main': { kind: 'scene', entities: [], mounts: [], count } } }; }
|
|
25
|
+
};`,
|
|
26
|
+
);
|
|
27
|
+
const loaded = await loadScriptablePack(source);
|
|
28
|
+
expect(loaded.ok).toBe(true);
|
|
29
|
+
if (!loaded.ok) throw loaded.error;
|
|
30
|
+
const built = await loaded.value.build({
|
|
31
|
+
packageId: loaded.value.packageId,
|
|
32
|
+
values: {},
|
|
33
|
+
readByGuid: async () => err('unused'),
|
|
34
|
+
});
|
|
35
|
+
expect(built).toMatchObject({
|
|
36
|
+
ok: true,
|
|
37
|
+
value: { 'scene/main': { kind: 'scene', count: 7 } },
|
|
38
|
+
});
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(root, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
10
44
|
it('loads the definition without invoking build', async () => {
|
|
11
45
|
const root = await mkdtemp(join(tmpdir(), 'forgeax-scriptable-pack-'));
|
|
12
46
|
try {
|
|
@@ -2,12 +2,20 @@ import type {
|
|
|
2
2
|
MaterialAsset,
|
|
3
3
|
MaterialParameter,
|
|
4
4
|
MaterialPass,
|
|
5
|
+
MaterialProgramAbi,
|
|
6
|
+
MaterialProgramAddress,
|
|
5
7
|
MaterialTextureReference,
|
|
6
8
|
MaterialTextureValue,
|
|
7
9
|
MaterialValue,
|
|
8
10
|
Result,
|
|
9
11
|
} from '@forgeax/engine-types';
|
|
10
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
deriveStandardLayerPlan,
|
|
14
|
+
err,
|
|
15
|
+
isMaterialProgramAbi,
|
|
16
|
+
MATERIAL_TEXTURE_SLOTS,
|
|
17
|
+
ok,
|
|
18
|
+
} from '@forgeax/engine-types';
|
|
11
19
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
12
20
|
import { bytesToHex } from '@noble/hashes/utils.js';
|
|
13
21
|
|
|
@@ -44,6 +52,12 @@ export interface MaterialCookProgram {
|
|
|
44
52
|
readonly selections: readonly {
|
|
45
53
|
readonly pass: string;
|
|
46
54
|
readonly context: MaterialCookProgramContext;
|
|
55
|
+
/** Renderer submission address selected by this publication. */
|
|
56
|
+
readonly address?: MaterialProgramAddress;
|
|
57
|
+
/** Actual entry point paired with the selected address. */
|
|
58
|
+
readonly entry?: string;
|
|
59
|
+
/** Producer-reflected ABI facts for the selected artifact/entry. */
|
|
60
|
+
readonly abi?: MaterialProgramAbi;
|
|
47
61
|
}[];
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -483,6 +497,9 @@ export function validateCookedMaterialRecord(
|
|
|
483
497
|
const programs: MaterialCookProgram[] = [];
|
|
484
498
|
const programKeys = new Set<string>();
|
|
485
499
|
const selections = new Set<string>();
|
|
500
|
+
const submissionSelections = new Map<string, Set<MaterialProgramAddress>>();
|
|
501
|
+
let modernPublication = false;
|
|
502
|
+
let legacySelectionField: string | undefined;
|
|
486
503
|
const selectedPasses = new Set<string>();
|
|
487
504
|
for (const [index, entry] of candidate.programs.entries()) {
|
|
488
505
|
const field = `programs[${index}]`;
|
|
@@ -511,9 +528,11 @@ export function validateCookedMaterialRecord(
|
|
|
511
528
|
if (!Array.isArray(program.selections) || program.selections.length === 0)
|
|
512
529
|
return invalid(`${field}.selections`);
|
|
513
530
|
const programSelections: MaterialCookProgram['selections'][number][] = [];
|
|
514
|
-
for (const [selectionIndex,
|
|
531
|
+
for (const [selectionIndex, rawSelection] of program.selections.entries()) {
|
|
515
532
|
const selectionField = `${field}.selections[${selectionIndex}]`;
|
|
516
|
-
if (
|
|
533
|
+
if (rawSelection === null || typeof rawSelection !== 'object') return invalid(selectionField);
|
|
534
|
+
const selection = rawSelection as Record<string, unknown>;
|
|
535
|
+
if (typeof selection.pass !== 'string' || !passNames.has(selection.pass))
|
|
517
536
|
return invalid(selectionField);
|
|
518
537
|
const context = validateMaterialCookProgramContext(selection.context);
|
|
519
538
|
if (!context.ok)
|
|
@@ -521,11 +540,55 @@ export function validateCookedMaterialRecord(
|
|
|
521
540
|
`${selectionField}.${context.error.detail.field}`,
|
|
522
541
|
context.error.detail.actual,
|
|
523
542
|
);
|
|
524
|
-
const
|
|
543
|
+
const address = selection.address === undefined ? 'direct' : selection.address;
|
|
544
|
+
if (address !== 'direct' && address !== 'scene-index')
|
|
545
|
+
return invalid(`${selectionField}.address`, selection.address);
|
|
546
|
+
const hasAddressFacts =
|
|
547
|
+
selection.address !== undefined ||
|
|
548
|
+
selection.entry !== undefined ||
|
|
549
|
+
selection.abi !== undefined;
|
|
550
|
+
modernPublication ||= hasAddressFacts;
|
|
551
|
+
if (!hasAddressFacts) legacySelectionField ??= selectionField;
|
|
552
|
+
const rawEntry = selection.entry;
|
|
553
|
+
const entry = typeof rawEntry === 'string' ? rawEntry : undefined;
|
|
554
|
+
if (hasAddressFacts && selection.address === undefined)
|
|
555
|
+
return invalid(
|
|
556
|
+
`${selectionField}.address`,
|
|
557
|
+
'modern ABI selections require an explicit address',
|
|
558
|
+
);
|
|
559
|
+
if (hasAddressFacts && (entry === undefined || entry.length === 0))
|
|
560
|
+
return invalid(`${selectionField}.entry`, rawEntry);
|
|
561
|
+
if (hasAddressFacts && !isMaterialProgramAbi(selection.abi))
|
|
562
|
+
return invalid(`${selectionField}.abi`, selection.abi);
|
|
563
|
+
if (hasAddressFacts) {
|
|
564
|
+
const abi = selection.abi as MaterialProgramAbi;
|
|
565
|
+
const expectedEntry = address === 'direct' ? abi.directEntry : abi.sceneIndexEntry;
|
|
566
|
+
if (entry !== expectedEntry)
|
|
567
|
+
return invalid(`${selectionField}.entry`, 'entry does not match published ABI');
|
|
568
|
+
const submissionKey = JSON.stringify([
|
|
569
|
+
selection.pass,
|
|
570
|
+
materialProgramContextKey(context.value),
|
|
571
|
+
]);
|
|
572
|
+
const addresses =
|
|
573
|
+
submissionSelections.get(submissionKey) ?? new Set<MaterialProgramAddress>();
|
|
574
|
+
addresses.add(address);
|
|
575
|
+
submissionSelections.set(submissionKey, addresses);
|
|
576
|
+
}
|
|
577
|
+
const key = JSON.stringify([
|
|
578
|
+
selection.pass,
|
|
579
|
+
materialProgramContextKey(context.value),
|
|
580
|
+
address,
|
|
581
|
+
]);
|
|
525
582
|
if (selections.has(key)) return invalid(selectionField, 'ambiguous Pass/context selection');
|
|
526
583
|
selections.add(key);
|
|
527
584
|
selectedPasses.add(selection.pass);
|
|
528
|
-
programSelections.push({
|
|
585
|
+
programSelections.push({
|
|
586
|
+
pass: selection.pass,
|
|
587
|
+
context: context.value,
|
|
588
|
+
...(selection.address === undefined ? {} : { address }),
|
|
589
|
+
...(entry === undefined ? {} : { entry }),
|
|
590
|
+
...(selection.abi === undefined ? {} : { abi: selection.abi as MaterialProgramAbi }),
|
|
591
|
+
});
|
|
529
592
|
}
|
|
530
593
|
programs.push({
|
|
531
594
|
specializationKey: program.specializationKey,
|
|
@@ -533,6 +596,17 @@ export function validateCookedMaterialRecord(
|
|
|
533
596
|
selections: programSelections,
|
|
534
597
|
});
|
|
535
598
|
}
|
|
599
|
+
if (modernPublication && legacySelectionField !== undefined) {
|
|
600
|
+
return invalid(
|
|
601
|
+
legacySelectionField,
|
|
602
|
+
'modern material publications require address, entry, and ABI facts on every selection',
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
for (const [selectionKey, addresses] of submissionSelections) {
|
|
606
|
+
if (addresses.size !== 2) {
|
|
607
|
+
return invalid('programs.selections', `incomplete submission address pair: ${selectionKey}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
536
610
|
if ([...passNames].some((pass) => !selectedPasses.has(pass)))
|
|
537
611
|
return invalid('programs.selections', 'unpublished Pass');
|
|
538
612
|
const manifestDigest = createMaterialProgramSetDigest(programs, passes);
|
package/src/pack-authoring.ts
CHANGED
|
@@ -569,7 +569,9 @@ function validateParameterDefinitions(
|
|
|
569
569
|
}
|
|
570
570
|
|
|
571
571
|
export interface PackBuildReadContext {
|
|
572
|
-
readByGuid<TAsset extends Asset = Asset>(
|
|
572
|
+
readByGuid<TAsset extends Asset | PackCookSource = Asset>(
|
|
573
|
+
guid: AssetGuid,
|
|
574
|
+
): Promise<Result<TAsset, unknown>>;
|
|
573
575
|
}
|
|
574
576
|
|
|
575
577
|
export interface PackBuildContextWithoutParameters extends PackBuildReadContext {
|
|
@@ -588,7 +590,14 @@ export type PackBuildContext<TParameters extends readonly PackParameterDefinitio
|
|
|
588
590
|
? PackBuildContextWithParameters<TParameters>
|
|
589
591
|
: PackBuildContextWithoutParameters;
|
|
590
592
|
|
|
591
|
-
|
|
593
|
+
/** Build-only custom source. The host derives identity and invokes the kind's NativeCooker. */
|
|
594
|
+
export interface PackCookSource {
|
|
595
|
+
readonly kind: string;
|
|
596
|
+
readonly execution: 'cooked';
|
|
597
|
+
readonly source: unknown;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export type PackOutputMap = Readonly<Record<string, Asset | PackCookSource>>;
|
|
592
601
|
export type PackBuildResult<TError = unknown> =
|
|
593
602
|
| Result<PackOutputMap, TError>
|
|
594
603
|
| Promise<Result<PackOutputMap, TError>>;
|
|
@@ -48,6 +48,8 @@ export interface RuntimePackPublicationInput {
|
|
|
48
48
|
readonly digest?: string;
|
|
49
49
|
readonly generation?: number;
|
|
50
50
|
readonly outputs?: readonly AssetPublicationOutput[];
|
|
51
|
+
/** Author/producer keys by normalized GUID when deriving output rows. */
|
|
52
|
+
readonly sourceKeys?: ReadonlyMap<string, string>;
|
|
51
53
|
readonly externalEvidence?: readonly AssetPublicationExternalEvidence[];
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -88,10 +90,10 @@ function normalizedAssets(pack: RuntimePackInput): readonly RuntimePackAsset[] {
|
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
function outputFor(asset: RuntimePackAsset): AssetPublicationOutput {
|
|
93
|
+
function outputFor(asset: RuntimePackAsset, sourceKey?: string): AssetPublicationOutput {
|
|
92
94
|
return {
|
|
93
95
|
guid: asset.guid.toLowerCase(),
|
|
94
|
-
sourceKey: asset.guid.toLowerCase(),
|
|
96
|
+
sourceKey: sourceKey ?? asset.guid.toLowerCase(),
|
|
95
97
|
kind: asset.kind,
|
|
96
98
|
digest: digest({
|
|
97
99
|
guid: asset.guid.toLowerCase(),
|
|
@@ -145,7 +147,9 @@ export function createRuntimePackPublication(
|
|
|
145
147
|
),
|
|
146
148
|
};
|
|
147
149
|
const valueDigest = input.digest ?? digest(semantic);
|
|
148
|
-
const outputs =
|
|
150
|
+
const outputs =
|
|
151
|
+
input.outputs ??
|
|
152
|
+
assets.map((asset) => outputFor(asset, input.sourceKeys?.get(asset.guid.toLowerCase())));
|
|
149
153
|
const outputDigest = outputSetDigest(outputs);
|
|
150
154
|
const generation =
|
|
151
155
|
input.generation ?? publicationGeneration(input.sourceRevision, valueDigest, outputDigest);
|
|
@@ -273,6 +273,113 @@
|
|
|
273
273
|
"type": "string",
|
|
274
274
|
"minLength": 1
|
|
275
275
|
},
|
|
276
|
+
"address": {
|
|
277
|
+
"enum": ["direct", "scene-index"]
|
|
278
|
+
},
|
|
279
|
+
"entry": {
|
|
280
|
+
"type": "string",
|
|
281
|
+
"minLength": 1
|
|
282
|
+
},
|
|
283
|
+
"abi": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"required": [
|
|
286
|
+
"directEntry",
|
|
287
|
+
"sceneIndexEntry",
|
|
288
|
+
"materialRow",
|
|
289
|
+
"resourceSlots",
|
|
290
|
+
"uvSets",
|
|
291
|
+
"vertexInputs",
|
|
292
|
+
"alphaMask",
|
|
293
|
+
"reflection",
|
|
294
|
+
"receiptIdentity",
|
|
295
|
+
"generation"
|
|
296
|
+
],
|
|
297
|
+
"properties": {
|
|
298
|
+
"directEntry": { "type": "string", "minLength": 1 },
|
|
299
|
+
"sceneIndexEntry": { "type": "string", "minLength": 1 },
|
|
300
|
+
"materialRow": {
|
|
301
|
+
"type": "object",
|
|
302
|
+
"required": ["byteLength", "fields"],
|
|
303
|
+
"properties": {
|
|
304
|
+
"byteLength": { "type": "integer", "minimum": 16, "multipleOf": 16 },
|
|
305
|
+
"fields": { "type": "array", "items": { "type": "string" } }
|
|
306
|
+
},
|
|
307
|
+
"additionalProperties": false
|
|
308
|
+
},
|
|
309
|
+
"resourceSlots": {
|
|
310
|
+
"type": "array",
|
|
311
|
+
"items": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"required": ["name", "parameter", "kind", "group", "binding"],
|
|
314
|
+
"properties": {
|
|
315
|
+
"name": { "type": "string", "minLength": 1 },
|
|
316
|
+
"parameter": { "type": "string", "minLength": 1 },
|
|
317
|
+
"kind": { "enum": ["sampler", "texture", "storage-buffer"] },
|
|
318
|
+
"group": { "type": "integer", "minimum": 0 },
|
|
319
|
+
"binding": { "type": "integer", "minimum": 0 }
|
|
320
|
+
},
|
|
321
|
+
"additionalProperties": false
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
"uvSets": {
|
|
325
|
+
"type": "array",
|
|
326
|
+
"items": {
|
|
327
|
+
"type": "object",
|
|
328
|
+
"required": ["parameter", "set"],
|
|
329
|
+
"properties": {
|
|
330
|
+
"parameter": { "type": "string", "minLength": 1 },
|
|
331
|
+
"set": { "type": "integer", "minimum": 0 }
|
|
332
|
+
},
|
|
333
|
+
"additionalProperties": false
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
"vertexInputs": {
|
|
337
|
+
"type": "array",
|
|
338
|
+
"items": {
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": ["semantic", "location", "format"],
|
|
341
|
+
"properties": {
|
|
342
|
+
"semantic": { "type": "string", "minLength": 1 },
|
|
343
|
+
"location": { "type": "integer", "minimum": 0 },
|
|
344
|
+
"format": { "type": "string", "minLength": 1 }
|
|
345
|
+
},
|
|
346
|
+
"additionalProperties": false
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"alphaMask": {
|
|
350
|
+
"type": "object",
|
|
351
|
+
"required": ["cutoff", "source"],
|
|
352
|
+
"properties": {
|
|
353
|
+
"cutoff": { "type": "string" },
|
|
354
|
+
"source": { "type": "string" }
|
|
355
|
+
},
|
|
356
|
+
"additionalProperties": false
|
|
357
|
+
},
|
|
358
|
+
"skinPaletteAddress": {
|
|
359
|
+
"type": "object",
|
|
360
|
+
"required": ["group", "binding", "stride"],
|
|
361
|
+
"properties": {
|
|
362
|
+
"group": { "type": "integer", "minimum": 0 },
|
|
363
|
+
"binding": { "type": "integer", "minimum": 0 },
|
|
364
|
+
"stride": { "type": "integer", "minimum": 1 }
|
|
365
|
+
},
|
|
366
|
+
"additionalProperties": false
|
|
367
|
+
},
|
|
368
|
+
"reflection": {
|
|
369
|
+
"type": "object",
|
|
370
|
+
"required": ["layoutIdentity", "resourceSlots", "vertexInputs"],
|
|
371
|
+
"properties": {
|
|
372
|
+
"layoutIdentity": { "type": "string", "minLength": 1 },
|
|
373
|
+
"resourceSlots": { "type": "array" },
|
|
374
|
+
"vertexInputs": { "type": "array" }
|
|
375
|
+
},
|
|
376
|
+
"additionalProperties": false
|
|
377
|
+
},
|
|
378
|
+
"receiptIdentity": { "type": "string", "minLength": 1 },
|
|
379
|
+
"generation": { "type": "integer", "minimum": 1 }
|
|
380
|
+
},
|
|
381
|
+
"additionalProperties": false
|
|
382
|
+
},
|
|
276
383
|
"context": {
|
|
277
384
|
"type": "object",
|
|
278
385
|
"required": [
|
|
@@ -684,7 +684,11 @@ export async function loadScriptablePack(
|
|
|
684
684
|
options: LoadScriptablePackOptions = {},
|
|
685
685
|
): Promise<Result<Readonly<AnyScriptablePackDefinition>, PackAuthoringError>> {
|
|
686
686
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
687
|
-
|
|
687
|
+
// Keep authored builds bounded, but give legitimate procedural packs the
|
|
688
|
+
// same cold-start budget already used for module initialization. The old
|
|
689
|
+
// five-second default rejected large production packs before they could
|
|
690
|
+
// publish a result.
|
|
691
|
+
const buildTimeoutMs = options.buildTimeoutMs ?? 15_000;
|
|
688
692
|
const executor = options.executor ?? new WorkerScriptablePackExecutor();
|
|
689
693
|
let disposeReason: 'complete' | 'timeout' | 'failure' | undefined;
|
|
690
694
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, extname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
import { parentPort, workerData } from 'node:worker_threads';
|
|
@@ -66,15 +66,26 @@ const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs']
|
|
|
66
66
|
function resolveRelativeImport(importer: string, specifier: string): string | undefined {
|
|
67
67
|
if (!specifier.startsWith('.')) return undefined;
|
|
68
68
|
const raw = resolve(dirname(importer), specifier);
|
|
69
|
+
const extension = extname(raw);
|
|
70
|
+
const stem = raw.slice(0, raw.length - extension.length);
|
|
71
|
+
// Authored NodeNext sources use runtime extensions even when their producer files are TypeScript.
|
|
72
|
+
const substitutions =
|
|
73
|
+
extension === '.js'
|
|
74
|
+
? [`${stem}.ts`, `${stem}.tsx`]
|
|
75
|
+
: extension === '.mjs'
|
|
76
|
+
? [`${stem}.mts`]
|
|
77
|
+
: extension === '.cjs'
|
|
78
|
+
? [`${stem}.cts`]
|
|
79
|
+
: [];
|
|
69
80
|
const candidates =
|
|
70
|
-
|
|
71
|
-
? [raw]
|
|
81
|
+
extension.length > 0
|
|
82
|
+
? [...substitutions, raw]
|
|
72
83
|
: [
|
|
73
84
|
raw,
|
|
74
85
|
...SOURCE_EXTENSIONS.map((extension) => `${raw}${extension}`),
|
|
75
86
|
resolve(raw, 'index.ts'),
|
|
76
87
|
];
|
|
77
|
-
return candidates.find((candidate) => existsSync(candidate));
|
|
88
|
+
return candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
function outputName(path: string): string {
|
package/src/scriptable-pack.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Asset, AssetGuid, Result } from '@forgeax/engine-types';
|
|
2
|
+
import type { PackCookSource } from './pack-authoring.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build-time Pack vocabulary shared by the source loader and output
|
|
@@ -82,7 +83,7 @@ export interface ScriptablePackSourceClosureEntry {
|
|
|
82
83
|
|
|
83
84
|
/** AssetReader is intentionally a small, realm-neutral source boundary. */
|
|
84
85
|
export interface AssetReader {
|
|
85
|
-
readByGuid<TAsset extends Asset = Asset>(
|
|
86
|
+
readByGuid<TAsset extends Asset | PackCookSource = Asset>(
|
|
86
87
|
guid: AssetGuid,
|
|
87
88
|
): Promise<Result<TAsset, ScriptablePackReadError>>;
|
|
88
89
|
}
|