@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.
- package/dist/compiler/compiler.cjs +46 -1
- package/dist/compiler/compiler.cjs.map +1 -1
- package/dist/compiler/compiler.js +46 -1
- package/dist/compiler/compiler.js.map +1 -1
- package/dist/compiler/main.cjs +132 -6
- package/dist/compiler/main.cjs.map +1 -1
- package/dist/compiler/main.js +132 -6
- package/dist/compiler/main.js.map +1 -1
- package/dist/compiler/pack_limits.cjs +49 -0
- package/dist/compiler/pack_limits.cjs.map +1 -0
- package/dist/compiler/pack_limits.js +47 -0
- package/dist/compiler/pack_limits.js.map +1 -0
- package/dist/compiler/package.cjs.map +1 -1
- package/dist/compiler/package.js.map +1 -1
- package/dist/compiler/source.cjs +6 -2
- package/dist/compiler/source.cjs.map +1 -1
- package/dist/compiler/source.js +6 -2
- package/dist/compiler/source.js.map +1 -1
- package/dist/shared/basecmd.cjs +2 -1
- package/dist/shared/basecmd.cjs.map +1 -1
- package/dist/shared/basecmd.js +2 -1
- package/dist/shared/basecmd.js.map +1 -1
- package/dist/shared/dump.cjs +4 -0
- package/dist/shared/dump.cjs.map +1 -1
- package/dist/shared/dump.js +4 -0
- package/dist/shared/dump.js.map +1 -1
- package/package.json +6 -6
- package/src/compiler/compiler.test.ts +91 -0
- package/src/compiler/compiler.ts +75 -1
- package/src/compiler/main.ts +217 -11
- package/src/compiler/pack_limits.test.ts +173 -0
- package/src/compiler/pack_limits.ts +103 -0
- package/src/compiler/package.ts +1 -1
- package/src/compiler/source.test.ts +30 -0
- package/src/compiler/source.ts +4 -0
- package/src/shared/basecmd.ts +1 -1
- 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
|
+
});
|
package/src/compiler/compiler.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
package/src/compiler/main.ts
CHANGED
|
@@ -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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
}
|