@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
@@ -0,0 +1,173 @@
1
+ import type { CompiledTemplateV3 } from "@milaboratories/pl-model-backend";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import type { FullArtifactName } from "./package";
5
+ import {
6
+ MAX_TEMPLATE_PACK_BYTES_GZIPPED,
7
+ MAX_WASM_FILE_BYTES,
8
+ assertTemplatePackSize,
9
+ assertWasmFileSize,
10
+ collectWasmContributions,
11
+ } from "./pack_limits";
12
+
13
+ const wasmName: FullArtifactName = {
14
+ type: "wasm",
15
+ pkg: "@test/pkg",
16
+ id: "main",
17
+ version: "1.0.0",
18
+ };
19
+
20
+ describe("assertWasmFileSize", () => {
21
+ it("passes at exactly the cap", () => {
22
+ expect(() => assertWasmFileSize("/p/a.wasm", MAX_WASM_FILE_BYTES, wasmName)).not.toThrow();
23
+ });
24
+
25
+ it("throws above the cap with file path, full name, and shrink hint", () => {
26
+ let err: unknown;
27
+ try {
28
+ assertWasmFileSize("/p/a.wasm", MAX_WASM_FILE_BYTES + 1, wasmName);
29
+ } catch (e) {
30
+ err = e;
31
+ }
32
+ expect(err).toBeInstanceOf(Error);
33
+ const msg = (err as Error).message;
34
+ expect(msg).toContain("/p/a.wasm");
35
+ expect(msg).toContain("@test/pkg:main");
36
+ expect(msg).toContain("wasm-opt -Oz");
37
+ // points the reader at the backend source-of-truth
38
+ expect(msg).toContain("assertions.go");
39
+ });
40
+ });
41
+
42
+ describe("assertTemplatePackSize", () => {
43
+ it("passes at exactly the cap", () => {
44
+ expect(() =>
45
+ assertTemplatePackSize("@t/pkg:main 1.0.0", MAX_TEMPLATE_PACK_BYTES_GZIPPED, []),
46
+ ).not.toThrow();
47
+ });
48
+
49
+ it("lists WASM contributions sorted by size (largest first)", () => {
50
+ let err: unknown;
51
+ try {
52
+ assertTemplatePackSize("@t/pkg:main 1.0.0", MAX_TEMPLATE_PACK_BYTES_GZIPPED + 1, [
53
+ { name: "@small/wasm:main", rawBytes: 100 * 1024 },
54
+ { name: "@big/wasm:main", rawBytes: 1.5 * 1024 * 1024 },
55
+ { name: "@mid/wasm:main", rawBytes: 800 * 1024 },
56
+ ]);
57
+ } catch (e) {
58
+ err = e;
59
+ }
60
+ expect(err).toBeInstanceOf(Error);
61
+ const msg = (err as Error).message;
62
+ const big = msg.indexOf("@big/wasm:main");
63
+ const mid = msg.indexOf("@mid/wasm:main");
64
+ const small = msg.indexOf("@small/wasm:main");
65
+ // all three present
66
+ expect(big).toBeGreaterThan(-1);
67
+ expect(mid).toBeGreaterThan(-1);
68
+ expect(small).toBeGreaterThan(-1);
69
+ // ordered big > mid > small
70
+ expect(big).toBeLessThan(mid);
71
+ expect(mid).toBeLessThan(small);
72
+ expect(msg).toContain("@t/pkg:main 1.0.0");
73
+ expect(msg).toContain("config.go");
74
+ });
75
+
76
+ it("includes a fallback hint when no WASM artefacts contributed", () => {
77
+ let err: unknown;
78
+ try {
79
+ assertTemplatePackSize("@t/pkg:main 1.0.0", MAX_TEMPLATE_PACK_BYTES_GZIPPED + 1, []);
80
+ } catch (e) {
81
+ err = e;
82
+ }
83
+ expect((err as Error).message).toContain("no WASM artefacts");
84
+ });
85
+ });
86
+
87
+ describe("collectWasmContributions", () => {
88
+ // base64 length L → ~L * 3 / 4 raw bytes. Build a base64 string of the
89
+ // requested length so the test is independent of any specific WASM payload.
90
+ function base64OfLength(len: number): string {
91
+ return "A".repeat(len);
92
+ }
93
+
94
+ it("reports direct WASM dependencies with raw byte size derived from base64 length", () => {
95
+ const base64 = base64OfLength(400); // 400 base64 chars → 300 raw bytes
96
+ const tpl: CompiledTemplateV3 = {
97
+ type: "pl.tengo-template.v3",
98
+ hashToSource: { "hash-a": base64 },
99
+ template: {
100
+ name: "@t/pkg:main",
101
+ version: "1.0.0",
102
+ sourceHash: "tpl-hash",
103
+ libs: {},
104
+ templates: {},
105
+ software: {},
106
+ assets: {},
107
+ wasm: {
108
+ "@w/wasm:a": { name: "@w/wasm:a", version: "1.0.0", sourceHash: "hash-a" },
109
+ },
110
+ },
111
+ };
112
+ const got = collectWasmContributions(tpl);
113
+ expect(got).toEqual([{ name: "@w/wasm:a", rawBytes: 300 }]);
114
+ });
115
+
116
+ it("recurses into sub-templates and deduplicates by artefact name", () => {
117
+ const base64a = base64OfLength(800); // 600 raw
118
+ const base64b = base64OfLength(400); // 300 raw
119
+ const tpl: CompiledTemplateV3 = {
120
+ type: "pl.tengo-template.v3",
121
+ hashToSource: { "hash-a": base64a, "hash-b": base64b },
122
+ template: {
123
+ name: "@t/pkg:root",
124
+ version: "1.0.0",
125
+ sourceHash: "root-hash",
126
+ libs: {},
127
+ software: {},
128
+ assets: {},
129
+ wasm: {
130
+ "@w/wasm:a": { name: "@w/wasm:a", version: "1.0.0", sourceHash: "hash-a" },
131
+ },
132
+ templates: {
133
+ "@t/pkg:child": {
134
+ name: "@t/pkg:child",
135
+ version: "1.0.0",
136
+ sourceHash: "child-hash",
137
+ libs: {},
138
+ templates: {},
139
+ software: {},
140
+ assets: {},
141
+ wasm: {
142
+ // duplicate of the parent's WASM — must not be counted twice
143
+ "@w/wasm:a": { name: "@w/wasm:a", version: "1.0.0", sourceHash: "hash-a" },
144
+ "@w/wasm:b": { name: "@w/wasm:b", version: "1.0.0", sourceHash: "hash-b" },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ };
150
+ const got = collectWasmContributions(tpl).sort((a, b) => a.name.localeCompare(b.name));
151
+ expect(got).toEqual([
152
+ { name: "@w/wasm:a", rawBytes: 600 },
153
+ { name: "@w/wasm:b", rawBytes: 300 },
154
+ ]);
155
+ });
156
+
157
+ it("returns an empty list when the template tree contains no WASM", () => {
158
+ const tpl: CompiledTemplateV3 = {
159
+ type: "pl.tengo-template.v3",
160
+ hashToSource: {},
161
+ template: {
162
+ name: "@t/pkg:main",
163
+ version: "1.0.0",
164
+ sourceHash: "tpl-hash",
165
+ libs: {},
166
+ templates: {},
167
+ software: {},
168
+ assets: {},
169
+ },
170
+ };
171
+ expect(collectWasmContributions(tpl)).toEqual([]);
172
+ });
173
+ });
@@ -0,0 +1,103 @@
1
+ import type { CompiledTemplateV3, TemplateDataV3 } from "@milaboratories/pl-model-backend";
2
+
3
+ import type { FullArtifactName } from "./package";
4
+ import { fullNameToString } from "./package";
5
+
6
+ // Build-time size guards mirroring the two backend caps that gate template
7
+ // ingestion. Hardcoded because tengo-builder runs at block build time and has
8
+ // no connection to a running backend; if pl ever changes the caps, update the
9
+ // constants here in lockstep with the source-of-truth lines pointed at below.
10
+ //
11
+ // Source of truth (`core/pl` repo):
12
+ // - platform/core/transaction/assertions.go : `maxResourceDataSize`
13
+ // 3 MiB per value resource, checked on every CreateValue marshal.
14
+ // Hits the WasmV1 resource after the controller unpacks a template pack
15
+ // into its constituent libs/software/wasm resources.
16
+ // - controllers/workflow/pkg/cfg/config.go : `TemplatePackSizeLimit`
17
+ // 3.5 MiB per gzipped template pack, checked before the pack itself is
18
+ // stored as a TengoTemplatePackV1 value resource.
19
+
20
+ // 2 MiB raw → ~2.67 MiB base64 + JSON wrapper → comfortably under the 3 MiB
21
+ // per-value-resource cap with ~330 KiB headroom for the WasmV1Data wrapper
22
+ // (Name, Version, Runtime, DefaultMemoryLimit, JSON syntax). WasmV1Data.Code
23
+ // is a Go `[]byte`, which encoding/json serialises as base64 — that's where
24
+ // the 4/3 expansion comes from.
25
+ export const MAX_WASM_FILE_BYTES = 2 * 1024 * 1024;
26
+
27
+ // 100 KiB below the backend's 3.5 MiB so blocks don't break on small
28
+ // platform-side metadata overhead, or if pl tightens the limit slightly.
29
+ export const MAX_TEMPLATE_PACK_BYTES_GZIPPED = 3.5 * 1024 * 1024 - 100 * 1024;
30
+
31
+ function formatBytes(n: number): string {
32
+ if (n < 1024) return `${n} B`;
33
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
34
+ return `${(n / 1024 / 1024).toFixed(2)} MiB`;
35
+ }
36
+
37
+ export function assertWasmFileSize(
38
+ file: string,
39
+ byteLength: number,
40
+ fullName: FullArtifactName,
41
+ ): void {
42
+ if (byteLength <= MAX_WASM_FILE_BYTES) return;
43
+ throw new Error(
44
+ `WASM artefact ${fullNameToString(fullName)} from ${file} is ${formatBytes(byteLength)}, ` +
45
+ `which exceeds tengo-builder's ${formatBytes(MAX_WASM_FILE_BYTES)} per-WASM cap. ` +
46
+ `The backend stores each WASM as a single value resource (3 MiB hard cap after base64+JSON ` +
47
+ `marshal — see maxResourceDataSize in pl: platform/core/transaction/assertions.go). ` +
48
+ `Strip debug info or run \`wasm-opt -Oz\` to shrink it.`,
49
+ );
50
+ }
51
+
52
+ export interface WasmContribution {
53
+ /** Artefact name as stored under TemplateDataV3.wasm (e.g. "@pkg:id"). */
54
+ readonly name: string;
55
+ /** Raw WASM bytes — decoded from the base64-encoded source in hashToSource. */
56
+ readonly rawBytes: number;
57
+ }
58
+
59
+ /**
60
+ * Walks a compiled template tree and reports every WASM artefact embedded
61
+ * directly or transitively. Used only by the too-large-pack error message —
62
+ * never on the success path. Deduplicates by artefact name (sub-templates can
63
+ * import the same WASM via different aliases and we want to attribute weight
64
+ * only once).
65
+ */
66
+ export function collectWasmContributions(tpl: CompiledTemplateV3): WasmContribution[] {
67
+ const seen = new Map<string, WasmContribution>();
68
+ function walk(t: TemplateDataV3) {
69
+ for (const [name, ref] of Object.entries(t.wasm ?? {})) {
70
+ if (seen.has(name)) continue;
71
+ const base64 = tpl.hashToSource[ref.sourceHash];
72
+ // base64 encodes 3 raw bytes per 4 chars; the off-by-padding is at most
73
+ // 2 bytes per artefact — fine for a human-readable size breakdown.
74
+ const rawBytes = base64 != null ? Math.floor((base64.length * 3) / 4) : 0;
75
+ seen.set(name, { name, rawBytes });
76
+ }
77
+ for (const sub of Object.values(t.templates ?? {})) walk(sub);
78
+ }
79
+ walk(tpl.template);
80
+ return [...seen.values()];
81
+ }
82
+
83
+ export function assertTemplatePackSize(
84
+ templateName: string,
85
+ gzippedByteLength: number,
86
+ wasmContributions: readonly WasmContribution[],
87
+ ): void {
88
+ if (gzippedByteLength <= MAX_TEMPLATE_PACK_BYTES_GZIPPED) return;
89
+ const ranked = [...wasmContributions].sort((a, b) => b.rawBytes - a.rawBytes);
90
+ const breakdown =
91
+ ranked.length === 0
92
+ ? "\n (no WASM artefacts in this pack — heavy libs, software descriptors, or sub-templates are the likely cause.)"
93
+ : "\nWASM artefacts in this pack (raw bytes):\n" +
94
+ ranked.map((w) => ` - ${w.name} — ${formatBytes(w.rawBytes)}`).join("\n");
95
+ throw new Error(
96
+ `Template pack ${templateName} is ${formatBytes(gzippedByteLength)} gzipped, ` +
97
+ `which exceeds tengo-builder's ${formatBytes(MAX_TEMPLATE_PACK_BYTES_GZIPPED)} per-pack cap. ` +
98
+ `The backend caps gzipped packs at 3.5 MiB (see TemplatePackSizeLimit in pl: ` +
99
+ `controllers/workflow/pkg/cfg/config.go).` +
100
+ breakdown +
101
+ "\nShrink WASM artefacts (wasm-opt -Oz), or split this template into smaller pieces.",
102
+ );
103
+ }
@@ -27,7 +27,7 @@ export type CompilerOption = {
27
27
  args: string[];
28
28
  };
29
29
 
30
- export type ArtifactType = "library" | "template" | "test" | "software" | "asset";
30
+ export type ArtifactType = "library" | "template" | "test" | "software" | "asset" | "wasm";
31
31
 
32
32
  /** Artifact Name including package version */
33
33
  export interface FullArtifactName {
@@ -126,6 +126,36 @@ describe("import statements", () => {
126
126
  const trickyCasesSrc = parseSource(logger, "dist", testTrickyCasesSrc, stubTplName, true);
127
127
  expect(trickyCasesSrc.src).toEqual(testTrickyCasesNormalized);
128
128
  });
129
+
130
+ test("assets.importWasm produces a wasm dependency", () => {
131
+ const logger = createLogger("error");
132
+
133
+ const wasmImport = `
134
+ assets := import("@platforma-sdk/workflow-tengo:assets")
135
+ wasm := assets.importWasm("@milaboratories/pframes-rs-wasip2:main")
136
+ `;
137
+
138
+ const parsed = parseSource(logger, "dist", wasmImport, stubTplName, true);
139
+ expect(parsed.dependencies).toContainEqual({
140
+ type: "wasm",
141
+ pkg: "@milaboratories/pframes-rs-wasip2",
142
+ id: "main",
143
+ });
144
+ });
145
+
146
+ test("assets.importWasm with variable id is rejected", () => {
147
+ const logger = createLogger("info");
148
+
149
+ const wasmByVar = `
150
+ assets := import("@platforma-sdk/workflow-tengo:assets")
151
+ wasmId := "@milaboratories/pframes-rs-wasip2:main"
152
+ wasm := assets.importWasm(wasmId)
153
+ `;
154
+
155
+ expect(() => parseSource(logger, "dist", wasmByVar, stubTplName, true)).toThrow(
156
+ "variables are not allowed",
157
+ );
158
+ });
129
159
  });
130
160
 
131
161
  describe("parseSingleSourceLine", () => {
@@ -42,6 +42,8 @@ const newImportSoftwareDetector = (moduleName: string) =>
42
42
  const newImportAssetRE = (moduleName: string) => functionCallRE(moduleName, "importAsset");
43
43
  const newImportAssetDetector = (moduleName: string) =>
44
44
  functionCallLikeRE(moduleName, "importAsset");
45
+ const newImportWasmRE = (moduleName: string) => functionCallRE(moduleName, "importWasm");
46
+ const newImportWasmDetector = (moduleName: string) => functionCallLikeRE(moduleName, "importWasm");
45
47
 
46
48
  const emptyLineRE = /^\s*$/;
47
49
  const compilerOptionRE = /^\/\/tengo:[\w]/;
@@ -369,11 +371,13 @@ function processModuleImport(
369
371
  ["template", newImportTemplateRE(iInfo.alias)],
370
372
  ["software", newImportSoftwareRE(iInfo.alias)],
371
373
  ["asset", newImportAssetRE(iInfo.alias)],
374
+ ["wasm", newImportWasmRE(iInfo.alias)],
372
375
  ]);
373
376
  context.importLikeREs.set(iInfo.module, [
374
377
  ["template", newImportTemplateDetector(iInfo.alias)],
375
378
  ["software", newImportSoftwareDetector(iInfo.alias)],
376
379
  ["asset", newImportAssetDetector(iInfo.alias)],
380
+ ["wasm", newImportWasmDetector(iInfo.alias)],
377
381
  ]);
378
382
  }
379
383
  }
@@ -31,7 +31,7 @@ export const ArtifactTypeFlag = {
31
31
  type: Flags.string({
32
32
  aliases: ["t"],
33
33
  description: "artifact type",
34
- options: ["all", "library", "template", "test", "software", "asset"],
34
+ options: ["all", "library", "template", "test", "software", "asset", "wasm"],
35
35
  default: "all",
36
36
  }),
37
37
  };
@@ -88,6 +88,15 @@ export function dumpArtifacts(
88
88
  }
89
89
  }
90
90
 
91
+ // Wasm
92
+
93
+ if (!aType || aType === "wasm") {
94
+ for (const wasm of compiler.allWasm()) {
95
+ logger.debug(`Dumping to pl-tester: ${typedArtifactNameToString(wasm.fullName)}`);
96
+ stream.write(JSON.stringify(wasm) + "\n");
97
+ }
98
+ }
99
+
91
100
  // Tests
92
101
 
93
102
  if (!aType || aType === "test") {