@pulse-compute/wasm-build-support 0.0.0 → 1.0.0-beta.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/README.md CHANGED
@@ -1,3 +1,28 @@
1
1
  # @pulse-compute/wasm-build-support
2
2
 
3
- This is an inert namespace-bootstrap package. It contains no executable code.
3
+ <!-- pulse-package-status:start -->
4
+ > **Support tier:** Implementation/transitive surface<br>
5
+ > **Audience:** Pulse release-set and compiler maintainers.<br>
6
+ > **Install directly:** No for application projects; it is installed transitively where required.<br>
7
+ > **Supported entry points:** `None for application authors.`<br>
8
+ > **Stability:** Internal release-set interface; exported modules may change with compiler implementation needs.<br>
9
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.1/packages/implementation-packages/)
10
+ >
11
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.1` package policy.
12
+ <!-- pulse-package-status:end -->
13
+
14
+ Shared build-time helpers for PulseWasm package-owned builders.
15
+
16
+ This package is intentionally build/tooling scoped. It owns utilities that future package-owned compiler surfaces need without importing `@pulse-compute/wasm-compiler` internals.
17
+
18
+ ## Exposed modules
19
+
20
+ - `artifactsDir` → canonical `wasm/artifacts` directory resolution helpers.
21
+ - `assemblyscriptCompile` → AssemblyScript compiler resolution and compile-smoke helper.
22
+ - `files` → shared slash normalization, SHA-256, truncation, ANSI stripping, and file-record helpers.
23
+
24
+ ## Boundary rules
25
+
26
+ - May depend on `@pulse-compute/wasm-contracts` for diagnostics/artifact normalization.
27
+ - Must not depend on `@pulse-compute/wasm-compiler`.
28
+ - Must not own the `assemblyscript` dependency; AS is resolved from the caller workspace/package.
package/package.json CHANGED
@@ -1,15 +1,39 @@
1
1
  {
2
2
  "name": "@pulse-compute/wasm-build-support",
3
- "version": "0.0.0",
4
- "description": "Inert namespace bootstrap for Pulse.",
3
+ "version": "1.0.0-beta.1",
5
4
  "license": "Apache-2.0",
5
+ "engines": {
6
+ "node": "^22.14.0 || ^24.0.0"
7
+ },
8
+ "description": "Shared build-time helpers for PulseWasm package-owned builders.",
9
+ "type": "commonjs",
10
+ "main": "./src/index.js",
11
+ "exports": {
12
+ ".": "./src/index.js",
13
+ "./artifacts-dir": "./src/artifacts-dir.js",
14
+ "./assemblyscript-compile": "./src/assemblyscript-compile.js",
15
+ "./native-optimization": "./src/native-optimization.js",
16
+ "./files": "./src/files.js"
17
+ },
18
+ "dependencies": {
19
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.1"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
6
24
  "files": [
25
+ "src",
7
26
  "README.md",
8
27
  "LICENSE",
9
28
  "NOTICE"
10
29
  ],
11
- "publishConfig": {
12
- "access": "public",
13
- "tag": "bootstrap"
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/pulse-compute/pulse.git",
33
+ "directory": "wasm/packages/build-support"
34
+ },
35
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.1/packages/implementation-packages/",
36
+ "bugs": {
37
+ "url": "https://github.com/pulse-compute/pulse/issues"
14
38
  }
15
- }
39
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ function getBuildSupportPackageRoot() {
7
+ return path.resolve(__dirname, '..');
8
+ }
9
+
10
+ function getWasmRoot() {
11
+ const packageRoot = getBuildSupportPackageRoot();
12
+ const candidate = path.resolve(packageRoot, '..', '..');
13
+ const manifestPath = path.join(candidate, 'package.json');
14
+ if (path.basename(path.dirname(packageRoot)) === 'packages' && fs.existsSync(manifestPath)) {
15
+ return candidate;
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ function getDefaultArtifactsDir(cwd = process.cwd()) {
21
+ if (process.env.PULSEWASM_ARTIFACTS_DIR) {
22
+ return path.resolve(cwd, process.env.PULSEWASM_ARTIFACTS_DIR);
23
+ }
24
+ const wasmRoot = getWasmRoot();
25
+ if (wasmRoot) {
26
+ return path.join(wasmRoot, 'artifacts');
27
+ }
28
+ return path.join(cwd, 'artifacts');
29
+ }
30
+
31
+ function resolveArtifactsDir(value, cwd = process.cwd()) {
32
+ if (!value) return getDefaultArtifactsDir(cwd);
33
+ return path.resolve(cwd, value);
34
+ }
35
+
36
+ module.exports = {
37
+ getBuildSupportPackageRoot,
38
+ getWasmRoot,
39
+ getDefaultArtifactsDir,
40
+ resolveArtifactsDir
41
+ };
@@ -0,0 +1,279 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { getDefaultArtifactsDir } = require('./artifacts-dir.js');
7
+ const { spawnSync } = require('node:child_process');
8
+
9
+ function loadContractsDiagnostics() {
10
+ try {
11
+ return require('@pulse-compute/wasm-contracts/diagnostics');
12
+ } catch (error) {
13
+ if (error && error.code === 'MODULE_NOT_FOUND' && String(error.message).includes('@pulse-compute/wasm-contracts')) {
14
+ return require('../../contracts/src/diagnostics.js');
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ const { PACKAGE_VERSION, normalizeArtifact } = loadContractsDiagnostics();
21
+
22
+ const ASSEMBLYSCRIPT_COMPILE_VERSION = 'pulsewasm.assemblyscript-compile.v1';
23
+
24
+ const {
25
+ fileRecord,
26
+ sha256Text,
27
+ stripAnsi,
28
+ truncate
29
+ } = require('./files.js');
30
+
31
+ function parseAscWarnings(stderr) {
32
+ const text = stripAnsi(stderr);
33
+ const warnings = [];
34
+ const regex = /WARNING\s+(AS\d+):\s+([^\n]+)(?:.|\n)*?in ([^\n]+)\((\d+),(\d+)\)/g;
35
+ let match;
36
+ while ((match = regex.exec(text))) {
37
+ warnings.push({
38
+ code: match[1],
39
+ message: match[2].trim(),
40
+ file: match[3],
41
+ line: Number(match[4]),
42
+ column: Number(match[5])
43
+ });
44
+ }
45
+ if (warnings.length === 0 && /WARNING\s+AS\d+:/m.test(text)) {
46
+ warnings.push({
47
+ code: 'AS_WARNING',
48
+ message: 'AssemblyScript emitted one or more warnings. Inspect result.stderr for details.'
49
+ });
50
+ }
51
+ return warnings;
52
+ }
53
+
54
+ function resolveAssemblyScriptPackageRoot(cwd) {
55
+ try {
56
+ const packageJson = require.resolve('assemblyscript/package.json', { paths: [cwd] });
57
+ return path.dirname(packageJson);
58
+ } catch (_) {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ function resolveAsc(cwd) {
64
+ const packageRoot = resolveAssemblyScriptPackageRoot(cwd);
65
+ if (!packageRoot) return undefined;
66
+ const ascJs = path.join(packageRoot, 'bin', 'asc.js');
67
+ if (!fs.existsSync(ascJs)) return undefined;
68
+ return {
69
+ packageRoot,
70
+ executable: process.execPath,
71
+ script: ascJs,
72
+ display: `node ${path.relative(cwd, ascJs).replace(/\\/g, '/')}`
73
+ };
74
+ }
75
+
76
+ function assemblyScriptVersion(cwd) {
77
+ const packageRoot = resolveAssemblyScriptPackageRoot(cwd);
78
+ if (!packageRoot) return undefined;
79
+ try {
80
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
81
+ return pkg.version;
82
+ } catch (_) {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ function writeStagedFiles(stagingDir, shape, core) {
88
+ const allFiles = [];
89
+ if (shape && Array.isArray(shape.files)) allFiles.push(...shape.files);
90
+ if (core && Array.isArray(core.files)) allFiles.push(...core.files);
91
+
92
+ // De-duplicate by output path, keeping later/core files when index.as.ts is upgraded by the core generator.
93
+ const byFile = new Map();
94
+ for (const file of allFiles) byFile.set(file.file, file.text);
95
+
96
+ for (const [file, text] of byFile.entries()) {
97
+ const abs = path.join(stagingDir, file);
98
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
99
+ fs.writeFileSync(abs, text, 'utf8');
100
+ }
101
+
102
+ return Array.from(byFile.entries()).map(([file, text]) => ({
103
+ file,
104
+ bytes: Buffer.byteLength(text, 'utf8'),
105
+ sha256: sha256Text(text)
106
+ }));
107
+ }
108
+
109
+ function runAscCompile(options) {
110
+ const cwd = options.cwd || process.cwd();
111
+ const outDir = options.outDir || getDefaultArtifactsDir(cwd);
112
+ const shape = options.assemblyScriptShape;
113
+ const core = options.assemblyScriptCore;
114
+ const asc = resolveAsc(cwd);
115
+
116
+ const diagnostics = [];
117
+ if (!asc) {
118
+ diagnostics.push({
119
+ pass: 'assemblyscript-compile',
120
+ code: 'PULSEWASM_ASSEMBLYSCRIPT_ASC_MISSING',
121
+ severity: 'error',
122
+ message: 'AssemblyScript compiler dependency was not found.',
123
+ hint: 'Run corepack pnpm install --frozen-lockfile so the assemblyscript devDependency is available, then rerun the compile smoke test.',
124
+ loc: { file: '<assemblyscript-compile>' }
125
+ });
126
+ return { artifact: buildArtifact({ cwd, outDir, status: 'error', diagnostics, shape, core }), diagnostics };
127
+ }
128
+
129
+ const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-as-compile-'));
130
+ const outputDir = path.join(outDir, 'generated', 'as-smoke');
131
+ fs.mkdirSync(outputDir, { recursive: true });
132
+
133
+ const stagedFiles = writeStagedFiles(stagingDir, shape, core);
134
+ const entry = path.join(stagingDir, 'generated/as/index.as.ts');
135
+ const wasmFile = path.join(outputDir, 'pulsewasm-smoke.wasm');
136
+ const watFile = path.join(outputDir, 'pulsewasm-smoke.wat');
137
+
138
+ const ascArgs = [
139
+ asc.script,
140
+ entry,
141
+ '--outFile', wasmFile,
142
+ '--textFile', watFile,
143
+ '--runtime', 'stub',
144
+ '--noAssert'
145
+ ];
146
+
147
+ const startedAt = Date.now();
148
+ const proc = spawnSync(asc.executable, ascArgs, {
149
+ cwd,
150
+ encoding: 'utf8',
151
+ maxBuffer: 1024 * 1024 * 12
152
+ });
153
+ const durationMs = Date.now() - startedAt;
154
+
155
+ const warnings = parseAscWarnings(proc.stderr);
156
+
157
+ if (proc.error) {
158
+ diagnostics.push({
159
+ pass: 'assemblyscript-compile',
160
+ code: 'PULSEWASM_ASSEMBLYSCRIPT_COMPILE_SPAWN_FAILED',
161
+ severity: 'error',
162
+ message: `Failed to start AssemblyScript compiler: ${proc.error.message}`,
163
+ hint: 'Verify Node can execute node_modules/assemblyscript/bin/asc.js.',
164
+ loc: { file: '<assemblyscript-compile>' }
165
+ });
166
+ } else if (proc.status !== 0) {
167
+ diagnostics.push({
168
+ pass: 'assemblyscript-compile',
169
+ code: 'PULSEWASM_ASSEMBLYSCRIPT_COMPILE_FAILED',
170
+ severity: 'error',
171
+ message: `AssemblyScript compile smoke test failed with exit code ${proc.status}.`,
172
+ hint: 'Inspect assemblyscript-compile.json stderr/stdout for asc diagnostics. Generated AS must compile before host binding work.',
173
+ loc: { file: '<assemblyscript-compile>' },
174
+ details: {
175
+ exitCode: proc.status,
176
+ stderr: truncate(proc.stderr, 8000),
177
+ stdout: truncate(proc.stdout, 8000)
178
+ }
179
+ });
180
+ }
181
+
182
+ const outputs = [];
183
+ if (fs.existsSync(wasmFile)) outputs.push(fileRecord(wasmFile, cwd, 'wasm-smoke-output'));
184
+ if (fs.existsSync(watFile)) outputs.push(fileRecord(watFile, cwd, 'wat-smoke-output'));
185
+
186
+ const artifact = buildArtifact({
187
+ cwd,
188
+ outDir,
189
+ status: diagnostics.length === 0 ? 'ok' : 'error',
190
+ diagnostics,
191
+ warnings,
192
+ shape,
193
+ core,
194
+ asc,
195
+ assemblyScriptVersion: assemblyScriptVersion(cwd),
196
+ stagedFiles,
197
+ outputs,
198
+ command: {
199
+ executable: asc.display,
200
+ args: ascArgs.slice(1).map((arg) => path.isAbsolute(arg) ? path.relative(cwd, arg).replace(/\\/g, '/') : arg)
201
+ },
202
+ result: {
203
+ warnings,
204
+ exitCode: proc.status,
205
+ signal: proc.signal,
206
+ durationMs,
207
+ stdout: truncate(proc.stdout),
208
+ stderr: truncate(proc.stderr)
209
+ }
210
+ });
211
+
212
+ return { artifact, diagnostics, outputFiles: outputs };
213
+ }
214
+
215
+ function buildArtifact(options = {}) {
216
+ const cwd = options.cwd || process.cwd();
217
+ return normalizeArtifact({
218
+ version: ASSEMBLYSCRIPT_COMPILE_VERSION,
219
+ generatedBy: options.generatedBy || PACKAGE_VERSION,
220
+ status: options.status || 'ok',
221
+ sourceAssemblyScriptShapeVersion: options.shape?.artifact?.version,
222
+ sourceAssemblyScriptCoreVersion: options.core?.artifact?.version,
223
+ sourceAssemblyScriptHandlersVersion: options.core?.handlersArtifact?.version,
224
+ policy: {
225
+ phase: '9E',
226
+ sourceOfTruth: 'generated/as/*.as.ts files emitted from validated artifacts',
227
+ reReadsSource: false,
228
+ reInfersRouteSemantics: false,
229
+ compiler: 'assemblyscript asc',
230
+ compilationOnly: true,
231
+ wasmExecution: false,
232
+ hostBindings: false,
233
+ witBindings: false,
234
+ purpose: 'prove generated AssemblyScript is syntactically and type valid before local Wasm execution work'
235
+ },
236
+ compiler: {
237
+ package: 'assemblyscript',
238
+ version: options.assemblyScriptVersion,
239
+ executable: options.asc?.display
240
+ },
241
+ command: options.command,
242
+ result: options.result,
243
+ generated: {
244
+ inputFiles: options.stagedFiles || [],
245
+ outputFiles: options.outputs || []
246
+ },
247
+ diagnostics: options.diagnostics || [],
248
+ warnings: options.warnings || [],
249
+ summary: {
250
+ compiled: options.status === 'ok',
251
+ inputFiles: (options.stagedFiles || []).length,
252
+ outputFiles: (options.outputs || []).length,
253
+ wasmBytes: (options.outputs || []).find((file) => file.kind === 'wasm-smoke-output')?.bytes || 0,
254
+ watBytes: (options.outputs || []).find((file) => file.kind === 'wat-smoke-output')?.bytes || 0,
255
+ diagnostics: (options.diagnostics || []).length,
256
+ warnings: (options.warnings || []).length,
257
+ wasmExecuted: false,
258
+ hostBindings: false
259
+ }
260
+ }, cwd);
261
+ }
262
+
263
+ function buildAssemblyScriptCompileSmoke(assemblyScriptShape, assemblyScriptCore, options = {}) {
264
+ return {
265
+ version: ASSEMBLYSCRIPT_COMPILE_VERSION,
266
+ ...runAscCompile({
267
+ cwd: options.cwd || process.cwd(),
268
+ outDir: options.outDir,
269
+ assemblyScriptShape,
270
+ assemblyScriptCore
271
+ })
272
+ };
273
+ }
274
+
275
+ module.exports = {
276
+ ASSEMBLYSCRIPT_COMPILE_VERSION,
277
+ buildAssemblyScriptCompileSmoke,
278
+ resolveAsc
279
+ };
package/src/files.js ADDED
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ function normalizeSlash(value) {
8
+ return String(value || '').replace(/\\/g, '/');
9
+ }
10
+
11
+ function sha256Buffer(buffer) {
12
+ return crypto.createHash('sha256').update(buffer).digest('hex');
13
+ }
14
+
15
+ function sha256Text(text) {
16
+ return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
17
+ }
18
+
19
+ function truncate(value, max = 20000) {
20
+ const text = String(value || '');
21
+ if (text.length <= max) return text;
22
+ return `${text.slice(0, max)}\n... <truncated ${text.length - max} chars>`;
23
+ }
24
+
25
+ function stripAnsi(value) {
26
+ return String(value || '').replace(/\u001b\[[0-9;]*m/g, '');
27
+ }
28
+
29
+ const TEXT_ARTIFACT_EXTENSIONS = new Set([
30
+ '.cjs', '.js', '.json', '.md', '.mjs', '.toml', '.ts', '.txt', '.wat', '.yaml', '.yml'
31
+ ]);
32
+
33
+ // AssemblyScript and several proof builders use mkdtemp staging directories.
34
+ // Those paths are operational details, not part of the generated program. Keep
35
+ // checked-in/compiler artifacts stable without weakening semantic hash checks.
36
+ function canonicalizeEphemeralPaths(value) {
37
+ const stagingNames = String(value || '').replace(
38
+ /pulsewasm-([A-Za-z0-9._-]+?)-[A-Za-z0-9]{6}(?=[^A-Za-z0-9]|$)/g,
39
+ 'pulsewasm-$1-STAGING'
40
+ );
41
+ // AssemblyScript writes source names relative to the output directory. That
42
+ // relative prefix changes when the repository is unpacked at a different
43
+ // filesystem depth even though the generated program is identical. Remove
44
+ // only the path leading to a known Pulse staging directory; retain the
45
+ // staging family and all source-relative suffixes for semantic hashing.
46
+ return stagingNames
47
+ .replace(/(?:\.\.\/)+tmp\/(pulsewasm-[A-Za-z0-9._-]+-STAGING)(?=\/)/g, 'tmp/$1')
48
+ .replace(/\/tmp\/(pulsewasm-[A-Za-z0-9._-]+-STAGING)(?=\/)/g, 'tmp/$1');
49
+ }
50
+
51
+ function isTextArtifact(filePath) {
52
+ return TEXT_ARTIFACT_EXTENSIONS.has(path.extname(String(filePath || '')).toLowerCase());
53
+ }
54
+
55
+ function canonicalizeArtifactBuffer(filePath, buffer) {
56
+ const source = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || '');
57
+ if (!isTextArtifact(filePath)) return source;
58
+ return Buffer.from(canonicalizeEphemeralPaths(source.toString('utf8')), 'utf8');
59
+ }
60
+
61
+ function normalizeArtifactFile(filePath) {
62
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false;
63
+ const before = fs.readFileSync(filePath);
64
+ const after = canonicalizeArtifactBuffer(filePath, before);
65
+ if (before.equals(after)) return false;
66
+ fs.writeFileSync(filePath, after);
67
+ return true;
68
+ }
69
+
70
+ function fileRecord(filePath, cwd, kind) {
71
+ const buffer = fs.readFileSync(filePath);
72
+ return {
73
+ file: normalizeSlash(path.relative(cwd, filePath)),
74
+ kind,
75
+ bytes: buffer.length,
76
+ sha256: sha256Buffer(buffer)
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ normalizeSlash,
82
+ sha256Buffer,
83
+ sha256Text,
84
+ truncate,
85
+ stripAnsi,
86
+ fileRecord,
87
+ canonicalizeEphemeralPaths,
88
+ canonicalizeArtifactBuffer,
89
+ normalizeArtifactFile,
90
+ isTextArtifact
91
+ };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ artifactsDir: require('./artifacts-dir.js'),
5
+ assemblyscriptCompile: require('./assemblyscript-compile.js'),
6
+ files: require('./files.js')
7
+ };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ NATIVE_OPTIMIZATION_VERSION,
5
+ EXPERIMENTAL_NATIVE_SIZE_MODE
6
+ } = require('@pulse-compute/wasm-contracts/project/native-optimization');
7
+ const EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION = Object.freeze({
8
+ version: NATIVE_OPTIMIZATION_VERSION,
9
+ mode: EXPERIMENTAL_NATIVE_SIZE_MODE,
10
+ experimental: true,
11
+ goal: 'size',
12
+ assemblyScript: Object.freeze({
13
+ optimize: true,
14
+ optimizeLevel: 3,
15
+ shrinkLevel: 2,
16
+ converge: true
17
+ })
18
+ });
19
+
20
+ function resolveNativeOptimization(value) {
21
+ if (value === undefined || value === null || value === false) return undefined;
22
+ if (
23
+ value === true
24
+ || value === EXPERIMENTAL_NATIVE_SIZE_MODE
25
+ || (typeof value === 'object' && value.mode === EXPERIMENTAL_NATIVE_SIZE_MODE)
26
+ ) {
27
+ return EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION;
28
+ }
29
+ throw new TypeError(`Unsupported Pulse Native optimization mode ${String(value && value.mode || value)}.`);
30
+ }
31
+
32
+ function appendAssemblyScriptOptimizationArgs(args, value) {
33
+ if (!Array.isArray(args)) throw new TypeError('AssemblyScript optimization arguments require a mutable argument array.');
34
+ const optimization = resolveNativeOptimization(value);
35
+ if (!optimization) return undefined;
36
+ args.push(
37
+ '--optimizeLevel', String(optimization.assemblyScript.optimizeLevel),
38
+ '--shrinkLevel', String(optimization.assemblyScript.shrinkLevel)
39
+ );
40
+ if (optimization.assemblyScript.converge) args.push('--converge');
41
+ return optimization;
42
+ }
43
+
44
+ module.exports = Object.freeze({
45
+ NATIVE_OPTIMIZATION_VERSION,
46
+ EXPERIMENTAL_NATIVE_SIZE_MODE,
47
+ EXPERIMENTAL_NATIVE_SIZE_OPTIMIZATION,
48
+ resolveNativeOptimization,
49
+ appendAssemblyScriptOptimizationArgs
50
+ });