@forgeax/engine-pack 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +52 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  4. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/package-finalizer.contract.test.d.ts +2 -0
  6. package/dist/__tests__/package-finalizer.contract.test.d.ts.map +1 -0
  7. package/dist/__tests__/runtime.browser.test.d.ts +2 -0
  8. package/dist/__tests__/runtime.browser.test.d.ts.map +1 -0
  9. package/dist/__tests__/scanner-inventory.contract.test.d.ts +2 -0
  10. package/dist/__tests__/scanner-inventory.contract.test.d.ts.map +1 -0
  11. package/dist/artifact-path.mjs +53 -0
  12. package/dist/artifact-path.mjs.map +1 -0
  13. package/dist/atlas/shelf-pack.d.ts.map +1 -1
  14. package/dist/build.d.ts +19 -0
  15. package/dist/build.d.ts.map +1 -0
  16. package/dist/build.mjs +10168 -0
  17. package/dist/build.mjs.map +1 -0
  18. package/dist/builtin.d.ts +20 -0
  19. package/dist/builtin.d.ts.map +1 -1
  20. package/dist/builtin.mjs +141 -0
  21. package/dist/builtin.mjs.map +1 -0
  22. package/dist/catalog-builder.d.ts +43 -0
  23. package/dist/catalog-builder.d.ts.map +1 -0
  24. package/dist/catalog-delta.d.ts +11 -0
  25. package/dist/catalog-delta.d.ts.map +1 -0
  26. package/dist/catalog-projection.d.ts +91 -0
  27. package/dist/catalog-projection.d.ts.map +1 -0
  28. package/dist/cli-asset.mjs +231 -484
  29. package/dist/cli-asset.mjs.map +1 -1
  30. package/dist/index.mjs +83 -1
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/material-cook.d.ts +2 -0
  33. package/dist/material-cook.d.ts.map +1 -0
  34. package/dist/material-cook.mjs +193 -0
  35. package/dist/material-cook.mjs.map +1 -0
  36. package/dist/mesh-bin-contract.mjs +96 -0
  37. package/dist/mesh-bin-contract.mjs.map +1 -0
  38. package/dist/native-cooker-registry.d.ts +19 -0
  39. package/dist/native-cooker-registry.d.ts.map +1 -1
  40. package/dist/native-cooker.mjs +37 -0
  41. package/dist/native-cooker.mjs.map +1 -1
  42. package/dist/package-finalizer.d.ts +121 -0
  43. package/dist/package-finalizer.d.ts.map +1 -0
  44. package/dist/resolve-asset-source.d.ts.map +1 -1
  45. package/dist/resolve-asset-source.mjs +16 -6
  46. package/dist/resolve-asset-source.mjs.map +1 -1
  47. package/dist/runtime-publication.d.ts +44 -0
  48. package/dist/runtime-publication.d.ts.map +1 -0
  49. package/dist/runtime.d.ts +8 -0
  50. package/dist/runtime.d.ts.map +1 -0
  51. package/dist/runtime.mjs +7261 -0
  52. package/dist/runtime.mjs.map +1 -0
  53. package/dist/scanner.d.ts +87 -54
  54. package/dist/scanner.d.ts.map +1 -1
  55. package/dist/scanner.mjs +258 -454
  56. package/dist/scanner.mjs.map +1 -1
  57. package/dist/scriptable-pack-node.d.ts +3 -8
  58. package/dist/scriptable-pack-node.d.ts.map +1 -1
  59. package/dist/scriptable-pack-node.mjs +93 -9
  60. package/dist/scriptable-pack-node.mjs.map +1 -1
  61. package/dist/scriptable-pack-worker.mjs +1 -0
  62. package/dist/scriptable-pack-worker.mjs.map +1 -1
  63. package/dist/scriptable-pack.d.ts +33 -0
  64. package/dist/scriptable-pack.d.ts.map +1 -1
  65. package/dist/scriptable-pack.mjs +91 -2
  66. package/dist/scriptable-pack.mjs.map +1 -1
  67. package/dist/topology.d.ts +1 -1
  68. package/dist/topology.d.ts.map +1 -1
  69. package/package.json +27 -2
  70. package/src/__tests__/mesh-bin-consumer-surface.unit.test.ts +2 -1
  71. package/src/__tests__/owner-chain.integration.test.ts +78 -0
  72. package/src/__tests__/package-finalizer.contract.test.ts +105 -0
  73. package/src/__tests__/runtime.browser.test.ts +15 -0
  74. package/src/__tests__/scanner-inventory.contract.test.ts +167 -0
  75. package/src/__tests__/scanner-inventory.test.ts +16 -12
  76. package/src/__tests__/scriptable-pack.unit.test.ts +42 -0
  77. package/src/atlas/shelf-pack.ts +20 -24
  78. package/src/build.ts +139 -0
  79. package/src/builtin.ts +38 -0
  80. package/src/catalog-builder.ts +313 -0
  81. package/src/catalog-delta.ts +189 -0
  82. package/src/catalog-projection.ts +344 -0
  83. package/src/cli-asset.ts +1 -5
  84. package/src/index.ts +2 -2
  85. package/src/material-cook.ts +13 -0
  86. package/src/native-cooker-registry.ts +72 -0
  87. package/src/package-finalizer.ts +394 -0
  88. package/src/resolve-asset-source.ts +2 -7
  89. package/src/runtime-publication.ts +190 -0
  90. package/src/runtime.ts +23 -0
  91. package/src/scanner.ts +289 -290
  92. package/src/scriptable-pack-node.ts +27 -23
  93. package/src/scriptable-pack-worker.ts +2 -0
  94. package/src/scriptable-pack.ts +123 -1
  95. package/src/topology.ts +2 -1
@@ -22,6 +22,7 @@ import {
22
22
  type ScriptablePackDefinition,
23
23
  type ScriptablePackError,
24
24
  type ScriptablePackReadError,
25
+ type ScriptablePackSourceClosureEntry,
25
26
  validateScriptablePackDefinition,
26
27
  } from './scriptable-pack.js';
27
28
 
@@ -76,13 +77,17 @@ export async function readOptionalBuildCache<T>(cache: OptionalBuildCache<T>): P
76
77
  return { value: await cache.coldCook(), fromCache: false };
77
78
  }
78
79
 
79
- /** Serializable source-closure evidence shared by scanner and producer paths. */
80
- export interface ScriptablePackSourceClosureEntry {
81
- readonly path: string;
82
- readonly digest: string;
80
+ export interface LoadScriptablePackOptions {
81
+ /** Maximum time allowed for module initialization in the isolated worker. */
82
+ readonly timeoutMs?: number;
83
+ /** Maximum time allowed for one definition.build(reader) in the isolated worker. */
84
+ readonly buildTimeoutMs?: number;
85
+ readonly executor?: ScriptablePackModuleExecutor;
86
+ /** Release the isolated loader after identity projection when build will not be called. */
87
+ readonly metadataOnly?: boolean;
83
88
  }
84
89
 
85
- async function resolveRelativeSourceImport(
90
+ async function resolveRelativeImport(
86
91
  sourcePath: string,
87
92
  specifier: string,
88
93
  ): Promise<string | undefined> {
@@ -111,38 +116,33 @@ async function resolveRelativeSourceImport(
111
116
  return undefined;
112
117
  }
113
118
 
114
- /** Inventory a ScriptablePack module without importing or executing it. */
119
+ /** Capture one immutable ScriptablePack module closure for inventory and production. */
115
120
  export async function inventoryScriptablePackSource(
116
121
  sourcePath: string,
122
+ initialSourceText?: string,
117
123
  ): Promise<readonly ScriptablePackSourceClosureEntry[]> {
118
- const pending = [await realpath(sourcePath)];
124
+ const root = await realpath(sourcePath);
125
+ const pending = [root];
119
126
  const seen = new Set<string>();
120
127
  const entries: ScriptablePackSourceClosureEntry[] = [];
121
128
  while (pending.length > 0) {
122
129
  const path = pending.pop();
123
130
  if (path === undefined || seen.has(path)) continue;
124
131
  seen.add(path);
125
- const bytes = await readFile(path);
132
+ const bytes =
133
+ path === root && initialSourceText !== undefined
134
+ ? new TextEncoder().encode(initialSourceText)
135
+ : await readFile(path);
126
136
  entries.push({ path, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` });
127
- const imports = ts.preProcessFile(new TextDecoder().decode(bytes), true, true).importedFiles;
128
- for (const imported of imports) {
129
- const resolved = await resolveRelativeSourceImport(path, imported.fileName);
137
+ const source = new TextDecoder().decode(bytes);
138
+ for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
139
+ const resolved = await resolveRelativeImport(path, imported.fileName);
130
140
  if (resolved !== undefined) pending.push(await realpath(resolved));
131
141
  }
132
142
  }
133
143
  return entries.sort((left, right) => left.path.localeCompare(right.path));
134
144
  }
135
145
 
136
- export interface LoadScriptablePackOptions {
137
- /** Maximum time allowed for module initialization in the isolated worker. */
138
- readonly timeoutMs?: number;
139
- /** Maximum time allowed for one definition.build(reader) in the isolated worker. */
140
- readonly buildTimeoutMs?: number;
141
- readonly executor?: ScriptablePackModuleExecutor;
142
- /** Release the isolated loader after identity projection when build will not be called. */
143
- readonly metadataOnly?: boolean;
144
- }
145
-
146
146
  interface StructuredFailure {
147
147
  readonly name?: unknown;
148
148
  readonly code?: unknown;
@@ -246,6 +246,10 @@ class WorkerScriptablePackExecutor implements ScriptablePackModuleExecutor {
246
246
  if (message === null || typeof message !== 'object') return;
247
247
  const value = message as Record<string, unknown>;
248
248
  if (value.kind === 'loaded') {
249
+ // The scan keeps this executor alive for the later no-reopen build
250
+ // route, but an idle source worker must not keep a Vite/Vitest host
251
+ // process alive after its own work has settled.
252
+ worker.unref();
249
253
  worker.off('message', onMessage);
250
254
  resolveLoad({
251
255
  default:
@@ -966,7 +970,7 @@ export default {
966
970
  packageId: ${guidExpression(manifest.packageId)},
967
971
  ${manifest.name === undefined ? '' : `name: ${JSON.stringify(manifest.name)},\n `}assets,
968
972
  externalAssets,
969
- build: () => ok(Object.fromEntries(Object.entries(assets).map(([sourceKey, asset]) => [sourceKey, createOutput(asset.kind)])) as never),
973
+ build: () => ok(Object.fromEntries(Object.entries(assets).map(([sourceKey, asset]) => [sourceKey, createOutput(asset.kind)])) as Record<string, Asset>),
970
974
  } satisfies ScriptablePackDefinition;
971
975
  `;
972
976
  }
@@ -1000,7 +1004,7 @@ function definitionFromCanonical(
1000
1004
  ...(manifest.name === undefined ? {} : { name: manifest.name }),
1001
1005
  assets,
1002
1006
  externalAssets,
1003
- build: () => ok({} as never),
1007
+ build: () => ok({} as Record<string, never>),
1004
1008
  });
1005
1009
  }
1006
1010
 
@@ -21,6 +21,7 @@ type LoadedDefinition = {
21
21
  readonly packageId?: unknown;
22
22
  readonly name?: unknown;
23
23
  readonly assets?: unknown;
24
+ readonly sceneComponents?: unknown;
24
25
  readonly externalAssets?: unknown;
25
26
  readonly build?: unknown;
26
27
  };
@@ -232,6 +233,7 @@ async function loadDefinition(task: {
232
233
  packageId: definition.packageId,
233
234
  name: definition.name,
234
235
  assets: definition.assets,
236
+ sceneComponents: definition.sceneComponents,
235
237
  externalAssets: definition.externalAssets,
236
238
  },
237
239
  });
@@ -25,6 +25,15 @@ export const SCRIPTABLE_PACK_ASSET_KINDS = [
25
25
  'particle-effect',
26
26
  ] as const satisfies readonly ScriptablePackAssetKind[];
27
27
 
28
+ /** Machine-readable producer/loader floor owned by engine-pack. */
29
+ export const SCRIPTABLE_PACK_CAPABILITY_MANIFEST = {
30
+ assetKinds: SCRIPTABLE_PACK_ASSET_KINDS,
31
+ durablePayload: true,
32
+ refs: true,
33
+ artifacts: true,
34
+ hostCapabilities: ['audio-install', 'video-play', 'particle-execute'] as const,
35
+ } as const;
36
+
28
37
  export function isScriptablePackAssetKind(value: string): value is ScriptablePackAssetKind {
29
38
  return (SCRIPTABLE_PACK_ASSET_KINDS as readonly string[]).includes(value);
30
39
  }
@@ -43,6 +52,43 @@ export type ScriptablePackAssetDeclarations = Readonly<
43
52
 
44
53
  export type ScriptablePackExternalAssets = Readonly<Record<string, AssetGuid>>;
45
54
 
55
+ /**
56
+ * Component-shaped input accepted by a ScriptablePack scene declaration.
57
+ * Engine component tokens satisfy this shape; plain `{ type }` rows keep the
58
+ * Pack contract independent of ECS and survive the isolated module worker.
59
+ */
60
+ export interface ScriptablePackSceneComponentInput {
61
+ readonly name: string;
62
+ readonly fields: Readonly<Record<string, string | { readonly type: string }>>;
63
+ }
64
+
65
+ /** Neutral scene schema projection owned by the Pack definition. */
66
+ export interface ScriptablePackSceneComponent {
67
+ readonly name: string;
68
+ readonly fields: Readonly<Record<string, string>>;
69
+ }
70
+
71
+ function sceneComponentFieldType(value: unknown): string | undefined {
72
+ if (typeof value === 'string') return value;
73
+ if (isRecord(value) && typeof value.type === 'string') return value.type;
74
+ return undefined;
75
+ }
76
+
77
+ /** Derive the frozen, ECS-neutral schema used by scene output producers. */
78
+ export function projectScriptablePackSceneComponents(
79
+ components: readonly ScriptablePackSceneComponentInput[] | undefined,
80
+ ): readonly ScriptablePackSceneComponent[] {
81
+ return (components ?? []).map((component) => ({
82
+ name: component.name,
83
+ fields: Object.fromEntries(
84
+ Object.entries(component.fields).map(([fieldName, field]) => [
85
+ fieldName,
86
+ sceneComponentFieldType(field) as string,
87
+ ]),
88
+ ),
89
+ }));
90
+ }
91
+
46
92
  /** Pack-facing name for the shared Engine publication envelope. */
47
93
  export type ScriptablePackPublicationEnvelope = AssetPublicationEnvelope;
48
94
 
@@ -96,6 +142,8 @@ export interface ScriptablePackDefinition<
96
142
  readonly packageId: AssetGuid;
97
143
  readonly name?: string;
98
144
  readonly assets: TAssets;
145
+ /** Component tokens/schema rows used by scene ref externalization. */
146
+ readonly sceneComponents?: readonly ScriptablePackSceneComponentInput[];
99
147
  readonly externalAssets: TExternal;
100
148
  readonly build: (
101
149
  assetReader: AssetReader,
@@ -104,6 +152,11 @@ export interface ScriptablePackDefinition<
104
152
  | Promise<ScriptablePackBuildResult<TAssets, TError>>;
105
153
  }
106
154
 
155
+ export interface ScriptablePackSourceClosureEntry {
156
+ readonly path: string;
157
+ readonly digest: string;
158
+ }
159
+
107
160
  export interface ScriptablePackMetaJson {
108
161
  readonly schemaVersion: '1.0.0';
109
162
  readonly kind: 'external-asset-package';
@@ -707,7 +760,7 @@ function invalid(
707
760
  });
708
761
  }
709
762
 
710
- function isRecord(value: unknown): value is Record<string, unknown> {
763
+ export function isRecord(value: unknown): value is Record<string, unknown> {
711
764
  return value !== null && typeof value === 'object' && !Array.isArray(value);
712
765
  }
713
766
 
@@ -734,11 +787,23 @@ function freezeDefinition(
734
787
  guid.slice() as AssetGuid,
735
788
  ]),
736
789
  );
790
+ const sceneComponents =
791
+ definition.sceneComponents === undefined
792
+ ? undefined
793
+ : Object.freeze(
794
+ projectScriptablePackSceneComponents(definition.sceneComponents).map((component) =>
795
+ Object.freeze({
796
+ ...component,
797
+ fields: Object.freeze({ ...component.fields }),
798
+ }),
799
+ ),
800
+ );
737
801
  return Object.freeze({
738
802
  ...definition,
739
803
  packageId: definition.packageId.slice() as AssetGuid,
740
804
  assets: Object.freeze(assets),
741
805
  externalAssets: Object.freeze(externalAssets),
806
+ ...(sceneComponents === undefined ? {} : { sceneComponents }),
742
807
  });
743
808
  }
744
809
 
@@ -773,6 +838,63 @@ export function validateScriptablePackDefinition(
773
838
  sourcePath,
774
839
  );
775
840
  }
841
+ if (value.sceneComponents !== undefined) {
842
+ if (!Array.isArray(value.sceneComponents)) {
843
+ return invalid(
844
+ '$.sceneComponents',
845
+ 'an array of component tokens or neutral schema rows when present',
846
+ value.sceneComponents,
847
+ sourcePath,
848
+ );
849
+ }
850
+ const componentNames = new Set<string>();
851
+ for (const [componentIndex, component] of value.sceneComponents.entries()) {
852
+ if (!isRecord(component)) {
853
+ return invalid(
854
+ `$.sceneComponents[${componentIndex}]`,
855
+ 'a component token or neutral schema row',
856
+ component,
857
+ sourcePath,
858
+ );
859
+ }
860
+ if (typeof component.name !== 'string' || component.name.trim().length === 0) {
861
+ return invalid(
862
+ `$.sceneComponents[${componentIndex}].name`,
863
+ 'a non-empty component name',
864
+ component.name,
865
+ sourcePath,
866
+ );
867
+ }
868
+ if (componentNames.has(component.name)) {
869
+ return invalid(
870
+ `$.sceneComponents[${componentIndex}].name`,
871
+ 'component names to be unique within one ScriptablePack',
872
+ component.name,
873
+ sourcePath,
874
+ );
875
+ }
876
+ componentNames.add(component.name);
877
+ if (!isRecord(component.fields)) {
878
+ return invalid(
879
+ `$.sceneComponents[${componentIndex}].fields`,
880
+ 'a field-name to schema-type object',
881
+ component.fields,
882
+ sourcePath,
883
+ );
884
+ }
885
+ for (const [fieldName, field] of Object.entries(component.fields)) {
886
+ const type = sceneComponentFieldType(field);
887
+ if (fieldName.trim().length === 0 || type === undefined || type.trim().length === 0) {
888
+ return invalid(
889
+ `$.sceneComponents[${componentIndex}].fields`,
890
+ 'field names with non-empty string schema types',
891
+ field,
892
+ sourcePath,
893
+ );
894
+ }
895
+ }
896
+ }
897
+ }
776
898
  if (typeof value.build !== 'function') {
777
899
  return invalid('$.build', 'a build(assetReader) function', value.build, sourcePath);
778
900
  }
package/src/topology.ts CHANGED
@@ -152,5 +152,6 @@ export function diffTopology(
152
152
  return { preserved, added, removed, changedKind, ambiguous };
153
153
  }
154
154
 
155
- /** Alias named after the producer-facing contract wording. */
156
155
  export const calculateTopologyDiff = diffTopology;
156
+
157
+ /** Alias named after the producer-facing contract wording. */