@platforma-sdk/tengo-builder 2.5.30 → 3.0.1

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 (37) hide show
  1. package/dist/compiler/compiler.cjs +46 -1
  2. package/dist/compiler/compiler.cjs.map +1 -1
  3. package/dist/compiler/compiler.js +46 -1
  4. package/dist/compiler/compiler.js.map +1 -1
  5. package/dist/compiler/main.cjs +132 -6
  6. package/dist/compiler/main.cjs.map +1 -1
  7. package/dist/compiler/main.js +132 -6
  8. package/dist/compiler/main.js.map +1 -1
  9. package/dist/compiler/pack_limits.cjs +49 -0
  10. package/dist/compiler/pack_limits.cjs.map +1 -0
  11. package/dist/compiler/pack_limits.js +47 -0
  12. package/dist/compiler/pack_limits.js.map +1 -0
  13. package/dist/compiler/package.cjs.map +1 -1
  14. package/dist/compiler/package.js.map +1 -1
  15. package/dist/compiler/source.cjs +6 -2
  16. package/dist/compiler/source.cjs.map +1 -1
  17. package/dist/compiler/source.js +6 -2
  18. package/dist/compiler/source.js.map +1 -1
  19. package/dist/shared/basecmd.cjs +2 -1
  20. package/dist/shared/basecmd.cjs.map +1 -1
  21. package/dist/shared/basecmd.js +2 -1
  22. package/dist/shared/basecmd.js.map +1 -1
  23. package/dist/shared/dump.cjs +4 -0
  24. package/dist/shared/dump.cjs.map +1 -1
  25. package/dist/shared/dump.js +4 -0
  26. package/dist/shared/dump.js.map +1 -1
  27. package/package.json +6 -6
  28. package/src/compiler/compiler.test.ts +91 -0
  29. package/src/compiler/compiler.ts +75 -1
  30. package/src/compiler/main.ts +217 -11
  31. package/src/compiler/pack_limits.test.ts +173 -0
  32. package/src/compiler/pack_limits.ts +103 -0
  33. package/src/compiler/package.ts +1 -1
  34. package/src/compiler/source.test.ts +30 -0
  35. package/src/compiler/source.ts +4 -0
  36. package/src/shared/basecmd.ts +1 -1
  37. package/src/shared/dump.ts +9 -0
@@ -333,3 +333,94 @@ describe("TengoTemplateCompiler.compileAndAdd", () => {
333
333
  expect(() => compiler.compileAndAdd(sources)).toThrow(/Unsatisfied dependencies/);
334
334
  });
335
335
  });
336
+
337
+ describe("TengoTemplateCompiler requiredCapabilities propagation", () => {
338
+ const wasmName: FullArtifactName = {
339
+ type: "wasm",
340
+ pkg: "@test/pkg",
341
+ id: "main",
342
+ version: "1.0.0",
343
+ };
344
+ const wasmSrc = new ArtifactSource(
345
+ "dist",
346
+ wasmName,
347
+ "wasm-hash",
348
+ "wasm-bytes-base64",
349
+ "wasm-file",
350
+ [],
351
+ [],
352
+ );
353
+
354
+ it("adds wasm:v1 when a template directly embeds a wasm artifact", () => {
355
+ const compiler = new TengoTemplateCompiler("dist");
356
+ compiler.addWasm(wasmSrc);
357
+
358
+ const tplName: FullArtifactName = {
359
+ type: "template",
360
+ pkg: "@test/pkg",
361
+ id: "tpl",
362
+ version: "1.0.0",
363
+ };
364
+ const result = compiler.compileAndAdd([
365
+ new ArtifactSource("dist", tplName, "tpl-hash", "tpl-src", "tpl-file", [wasmName], []),
366
+ ]);
367
+
368
+ expect(result.templates[0].data.template.requiredCapabilities).toEqual(["wasm:v1"]);
369
+ });
370
+
371
+ it("unions child template requiredCapabilities into the parent", () => {
372
+ const compiler = new TengoTemplateCompiler("dist");
373
+ compiler.addWasm(wasmSrc);
374
+
375
+ const childName: FullArtifactName = {
376
+ type: "template",
377
+ pkg: "@test/pkg",
378
+ id: "child",
379
+ version: "1.0.0",
380
+ };
381
+ const parentName: FullArtifactName = {
382
+ type: "template",
383
+ pkg: "@test/pkg",
384
+ id: "parent",
385
+ version: "1.0.0",
386
+ };
387
+ const childSrc = new ArtifactSource(
388
+ "dist",
389
+ childName,
390
+ "child-hash",
391
+ "child-src",
392
+ "child-file",
393
+ [wasmName],
394
+ [],
395
+ );
396
+ const parentSrc = new ArtifactSource(
397
+ "dist",
398
+ parentName,
399
+ "parent-hash",
400
+ "parent-src",
401
+ "parent-file",
402
+ [childName],
403
+ [],
404
+ );
405
+
406
+ const result = compiler.compileAndAdd([childSrc, parentSrc]);
407
+ const parent = result.templates.find((t) => t.fullName.id === "parent")!;
408
+ expect(parent.data.template.requiredCapabilities).toEqual(["wasm:v1"]);
409
+ });
410
+
411
+ it("leaves requiredCapabilities undefined when no capability is needed", () => {
412
+ const compiler = new TengoTemplateCompiler("dist");
413
+
414
+ const tplName: FullArtifactName = {
415
+ type: "template",
416
+ pkg: "@test/pkg",
417
+ id: "plain",
418
+ version: "1.0.0",
419
+ };
420
+ const result = compiler.compileAndAdd([
421
+ new ArtifactSource("dist", tplName, "plain-hash", "plain-src", "plain-file", [], []),
422
+ ]);
423
+
424
+ expect(result.templates[0].data.template.requiredCapabilities).toBeUndefined();
425
+ });
426
+ });
@@ -13,7 +13,21 @@ import {
13
13
  import { ArtifactStore } from "./artifactset";
14
14
  import { assertNever } from "./util";
15
15
  import { applyLibraryCompilerOptions, applyTemplateCompilerOptions } from "./compileroptions";
16
- import type { CompiledTemplateV3 } from "@milaboratories/pl-model-backend";
16
+ import type {
17
+ BackendCapability,
18
+ CompiledTemplateV3,
19
+ TemplateDataV3,
20
+ } from "@milaboratories/pl-model-backend";
21
+
22
+ /** Add a capability token to a template's `requiredCapabilities` list,
23
+ * skipping duplicates. Initializes the array lazily so the field stays
24
+ * absent for templates that need nothing. */
25
+ function addRequiredCapability(template: TemplateDataV3, capability: BackendCapability): void {
26
+ template.requiredCapabilities = template.requiredCapabilities ?? [];
27
+ if (!template.requiredCapabilities.includes(capability)) {
28
+ template.requiredCapabilities.push(capability);
29
+ }
30
+ }
17
31
 
18
32
  /** A compilation result. */
19
33
  export interface TemplatesAndLibs {
@@ -21,12 +35,14 @@ export interface TemplatesAndLibs {
21
35
  libs: ArtifactSource[];
22
36
  software: ArtifactSource[];
23
37
  assets: ArtifactSource[];
38
+ wasm: ArtifactSource[];
24
39
  }
25
40
 
26
41
  export class TengoTemplateCompiler {
27
42
  private readonly libs = new ArtifactStore<ArtifactSource>((src) => src.fullName);
28
43
  private readonly software = new ArtifactStore<ArtifactSource>((src) => src.fullName);
29
44
  private readonly assets = new ArtifactStore<ArtifactSource>((src) => src.fullName);
45
+ private readonly wasm = new ArtifactStore<ArtifactSource>((src) => src.fullName);
30
46
  private readonly templates = new ArtifactStore<TemplateWithSource>((tpl) => tpl.fullName);
31
47
 
32
48
  constructor(private readonly compileMode: CompileMode) {}
@@ -89,6 +105,20 @@ export class TengoTemplateCompiler {
89
105
  data.hashToSource[asset.sourceHash] = asset.src;
90
106
  break;
91
107
  }
108
+ case "wasm": {
109
+ const wasm = this.getWasmOrError(dep);
110
+ data.template.wasm = data.template.wasm ?? {};
111
+ data.template.wasm[artifactNameToString(dep)] = {
112
+ ...formatArtefactNameAndVersion(wasm.fullName),
113
+ sourceHash: wasm.sourceHash,
114
+ };
115
+ data.hashToSource[wasm.sourceHash] = wasm.src;
116
+ // Embedding a wasm artifact means this template requires the
117
+ // backend's wasm runtime to run. The token is propagated upward
118
+ // when this template is in turn embedded as a sub-template.
119
+ addRequiredCapability(data.template, "wasm:v1");
120
+ break;
121
+ }
92
122
  case "template": {
93
123
  if (typedArtifactNamesEquals(fullName, dep))
94
124
  // skipping self reference
@@ -103,6 +133,13 @@ export class TengoTemplateCompiler {
103
133
  data.hashToSource[hash] = src;
104
134
  }
105
135
 
136
+ // Union the sub-template's compile-time-computed capability
137
+ // requirements into ours, so the top template carries the full
138
+ // transitive set without consumers having to walk the tree.
139
+ for (const cap of tpl.data.template.requiredCapabilities ?? []) {
140
+ addRequiredCapability(data.template, cap);
141
+ }
142
+
106
143
  break;
107
144
  }
108
145
  case "test":
@@ -234,6 +271,31 @@ export class TengoTemplateCompiler {
234
271
  return asset;
235
272
  }
236
273
 
274
+ addWasm(wasm: ArtifactSource) {
275
+ const wasmFromMap = this.wasm.add(wasm.compileMode, wasm, false);
276
+ if (wasmFromMap && !fullNameEquals(wasm.fullName, wasmFromMap.fullName))
277
+ throw new Error(
278
+ `compiler already contain info for wasm: adding = ${fullNameToString(wasm.fullName)}, contains = ${fullNameToString(wasmFromMap.fullName)}`,
279
+ );
280
+ }
281
+
282
+ allWasm(): ArtifactSource[] {
283
+ return this.wasm.array(this.compileMode);
284
+ }
285
+
286
+ getWasm(name: TypedArtifactName): ArtifactSource | undefined {
287
+ if (name.type !== "wasm")
288
+ throw new Error(`illegal artifact type: got ${name.type} instead of 'wasm'`);
289
+
290
+ return this.wasm.get(this.compileMode, name);
291
+ }
292
+
293
+ getWasmOrError(name: TypedArtifactName): ArtifactSource {
294
+ const wasm = this.getWasm(name);
295
+ if (!wasm) throw new Error(`wasm info not found: ${artifactNameToString(name)}`);
296
+ return wasm;
297
+ }
298
+
237
299
  addTemplate(tpl: TemplateWithSource) {
238
300
  const tplFromMap = this.templates.add(tpl.compileMode, tpl, false);
239
301
  if (tplFromMap && !fullNameEquals(tpl.fullName, tplFromMap.fullName))
@@ -268,6 +330,8 @@ export class TengoTemplateCompiler {
268
330
  return this.getSoftware(name);
269
331
  case "asset":
270
332
  return this.getAsset(name);
333
+ case "wasm":
334
+ return this.getWasm(name);
271
335
  case "test":
272
336
  // Tests are ignored by the complier. They should never be compiled into templates or libs and
273
337
  // should never be a dependency.
@@ -283,6 +347,7 @@ export class TengoTemplateCompiler {
283
347
  libs: [],
284
348
  software: [],
285
349
  assets: [],
350
+ wasm: [],
286
351
  };
287
352
 
288
353
  let current: ArtifactSource[] = [];
@@ -300,6 +365,10 @@ export class TengoTemplateCompiler {
300
365
  // add assets 'as-is' to be able to resolve them as dependencies
301
366
  this.addAsset(src);
302
367
  result.assets.push(src);
368
+ } else if (src.fullName.type === "wasm") {
369
+ // add wasm 'as-is' to be able to resolve them as dependencies
370
+ this.addWasm(src);
371
+ result.wasm.push(src);
303
372
  } else {
304
373
  current.push(src);
305
374
  }
@@ -349,6 +418,11 @@ export class TengoTemplateCompiler {
349
418
  this.addAsset(src);
350
419
  result.assets.push(src);
351
420
  break;
421
+ case "wasm":
422
+ // wasm dependencies are added as is
423
+ this.addWasm(src);
424
+ result.wasm.push(src);
425
+ break;
352
426
  case "template":
353
427
  // templates are compiled and then added
354
428
  try {
@@ -13,6 +13,18 @@ import { newTemplateFromContent, templateToSource } from "./template";
13
13
  import type winston from "winston";
14
14
  import { tryResolve, tryResolveOrError } from "@milaboratories/resolve-helper";
15
15
  import { serializeTemplate } from "@milaboratories/pl-model-backend";
16
+ import {
17
+ assertTemplatePackSize,
18
+ assertWasmFileSize,
19
+ collectWasmContributions,
20
+ } from "./pack_limits";
21
+
22
+ // Convention: wasm component packages declare their wasm under a "wasm" condition
23
+ // in package.json `exports`. Subpath "." maps to the tengo id "main"; subpath
24
+ // "./foo" maps to id "foo". Other shapes of `exports` (string-valued, condition-only)
25
+ // don't produce wasm artifacts.
26
+ const wasmExportCondition = "wasm";
27
+ const defaultWasmExportId = "main";
16
28
 
17
29
  interface PackageId {
18
30
  /** Package name from package.json */
@@ -44,6 +56,7 @@ const compiledTplSuffix = ".plj.gz";
44
56
  const compiledLibSuffix = ".lib.tengo";
45
57
  const compiledSoftwareSuffix = ".sw.json";
46
58
  const compiledAssetSuffix = ".as.json";
59
+ const compiledWasmSuffix = ".wasm";
47
60
 
48
61
  // We need to keep track of dependencies for correct tgo-test CLI utility configuraiton.
49
62
  // It is much simpler to do this here, than duplicate all tle logic regarding dependencies
@@ -79,15 +92,32 @@ function resolvePackageJsonPackage(root: string, depPackageName: string): string
79
92
 
80
93
  // Second approach: trying to find package.json in the package dir.
81
94
  const resolved2 = tryResolveOrError(root, `${depPackageName}/package.json`);
82
- if (resolved2.result === undefined) {
83
- if (resolved2.err === "ERR_PACKAGE_PATH_NOT_EXPORTED")
84
- // tolerating not-exported package.json for dev dependencies
85
- return undefined;
86
- throw new Error(
87
- `Can't resolve package.json for package ${depPackageName ?? "."} relative to ${root}`,
88
- );
95
+ if (resolved2.result !== undefined) {
96
+ return resolved2.result;
97
+ }
98
+
99
+ // Third approach: some packages (wasm-only components) don't expose any
100
+ // import-resolvable entry point — neither a default export nor `./package.json`.
101
+ // pnpm still installs them at <root>/node_modules/<depPackageName>/, so walk
102
+ // the parent chain and check that path directly.
103
+ if (resolved2.err === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
104
+ const fromNodeModules = findInNodeModules(root, depPackageName);
105
+ if (fromNodeModules !== undefined) return fromNodeModules;
106
+ // tolerating not-exported package.json for dev dependencies even if no
107
+ // physical node_modules copy is found.
108
+ return undefined;
89
109
  }
90
- return resolved2.result;
110
+ throw new Error(
111
+ `Can't resolve package.json for package ${depPackageName ?? "."} relative to ${root}`,
112
+ );
113
+ }
114
+
115
+ function findInNodeModules(root: string, depPackageName: string): string | undefined {
116
+ const candidate = path.join(root, "node_modules", depPackageName, "package.json");
117
+ if (pathType(candidate) === "file") return candidate;
118
+ const parent = path.dirname(root);
119
+ if (parent === root) return undefined;
120
+ return findInNodeModules(parent, depPackageName);
91
121
  }
92
122
 
93
123
  export function resolvePackageJsonRoot(root: string): string {
@@ -165,6 +195,10 @@ function resolveAssetsDst(mode: CompileMode, root: string) {
165
195
  return path.resolve(root, mode, "tengo", "asset");
166
196
  }
167
197
 
198
+ function resolveWasmDst(mode: CompileMode, root: string) {
199
+ return path.resolve(root, mode, "tengo", "wasm");
200
+ }
201
+
168
202
  function loadDependencies(
169
203
  logger: winston.Logger,
170
204
  compiler: TengoTemplateCompiler,
@@ -181,13 +215,23 @@ function loadDependencies(
181
215
  const tplDistFolder = resolveTemplatesDst("dist", packageInfo.root);
182
216
  const softwareDistFolder = resolveSoftwareDst("dist", packageInfo.root);
183
217
  const assetDistFolder = resolveAssetsDst("dist", packageInfo.root);
218
+ const wasmDistFolder = resolveWasmDst("dist", packageInfo.root);
184
219
 
185
220
  const libDistExists = pathType(libDistFolder) === "dir";
186
221
  const tplDistExists = pathType(tplDistFolder) === "dir";
187
222
  const softwareDistExists = pathType(softwareDistFolder) === "dir";
188
223
  const assetDistExists = pathType(assetDistFolder) === "dir";
189
-
190
- if (!libDistExists && !tplDistExists && !softwareDistExists && !assetDistExists)
224
+ const wasmDistExists = pathType(wasmDistFolder) === "dir";
225
+ const wasmExports = collectWasmExportPaths(packageInfo.root);
226
+
227
+ if (
228
+ !libDistExists &&
229
+ !tplDistExists &&
230
+ !softwareDistExists &&
231
+ !assetDistExists &&
232
+ !wasmDistExists &&
233
+ wasmExports.length === 0
234
+ )
191
235
  // if neither of tengo-specific folders detected, skipping package
192
236
  return;
193
237
 
@@ -208,6 +252,14 @@ function loadDependencies(
208
252
  if (assetDistExists) {
209
253
  loadAssetsFromDir(logger, packageId, "dist", assetDistFolder, compiler);
210
254
  }
255
+
256
+ if (wasmDistExists) {
257
+ loadWasmFromDir(logger, packageId, "dist", wasmDistFolder, compiler);
258
+ }
259
+
260
+ for (const wasmExport of wasmExports) {
261
+ loadWasmFromExport(logger, packageId, "dist", packageInfo.root, wasmExport, compiler);
262
+ }
211
263
  }
212
264
 
213
265
  function loadLibsFromDir(
@@ -313,6 +365,142 @@ function loadAssetsFromDir(
313
365
  }
314
366
  }
315
367
 
368
+ function loadWasmFromDir(
369
+ logger: winston.Logger,
370
+ packageId: PackageId,
371
+ mode: CompileMode,
372
+ folder: string,
373
+ compiler: TengoTemplateCompiler,
374
+ ) {
375
+ for (const f of fs.readdirSync(folder)) {
376
+ const file = path.resolve(folder, f);
377
+ if (!f.endsWith(compiledWasmSuffix))
378
+ throw new Error(`unexpected file in 'wasm' folder: ${file}`);
379
+ const fullName: FullArtifactName = {
380
+ type: "wasm",
381
+ pkg: packageId.name,
382
+ id: f.slice(0, f.length - compiledWasmSuffix.length),
383
+ version: packageId.version,
384
+ };
385
+
386
+ addWasmFile(logger, mode, file, fullName, compiler);
387
+ }
388
+ }
389
+
390
+ interface WasmExport {
391
+ id: string;
392
+ /** wasm file path, relative to the package root */
393
+ relPath: string;
394
+ }
395
+
396
+ function loadWasmFromExport(
397
+ logger: winston.Logger,
398
+ packageId: PackageId,
399
+ mode: CompileMode,
400
+ packageRoot: string,
401
+ wasmExport: WasmExport,
402
+ compiler: TengoTemplateCompiler,
403
+ ) {
404
+ const file = path.resolve(packageRoot, wasmExport.relPath);
405
+ if (pathType(file) !== "file") {
406
+ throw new Error(
407
+ `wasm export "${wasmExport.id}" points to missing file: ${file} (package ${packageId.name})`,
408
+ );
409
+ }
410
+ const fullName: FullArtifactName = {
411
+ type: "wasm",
412
+ pkg: packageId.name,
413
+ id: wasmExport.id,
414
+ version: packageId.version,
415
+ };
416
+
417
+ addWasmFile(logger, mode, file, fullName, compiler);
418
+ }
419
+
420
+ function addWasmFile(
421
+ logger: winston.Logger,
422
+ mode: CompileMode,
423
+ file: string,
424
+ fullName: FullArtifactName,
425
+ compiler: TengoTemplateCompiler,
426
+ ) {
427
+ // wasm bytes are binary, but hashToSource carries strings only. We base64-encode
428
+ // the bytes; the sourceHash is sha256 of the base64 string so it stays consistent
429
+ // with the existing `hashToSource[hash] === stored value` invariant.
430
+ const bytes = fs.readFileSync(file);
431
+ assertWasmFileSize(file, bytes.length, fullName);
432
+ const source = bytes.toString("base64");
433
+ const wasm = new ArtifactSource(mode, fullName, getSha256(source), source, file, [], []);
434
+
435
+ logger.debug(`Adding dependency ${fullNameToString(fullName)} from ${file}`);
436
+ compiler.addWasm(wasm);
437
+ }
438
+
439
+ /**
440
+ * Discover wasm artifacts declared by a package's `package.json` via the
441
+ * node `exports` conditional-exports map.
442
+ *
443
+ * Convention: a package that wants to ship one or more wasm components to
444
+ * tengo-builder publishes them under a `"wasm"` condition keyed by an
445
+ * `exports` subpath. The subpath becomes the wasm artifact id (`.` →
446
+ * `main`, `./foo` → `foo`). Example shape we look for:
447
+ *
448
+ * {
449
+ * "exports": {
450
+ * ".": {
451
+ * "wasm": "./dist/pframes_rs_wasip2.wasm", // ← discovered
452
+ * "wit": "./wit/world.wit"
453
+ * },
454
+ * "./extra": {
455
+ * "wasm": "./dist/extra.wasm" // ← discovered as id "extra"
456
+ * }
457
+ * }
458
+ * }
459
+ *
460
+ * Why raw traversal instead of Zod / a typed schema:
461
+ * 1. `package.json#exports` is a community convention with no fixed shape
462
+ * — any combination of strings, objects, arrays, and conditions is
463
+ * legal node syntax. A strict schema would either reject valid
464
+ * packages or have to admit `unknown` for half of it anyway.
465
+ * 2. We care about exactly one condition (`wasm`) on subpath entries,
466
+ * not the rest of the exports tree. A targeted walk reads more
467
+ * directly than a schema that re-describes the conditional-exports
468
+ * grammar.
469
+ * 3. Errors here should always mean "this package contributes no wasm";
470
+ * never crash the build. Every check returns `[]` or `continue`s.
471
+ *
472
+ * Returns the discovered wasm exports; empty when the file is missing,
473
+ * malformed, has no `exports`, or has no subpath with a `"wasm"`
474
+ * condition pointing at a string path.
475
+ */
476
+ function collectWasmExportPaths(packageRoot: string): WasmExport[] {
477
+ const packageJsonPath = path.join(packageRoot, "package.json");
478
+ if (pathType(packageJsonPath) !== "file") return [];
479
+
480
+ let raw: unknown;
481
+ try {
482
+ raw = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
483
+ } catch {
484
+ return [];
485
+ }
486
+ if (typeof raw !== "object" || raw === null) return [];
487
+
488
+ const exportsField = (raw as { exports?: unknown }).exports;
489
+ if (typeof exportsField !== "object" || exportsField === null) return [];
490
+
491
+ const result: WasmExport[] = [];
492
+ for (const [subpath, value] of Object.entries(exportsField as Record<string, unknown>)) {
493
+ if (!subpath.startsWith(".")) continue; // skip condition-only entries
494
+ if (typeof value !== "object" || value === null) continue;
495
+ const wasmPath = (value as Record<string, unknown>)[wasmExportCondition];
496
+ if (typeof wasmPath !== "string") continue;
497
+ const id = subpath === "." ? defaultWasmExportId : subpath.replace(/^\.\/?/, "");
498
+ if (!id) continue;
499
+ result.push({ id, relPath: wasmPath });
500
+ }
501
+ return result;
502
+ }
503
+
316
504
  export function parseSources(
317
505
  logger: winston.Logger,
318
506
  packageId: PackageId,
@@ -468,8 +656,14 @@ export function savePacks(logger: winston.Logger, compiled: TemplatesAndLibs, mo
468
656
  fs.mkdirSync(tplOutput, { recursive: true });
469
657
  for (const tpl of compiled.templates) {
470
658
  const file = path.resolve(tplOutput, tpl.fullName.id + compiledTplSuffix);
659
+ const packed = serializeTemplate(tpl.data);
660
+ assertTemplatePackSize(
661
+ fullNameToString(tpl.fullName),
662
+ packed.byteLength,
663
+ collectWasmContributions(tpl.data),
664
+ );
471
665
  logger.info(` - writing ${file}`);
472
- fs.writeFileSync(file, serializeTemplate(tpl.data));
666
+ fs.writeFileSync(file, packed);
473
667
  }
474
668
  }
475
669
 
@@ -494,4 +688,16 @@ export function savePacks(logger: winston.Logger, compiled: TemplatesAndLibs, mo
494
688
  fs.writeFileSync(file, sw.src);
495
689
  }
496
690
  }
691
+
692
+ // writing wasm
693
+ if (compiled.wasm.length > 0) {
694
+ const wasmOutput = resolveWasmDst(mode, ".");
695
+ fs.mkdirSync(wasmOutput, { recursive: true });
696
+ for (const wasm of compiled.wasm) {
697
+ const file = path.resolve(wasmOutput, wasm.fullName.id + compiledWasmSuffix);
698
+ logger.info(` - writing ${file}`);
699
+ // wasm.src holds base64-encoded bytes; decode back to raw bytes on disk
700
+ fs.writeFileSync(file, Buffer.from(wasm.src, "base64"));
701
+ }
702
+ }
497
703
  }