wasm-bindgen-lite 0.1.0 → 0.2.0

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/Cargo.toml CHANGED
@@ -21,3 +21,4 @@ codegen-units = 1
21
21
  panic = "abort"
22
22
  strip = true
23
23
 
24
+
package/LICENSE CHANGED
@@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wasm-bindgen-lite",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "CLI tool to build Rust crates into minimal, SIMD-optimized WASM packages with JS loaders",
6
6
  "repository": {
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "scripts": {
31
31
  "build": "node bin/wasm-bindgen-lite.js build --crate . --out dist --no-update-package-json",
32
- "test": "cargo test && node scripts/test.js",
32
+ "test": "npm run test:unit && cargo test && node scripts/test.js",
33
+ "test:unit": "node --test test/*.test.js",
33
34
  "test:examples": "./scripts/test-examples.sh",
34
35
  "lint": "npm run lint:js && npm run lint:rust",
35
36
  "lint:js": "eslint . && prettier --check .",
package/scripts/build.js CHANGED
@@ -58,7 +58,7 @@ try {
58
58
  run(
59
59
  `wasm-opt -Oz ${join(DIST, 'wasm/mod.simd.wasm')} -o ${join(DIST, 'wasm/mod.simd.wasm')}`
60
60
  )
61
- } catch (e) {
61
+ } catch {
62
62
  console.warn('wasm-opt not found, skipping optimization.')
63
63
  }
64
64
 
@@ -23,3 +23,4 @@ for dir in examples/*; do
23
23
  cd ../..
24
24
  fi
25
25
  done
26
+
package/src/cli/build.js CHANGED
@@ -2,6 +2,15 @@ import { execSync } from 'node:child_process'
2
2
  import { copyFileSync, mkdirSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
 
5
+ function exec(cmd, options = {}) {
6
+ try {
7
+ execSync(cmd, { stdio: 'inherit', ...options })
8
+ } catch {
9
+ console.error(`\nError: Command failed: ${cmd}`)
10
+ process.exit(1)
11
+ }
12
+ }
13
+
5
14
  function runCargoBuild({ crateDir, release, simd }) {
6
15
  const args = ['build', '--target', 'wasm32-unknown-unknown']
7
16
  if (release) args.push('--release')
@@ -13,9 +22,8 @@ function runCargoBuild({ crateDir, release, simd }) {
13
22
  env.RUSTFLAGS = [base, extra].filter(Boolean).join(' ').trim()
14
23
  }
15
24
 
16
- execSync(`cargo ${args.join(' ')}`, {
25
+ exec(`cargo ${args.join(' ')}`, {
17
26
  cwd: crateDir,
18
- stdio: 'inherit',
19
27
  env,
20
28
  })
21
29
  }
@@ -42,7 +50,7 @@ function maybeRunWasmOpt(wasmFile, wasmOpt) {
42
50
  }
43
51
 
44
52
  const args = ['wasm-opt', ...wasmOpt.args, wasmFile, '-o', wasmFile]
45
- execSync(args.join(' '), { stdio: 'inherit' })
53
+ exec(args.join(' '))
46
54
  }
47
55
 
48
56
  export function buildArtifacts({
@@ -58,26 +66,24 @@ export function buildArtifacts({
58
66
  const wasmOutDir = join(outDir, 'wasm')
59
67
  mkdirSync(wasmOutDir, { recursive: true })
60
68
 
61
- let baselinePath = null
62
- let simdPath = null
69
+ const paths = { baselinePath: null, simdPath: null, wasmOutDir }
63
70
 
64
- if (targets.baseline) {
65
- console.log('Building baseline wasm...')
66
- runCargoBuild({ crateDir, release, simd: false })
67
- const built = wasmPath({ crateDir, release, wasmFileStem })
68
- baselinePath = join(wasmOutDir, `${artifactBaseName}.base.wasm`)
69
- copyFileSync(built, baselinePath)
70
- maybeRunWasmOpt(baselinePath, wasmOpt)
71
- }
71
+ const build = (isSimd, suffix) => {
72
+ const label = isSimd ? 'SIMD' : 'baseline'
73
+ console.log(`Building ${label} wasm...`)
74
+
75
+ runCargoBuild({ crateDir, release, simd: isSimd })
72
76
 
73
- if (targets.simd) {
74
- console.log('Building SIMD wasm...')
75
- runCargoBuild({ crateDir, release, simd: true })
76
77
  const built = wasmPath({ crateDir, release, wasmFileStem })
77
- simdPath = join(wasmOutDir, `${artifactBaseName}.simd.wasm`)
78
- copyFileSync(built, simdPath)
79
- maybeRunWasmOpt(simdPath, wasmOpt)
78
+ const dest = join(wasmOutDir, `${artifactBaseName}.${suffix}.wasm`)
79
+
80
+ copyFileSync(built, dest)
81
+ maybeRunWasmOpt(dest, wasmOpt)
82
+ return dest
80
83
  }
81
84
 
82
- return { baselinePath, simdPath, wasmOutDir }
85
+ if (targets.baseline) paths.baselinePath = build(false, 'base')
86
+ if (targets.simd) paths.simdPath = build(true, 'simd')
87
+
88
+ return paths
83
89
  }
package/src/cli/config.js CHANGED
@@ -104,90 +104,81 @@ export function loadConfigFromCli(cliOpts = {}) {
104
104
  }
105
105
 
106
106
  const crateName = readCrateName(crateDir)
107
- const artifactBaseName =
108
- cliOpts.artifactBaseName ??
109
- fileConfig.artifactBaseName ??
110
- DEFAULT_CONFIG.artifactBaseName
111
-
112
- const outDir = resolve(
113
- crateDir,
114
- cliOpts.out ?? fileConfig.outDir ?? DEFAULT_CONFIG.outDir
115
- )
116
-
117
- const release =
118
- typeof cliOpts.release === 'boolean'
119
- ? cliOpts.release
120
- : (fileConfig.release ?? DEFAULT_CONFIG.release)
121
-
122
- const targets = {
123
- baseline:
124
- cliOpts.baseline ??
125
- fileConfig.targets?.baseline ??
126
- DEFAULT_CONFIG.targets.baseline,
127
- simd:
128
- typeof cliOpts.simd === 'boolean'
129
- ? cliOpts.simd
130
- : (fileConfig.targets?.simd ?? DEFAULT_CONFIG.targets.simd),
131
- }
132
-
133
- const inline =
134
- typeof cliOpts.inline === 'boolean'
135
- ? cliOpts.inline
136
- : (fileConfig.inline ?? DEFAULT_CONFIG.inline)
137
-
138
- const wasmOpt = normalizeWasmOpt(
139
- cliOpts.wasmOptMode
140
- ? { mode: cliOpts.wasmOptMode, args: cliOpts.wasmOptArgs }
141
- : (fileConfig.wasmOpt ?? DEFAULT_CONFIG.wasmOpt)
142
- )
143
-
144
- const jsEmit = normalizeEmit(fileConfig.js?.emit ?? DEFAULT_CONFIG.js.emit)
145
- const jsCustom = fileConfig.js?.custom ?? DEFAULT_CONFIG.js.custom
146
-
147
- const exportsList =
148
- fileConfig.exports &&
149
- Array.isArray(fileConfig.exports) &&
150
- fileConfig.exports.length
151
- ? fileConfig.exports
152
- : DEFAULT_CONFIG.exports
153
-
154
- const autoInit =
155
- fileConfig.autoInit === 'lazy' ||
156
- fileConfig.autoInit === 'eager' ||
157
- fileConfig.autoInit === 'off'
158
- ? fileConfig.autoInit
159
- : DEFAULT_CONFIG.autoInit
160
-
161
- const streamCfg = {
162
- enable: fileConfig.stream?.enable ?? DEFAULT_CONFIG.stream.enable,
163
- export:
164
- fileConfig.stream?.export ??
165
- exportsList[0]?.name ??
166
- DEFAULT_CONFIG.stream.export,
167
- delimiter: fileConfig.stream?.delimiter ?? DEFAULT_CONFIG.stream.delimiter,
168
- }
169
-
170
- const wasmDelivery = {
171
- type: fileConfig.wasmDelivery?.type ?? DEFAULT_CONFIG.wasmDelivery.type,
172
- package: fileConfig.wasmDelivery?.package ?? fileConfig.name ?? crateName,
173
- version: fileConfig.wasmDelivery?.version ?? fileConfig.version ?? 'latest',
174
- }
175
107
 
108
+ // Merge defaults, file config, and CLI options
176
109
  const config = {
177
110
  crateDir,
178
111
  crateName,
179
112
  wasmFileStem: crateName.replace(/-/g, '_'),
180
- artifactBaseName,
181
- outDir,
182
- release,
183
- inline,
184
- targets,
185
- wasmOpt,
186
- js: { emit: jsEmit, custom: jsCustom },
187
- exports: exportsList,
188
- autoInit,
189
- stream: streamCfg,
190
- wasmDelivery,
113
+
114
+ artifactBaseName:
115
+ cliOpts.artifactBaseName ??
116
+ fileConfig.artifactBaseName ??
117
+ DEFAULT_CONFIG.artifactBaseName,
118
+
119
+ outDir: resolve(
120
+ crateDir,
121
+ cliOpts.out ?? fileConfig.outDir ?? DEFAULT_CONFIG.outDir
122
+ ),
123
+
124
+ release:
125
+ typeof cliOpts.release === 'boolean'
126
+ ? cliOpts.release
127
+ : (fileConfig.release ?? DEFAULT_CONFIG.release),
128
+
129
+ targets: {
130
+ baseline:
131
+ cliOpts.baseline ??
132
+ fileConfig.targets?.baseline ??
133
+ DEFAULT_CONFIG.targets.baseline,
134
+ simd:
135
+ typeof cliOpts.simd === 'boolean'
136
+ ? cliOpts.simd
137
+ : (fileConfig.targets?.simd ?? DEFAULT_CONFIG.targets.simd),
138
+ },
139
+
140
+ inline:
141
+ typeof cliOpts.inline === 'boolean'
142
+ ? cliOpts.inline
143
+ : (fileConfig.inline ?? DEFAULT_CONFIG.inline),
144
+
145
+ wasmOpt: normalizeWasmOpt(
146
+ cliOpts.wasmOptMode
147
+ ? { mode: cliOpts.wasmOptMode, args: cliOpts.wasmOptArgs }
148
+ : (fileConfig.wasmOpt ?? DEFAULT_CONFIG.wasmOpt)
149
+ ),
150
+
151
+ js: {
152
+ emit: normalizeEmit(fileConfig.js?.emit ?? DEFAULT_CONFIG.js.emit),
153
+ custom: fileConfig.js?.custom ?? DEFAULT_CONFIG.js.custom,
154
+ },
155
+
156
+ exports:
157
+ fileConfig.exports && Array.isArray(fileConfig.exports)
158
+ ? fileConfig.exports
159
+ : DEFAULT_CONFIG.exports,
160
+
161
+ autoInit: ['lazy', 'eager', 'off'].includes(fileConfig.autoInit)
162
+ ? fileConfig.autoInit
163
+ : DEFAULT_CONFIG.autoInit,
164
+
165
+ stream: {
166
+ enable: fileConfig.stream?.enable ?? DEFAULT_CONFIG.stream.enable,
167
+ export:
168
+ fileConfig.stream?.export ??
169
+ fileConfig.exports?.[0]?.name ??
170
+ DEFAULT_CONFIG.stream.export,
171
+ delimiter:
172
+ fileConfig.stream?.delimiter ?? DEFAULT_CONFIG.stream.delimiter,
173
+ blockSize: fileConfig.stream?.blockSize ?? null,
174
+ },
175
+
176
+ wasmDelivery: {
177
+ type: fileConfig.wasmDelivery?.type ?? DEFAULT_CONFIG.wasmDelivery.type,
178
+ package: fileConfig.wasmDelivery?.package ?? fileConfig.name ?? crateName,
179
+ version:
180
+ fileConfig.wasmDelivery?.version ?? fileConfig.version ?? 'latest',
181
+ },
191
182
  }
192
183
 
193
184
  return config
package/src/cli/emit.js CHANGED
@@ -4,331 +4,468 @@ import { fileURLToPath } from 'node:url'
4
4
 
5
5
  const UTIL_PATH = fileURLToPath(new URL('../js/util.js', import.meta.url))
6
6
 
7
- function createCore({ exportsList, autoInit, stream }) {
8
- const needsEnsure = autoInit === 'lazy'
9
- const toBytesHelper = `function toBytes(input) {
10
- if (input instanceof Uint8Array) return input;
11
- if (ArrayBuffer.isView(input)) {
12
- return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
13
- }
14
- if (input instanceof ArrayBuffer) return new Uint8Array(input);
15
- throw new TypeError("Expected a TypedArray or ArrayBuffer");
16
- }
17
- `
18
- const scalarSizeHelper = `function scalarSize(type) {
19
- switch (type) {
20
- case "f64": return 8;
21
- case "f32":
22
- case "i32":
23
- case "u32": return 4;
24
- case "i16":
25
- case "u16": return 2;
26
- case "i8":
27
- case "u8": return 1;
28
- case "u32_array":
29
- case "i32_array":
30
- case "f32_array": return 1024 * 1024; // Default large buffer for arrays, or we can improve this
31
- default: return 0;
32
- }
33
- }
34
- `
35
- const decodeHelper = `function decodeReturn(view, type) {
36
- switch (type) {
37
- case "f32": return view.getFloat32(0, true);
38
- case "f64": return view.getFloat64(0, true);
39
- case "i32": return view.getInt32(0, true);
40
- case "u32": return view.getUint32(0, true);
41
- case "i16": return view.getInt16(0, true);
42
- case "u16": return view.getUint16(0, true);
43
- case "i8": return view.getInt8(0);
44
- case "u8": return view.getUint8(0);
45
- case "u32_array": return new Uint32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
46
- case "i32_array": return new Int32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
47
- case "f32_array": return new Float32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));
48
- default: return null;
49
- }
7
+ export function buildWrapperIR(exportsList) {
8
+ return exportsList.map((entry) => {
9
+ const { abi, name, return: retType, reuseBuffer, outSize } = entry
10
+ const returnType = retType || 'bytes'
11
+ const fnName = name || abi
12
+ const outSizeExpr =
13
+ returnType !== 'bytes'
14
+ ? `(scalarSize('${returnType}') || 4)`
15
+ : outSize
16
+ ? outSize.replace(/\blen\b/g, 'len')
17
+ : 'Math.max(len, 4)'
18
+
19
+ return {
20
+ abi,
21
+ fnName,
22
+ returnType,
23
+ reuseBuffer: !!reuseBuffer,
24
+ outSizeExpr,
25
+ }
26
+ })
50
27
  }
51
- `
52
-
53
- const wrappers = exportsList
54
- .map(({ abi, name, return: retType, reuseBuffer }) => {
55
- const returnType = retType || 'bytes'
56
- const fnName = name || abi
57
- const outSizeExpr =
58
- returnType === 'bytes'
59
- ? 'Math.max(len, 4)'
60
- : `(scalarSize('${returnType}') || 4)`
61
-
62
- const stateVars = reuseBuffer
63
- ? `let _${fnName}_in = { ptr: 0, len: 0 };
64
- let _${fnName}_out = { ptr: 0, len: 0 };`
65
- : ''
66
-
67
- const allocIn = reuseBuffer
68
- ? `if (_${fnName}_in.len < len) {
69
- if (_${fnName}_in.ptr) free(_${fnName}_in.ptr, _${fnName}_in.len);
70
- _${fnName}_in.ptr = alloc(len);
71
- _${fnName}_in.len = len;
72
- }
73
- const inPtr = _${fnName}_in.ptr;`
74
- : `const inPtr = alloc(len);`
75
-
76
- const allocOut = reuseBuffer
77
- ? `if (_${fnName}_out.len < outLen) {
78
- if (_${fnName}_out.ptr) free(_${fnName}_out.ptr, _${fnName}_out.len);
79
- _${fnName}_out.ptr = alloc(outLen);
80
- _${fnName}_out.len = outLen;
81
- }
82
- const outPtr = _${fnName}_out.ptr;`
83
- : `const outPtr = alloc(outLen);`
84
-
85
- const freeIn = reuseBuffer ? '' : `free(inPtr, len);`
86
- const freeOut = reuseBuffer ? '' : `free(outPtr, outLen);`
87
-
88
- const body = `
89
- if (!_inst) throw new Error("WASM instance not initialized");
90
- const view = toBytes(input);
91
- const len = view.byteLength;
92
- ${allocIn}
93
- memoryU8().set(view, inPtr);
94
- const outLen = ${outSizeExpr};
95
- ${allocOut}
96
- const written = _inst.exports.${abi}(
97
- inPtr, len,
98
- outPtr, outLen
99
- );
100
- if (written < 0) {
101
- ${reuseBuffer ? '' : `free(inPtr, len); free(outPtr, outLen);`}
102
- throw new Error("${abi} failed: " + written);
103
- }
104
- ${
105
- returnType === 'bytes'
106
- ? `const result = memoryU8().slice(outPtr, outPtr + written);`
107
- : `const retView = new DataView(memoryU8().buffer, outPtr, written);
108
- const ret = decodeReturn(retView, "${returnType}");`
109
- }
110
- ${freeIn}
111
- ${freeOut}
112
- ${returnType === 'bytes' ? 'return result;' : 'return ret;'}`
113
28
 
114
- const wrapper = needsEnsure
115
- ? `async function ${fnName}(input) { await ensureReady(); ${body} }`
116
- : `function ${fnName}(input) { ${body} }`
117
-
118
- return `${stateVars}\n${wrapper}\nexport { ${fnName} };`
29
+ export function createCore({ exportsList, autoInit, stream }) {
30
+ const needsEnsure = autoInit === 'lazy'
31
+ const wrappersIR = buildWrapperIR(exportsList)
32
+ const b = code()
33
+
34
+ b.line('let _inst = null;')
35
+ b.line('let _memU8 = null;')
36
+ b.line('let _initFn = null;')
37
+ b.blank()
38
+
39
+ b.line('function refreshViews() {')
40
+ b.indent(() => {
41
+ b.line('_memU8 = new Uint8Array(_inst.exports.memory.buffer);')
42
+ })
43
+ b.line('}')
44
+ b.blank()
45
+
46
+ b.line('export function setInstance(instance) {')
47
+ b.indent(() => {
48
+ b.line('_inst = instance;')
49
+ b.line('refreshViews();')
50
+ })
51
+ b.line('}')
52
+ b.blank()
53
+
54
+ b.line('export function wasmExports() {')
55
+ b.indent(() => {
56
+ b.line('return _inst.exports;')
57
+ })
58
+ b.line('}')
59
+ b.blank()
60
+
61
+ if (needsEnsure) {
62
+ b.line('let _ready = null;')
63
+ b.line('export function registerInit(fn) { _initFn = fn; }')
64
+ b.blank()
65
+ b.line('async function ensureReady() {')
66
+ b.indent(() => {
67
+ b.line('if (_ready) return _ready;')
68
+ b.line('if (!_initFn) throw new Error("init not registered");')
69
+ b.line('_ready = _initFn();')
70
+ b.line('return _ready;')
119
71
  })
120
- .join('\n\n')
121
-
122
- const streamHelper = stream?.enable
123
- ? `
124
- const __exports = { ${exportsList.map(({ name, abi }) => `${name || abi}: ${name || abi}`).join(', ')} };
125
-
126
- export function createTransformStream(fnName = "${stream.export}") {
127
- const fn = __exports[fnName];
128
- if (!fn) throw new Error("Unknown export for streaming: " + fnName);
129
-
130
- ${
131
- stream.delimiter !== null
132
- ? `let buffer = new Uint8Array(0);
133
- const delimiter = ${stream.delimiter};`
134
- : ''
72
+ b.line('}')
73
+ } else {
74
+ b.line('export function registerInit(fn) { _initFn = fn; }')
135
75
  }
76
+ b.blank()
136
77
 
137
- return new TransformStream({
138
- async transform(chunk, controller) {
139
- const bytes = toBytes(chunk);
140
- const processed = ${needsEnsure ? 'await fn(bytes)' : 'fn(bytes)'};
141
-
142
- ${
143
- stream.delimiter !== null
144
- ? `// Split and buffer
145
- const combined = new Uint8Array(buffer.length + processed.length);
146
- combined.set(buffer, 0);
147
- combined.set(processed, buffer.length);
148
-
149
- let start = 0;
150
- for (let i = 0; i < combined.length; i += 1) {
151
- if (combined[i] === delimiter) {
152
- controller.enqueue(combined.subarray(start, i));
153
- start = i + 1;
154
- }
78
+ b.line('export function memoryU8() {')
79
+ b.indent(() => {
80
+ b.line(
81
+ 'if (_memU8 && _memU8.buffer !== _inst.exports.memory.buffer) refreshViews();'
82
+ )
83
+ b.line('return _memU8;')
84
+ })
85
+ b.line('}')
86
+ b.blank()
87
+
88
+ b.line('export function alloc(len) {')
89
+ b.indent(() => {
90
+ b.line('return _inst.exports.alloc_bytes(len) >>> 0;')
91
+ })
92
+ b.line('}')
93
+ b.blank()
94
+
95
+ b.line('export function free(ptr, len) {')
96
+ b.indent(() => {
97
+ b.line('_inst.exports.free_bytes(ptr >>> 0, len >>> 0);')
98
+ })
99
+ b.line('}')
100
+ b.blank()
101
+
102
+ // Runtime Helpers
103
+ b.line('function toBytes(input) {')
104
+ b.indent(() => {
105
+ b.line('if (input instanceof Uint8Array) return input;')
106
+ b.line(
107
+ 'if (ArrayBuffer.isView(input)) return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);'
108
+ )
109
+ b.line('if (input instanceof ArrayBuffer) return new Uint8Array(input);')
110
+ b.line('throw new TypeError("Expected a TypedArray or ArrayBuffer");')
111
+ })
112
+ b.line('}')
113
+ b.blank()
114
+
115
+ b.line('function scalarSize(type) {')
116
+ b.indent(() => {
117
+ b.line('switch (type) {')
118
+ b.line(' case "f64": return 8;')
119
+ b.line(' case "f32":')
120
+ b.line(' case "i32":')
121
+ b.line(' case "u32": return 4;')
122
+ b.line(' case "i16":')
123
+ b.line(' case "u16": return 2;')
124
+ b.line(' case "i8":')
125
+ b.line(' case "u8": return 1;')
126
+ b.line(' case "u32_array":')
127
+ b.line(' case "i32_array":')
128
+ b.line(' case "f32_array": return 1024 * 1024;')
129
+ b.line(' default: return 0;')
130
+ b.line('}')
131
+ })
132
+ b.line('}')
133
+ b.blank()
134
+
135
+ b.line('function decodeReturn(view, type) {')
136
+ b.indent(() => {
137
+ b.line('switch (type) {')
138
+ b.line(' case "f32": return view.getFloat32(0, true);')
139
+ b.line(' case "f64": return view.getFloat64(0, true);')
140
+ b.line(' case "i32": return view.getInt32(0, true);')
141
+ b.line(' case "u32": return view.getUint32(0, true);')
142
+ b.line(' case "i16": return view.getInt16(0, true);')
143
+ b.line(' case "u16": return view.getUint16(0, true);')
144
+ b.line(' case "i8": return view.getInt8(0);')
145
+ b.line(' case "u8": return view.getUint8(0);')
146
+ b.line(
147
+ ' case "u32_array": return new Uint32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));'
148
+ )
149
+ b.line(
150
+ ' case "i32_array": return new Int32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));'
151
+ )
152
+ b.line(
153
+ ' case "f32_array": return new Float32Array(view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength));'
154
+ )
155
+ b.line(' default: return null;')
156
+ b.line('}')
157
+ })
158
+ b.line('}')
159
+ b.blank()
160
+
161
+ b.line('function callWasm(abi, input, outLen, reuse) {')
162
+ b.indent(() => {
163
+ b.line('if (!_inst) throw new Error("WASM instance not initialized");')
164
+ b.line('const view = toBytes(input);')
165
+ b.line('const len = view.byteLength;')
166
+ b.blank()
167
+ b.line('let inPtr, outPtr;')
168
+ b.line('if (reuse) {')
169
+ b.indent(() => {
170
+ b.line('if (reuse.in.len < len) {')
171
+ b.indent(() => {
172
+ b.line('if (reuse.in.ptr) free(reuse.in.ptr, reuse.in.len);')
173
+ b.line('reuse.in.ptr = alloc(len);')
174
+ b.line('reuse.in.len = len;')
175
+ })
176
+ b.line('}')
177
+ b.line('if (reuse.out.len < outLen) {')
178
+ b.indent(() => {
179
+ b.line('if (reuse.out.ptr) free(reuse.out.ptr, reuse.out.len);')
180
+ b.line('reuse.out.ptr = alloc(outLen);')
181
+ b.line('reuse.out.len = outLen;')
182
+ })
183
+ b.line('}')
184
+ b.line('inPtr = reuse.in.ptr;')
185
+ b.line('outPtr = reuse.out.ptr;')
186
+ })
187
+ b.line('} else {')
188
+ b.indent(() => {
189
+ b.line('inPtr = alloc(len);')
190
+ b.line('outPtr = alloc(outLen);')
191
+ })
192
+ b.line('}')
193
+ b.blank()
194
+ b.line('memoryU8().set(view, inPtr);')
195
+ b.line('const written = _inst.exports[abi](inPtr, len, outPtr, outLen);')
196
+ b.line('if (written < 0) {')
197
+ b.indent(() => {
198
+ b.line('if (!reuse) { free(inPtr, len); free(outPtr, outLen); }')
199
+ b.line('throw new Error(abi + " failed: " + written);')
200
+ })
201
+ b.line('}')
202
+ b.blank()
203
+ b.line('return { inPtr, outPtr, len, outLen, written };')
204
+ })
205
+ b.line('}')
206
+ b.blank()
207
+
208
+ // Wrappers
209
+ wrappersIR.forEach((w) => {
210
+ if (w.reuseBuffer) {
211
+ b.line(
212
+ `const _${w.fnName}_reuse = { in: { ptr: 0, len: 0 }, out: { ptr: 0, len: 0 } };`
213
+ )
214
+ }
215
+ const asyncPrefix = needsEnsure ? 'async ' : ''
216
+ b.line(`${asyncPrefix}function ${w.fnName}(input) {`)
217
+ b.indent(() => {
218
+ if (needsEnsure) b.line('await ensureReady();')
219
+ b.line('const view = toBytes(input);')
220
+ b.line('const len = view.byteLength;')
221
+ b.line(`const outLen = ${w.outSizeExpr};`)
222
+ b.line(
223
+ `const { outPtr, written, inPtr } = callWasm("${w.abi}", view, outLen, ${w.reuseBuffer ? `_${w.fnName}_reuse` : 'null'});`
224
+ )
225
+ b.blank()
226
+ if (w.returnType === 'bytes') {
227
+ b.line('const result = memoryU8().slice(outPtr, outPtr + written);')
228
+ } else {
229
+ b.line(
230
+ 'const retView = new DataView(memoryU8().buffer, outPtr, written);'
231
+ )
232
+ b.line(`const result = decodeReturn(retView, "${w.returnType}");`)
155
233
  }
156
- buffer = combined.slice(start);`
157
- : 'controller.enqueue(processed);'
234
+ b.blank()
235
+ if (!w.reuseBuffer) {
236
+ b.line('free(inPtr, len);')
237
+ b.line('free(outPtr, outLen);')
158
238
  }
159
- }${
160
- stream.delimiter !== null
161
- ? `,
162
- flush(controller) {
163
- if (buffer.length) controller.enqueue(buffer);
164
- }`
165
- : ''
166
- }
167
- });
168
- }
169
- `
170
- : ''
171
-
172
- const ensure = needsEnsure
173
- ? `
174
- let _ready = null;
175
- export function registerInit(fn) {
176
- _initFn = fn;
177
- }
178
-
179
- async function ensureReady() {
180
- if (_ready) return _ready;
181
- if (!_initFn) throw new Error("init not registered");
182
- _ready = _initFn();
183
- return _ready;
184
- }
185
- `
186
- : `
187
- export function registerInit(fn) {
188
- _initFn = fn;
189
- }
190
- `
239
+ b.line('return result;')
240
+ })
241
+ b.line('}')
242
+ b.line(`export { ${w.fnName} };`)
243
+ b.blank()
244
+ })
245
+
246
+ // Streaming
247
+ if (stream?.enable) {
248
+ b.line('const __exports = {')
249
+ b.indent(() => {
250
+ exportsList.forEach((e) => {
251
+ b.line(
252
+ `${e.name || e.abi}: { fn: ${e.name || e.abi}, blockSize: ${e.blockSize || 'null'} },`
253
+ )
254
+ })
255
+ })
256
+ b.line('};')
257
+ b.blank()
191
258
 
192
- return `let _inst = null;
193
- let _memU8 = null;
194
- let _initFn = null;
259
+ b.line(
260
+ 'export function createChunkTransform(processFn, { blockSize = null, delimiter = null } = {}) {'
261
+ )
262
+ b.indent(() => {
263
+ b.line('let buffer = new Uint8Array(0);')
264
+ b.line('return new TransformStream({')
265
+ b.indent(() => {
266
+ b.line('async transform(chunk, controller) {')
267
+ b.indent(() => {
268
+ b.line('const bytes = toBytes(chunk);')
269
+ b.line('let input = bytes;')
270
+ b.blank()
271
+ b.line(
272
+ 'if (buffer.length > 0 || blockSize !== null || delimiter !== null) {'
273
+ )
274
+ b.indent(() => {
275
+ b.line(
276
+ 'const combined = new Uint8Array(buffer.length + bytes.length);'
277
+ )
278
+ b.line('combined.set(buffer, 0);')
279
+ b.line('combined.set(bytes, buffer.length);')
280
+ b.line('input = combined;')
281
+ })
282
+ b.line('}')
283
+ b.blank()
284
+ b.line('if (delimiter !== null) {')
285
+ b.indent(() => {
286
+ b.line('let start = 0;')
287
+ b.line('for (let i = 0; i < input.length; i++) {')
288
+ b.indent(() => {
289
+ b.line('if (input[i] === delimiter) {')
290
+ b.indent(() => {
291
+ b.line(
292
+ 'controller.enqueue(await processFn(input.subarray(start, i)));'
293
+ )
294
+ b.line('start = i + 1;')
295
+ })
296
+ b.line('}')
297
+ })
298
+ b.line('}')
299
+ b.line('buffer = input.slice(start);')
300
+ })
301
+ b.line('} else if (blockSize !== null) {')
302
+ b.indent(() => {
303
+ b.line(
304
+ 'const processLen = input.length - (input.length % blockSize);'
305
+ )
306
+ b.line('if (processLen > 0) {')
307
+ b.indent(() => {
308
+ b.line(
309
+ 'controller.enqueue(await processFn(input.subarray(0, processLen)));'
310
+ )
311
+ b.line('buffer = input.slice(processLen);')
312
+ })
313
+ b.line('} else {')
314
+ b.indent(() => {
315
+ b.line('buffer = input;')
316
+ })
317
+ b.line('}')
318
+ })
319
+ b.line('} else {')
320
+ b.indent(() => {
321
+ b.line('controller.enqueue(await processFn(input));')
322
+ b.line('buffer = new Uint8Array(0);')
323
+ })
324
+ b.line('}')
325
+ })
326
+ b.line('},')
327
+ b.line('async flush(controller) {')
328
+ b.indent(() => {
329
+ b.line('if (buffer.length > 0) {')
330
+ b.indent(() => {
331
+ b.line('controller.enqueue(await processFn(buffer));')
332
+ b.line('buffer = new Uint8Array(0);')
333
+ })
334
+ b.line('}')
335
+ })
336
+ b.line('}')
337
+ })
338
+ b.line('});')
339
+ })
340
+ b.line('}')
341
+ b.blank()
195
342
 
196
- function refreshViews() {
197
- _memU8 = new Uint8Array(_inst.exports.memory.buffer);
198
- }
343
+ b.line(
344
+ `export function createTransformStream(fnName = "${stream.export}") {`
345
+ )
346
+ b.indent(() => {
347
+ b.line('const entry = __exports[fnName];')
348
+ b.line(
349
+ 'if (!entry) throw new Error("Unknown export for streaming: " + fnName);'
350
+ )
351
+ b.line('const { fn, blockSize: entryBlockSize } = entry;')
352
+ b.line(
353
+ `const blockSize = entryBlockSize ?? ${stream.blockSize !== null ? stream.blockSize : 'null'};`
354
+ )
355
+ b.line(
356
+ `const delimiter = ${stream.delimiter !== null ? stream.delimiter : 'null'};`
357
+ )
358
+ b.line('return createChunkTransform(fn, { blockSize, delimiter });')
359
+ })
360
+ b.line('}')
361
+ }
199
362
 
200
- export function setInstance(instance) {
201
- _inst = instance;
202
- refreshViews();
363
+ return b.toString()
203
364
  }
204
365
 
205
- export function wasmExports() {
206
- return _inst.exports;
366
+ export function code() {
367
+ const lines = []
368
+ let indent = 0
369
+ const api = {
370
+ line(s = '') {
371
+ lines.push(' '.repeat(indent) + s)
372
+ return api
373
+ },
374
+ blank() {
375
+ lines.push('')
376
+ return api
377
+ },
378
+ indent(fn) {
379
+ indent++
380
+ fn()
381
+ indent--
382
+ return api
383
+ },
384
+ toString() {
385
+ return lines.join('\n')
386
+ },
387
+ }
388
+ return api
207
389
  }
208
390
 
209
- ${ensure}
210
-
211
- export function memoryU8() {
212
- if (_memU8 && _memU8.buffer !== _inst.exports.memory.buffer) refreshViews();
213
- return _memU8;
214
- }
391
+ export function createLoader({ exportFrom, autoInit, getBytesSrc }) {
392
+ const eager =
393
+ autoInit === 'eager'
394
+ ? '\nregisterInit(init);\ninit();'
395
+ : '\nregisterInit(init);'
215
396
 
216
- export function alloc(len) {
217
- return _inst.exports.alloc_bytes(len) >>> 0;
218
- }
397
+ return `import { setInstance, registerInit } from "./core.js";
398
+ import { instantiateWithFallback } from "./util.js";
399
+ ${getBytesSrc}
219
400
 
220
- export function free(ptr, len) {
221
- _inst.exports.free_bytes(ptr >>> 0, len >>> 0);
401
+ let _ready = null;
402
+ export function init(imports = {}) {
403
+ return (_ready ??= (async () => {
404
+ const { simdBytes, baseBytes } = await getWasmBytes();
405
+ const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
406
+ setInstance(instance);
407
+ })());
222
408
  }
223
-
224
- ${wrappers}
225
- ${scalarSizeHelper}
226
- ${decodeHelper}
227
- ${toBytesHelper}
228
- ${streamHelper}
409
+ ${eager}
410
+ export * from "${exportFrom}";
229
411
  `
230
412
  }
231
413
 
232
414
  function createBrowserLoader({ name, autoInit, customJs, wasmDelivery }) {
233
- const eager =
234
- autoInit === 'eager'
235
- ? '\nregisterInit(init);\ninit();'
236
- : '\nregisterInit(init);'
237
415
  const exportFrom = customJs ? './custom.js' : './core.js'
238
416
 
239
417
  let simdUrl, baseUrl
240
418
  if (wasmDelivery.type === 'jsdelivr') {
241
419
  const pkg = wasmDelivery.package
242
420
  const ver = wasmDelivery.version
243
- simdUrl = `https://cdn.jsdelivr.net/npm/${pkg}@${ver}/dist/wasm/${name}.simd.wasm`
244
- baseUrl = `https://cdn.jsdelivr.net/npm/${pkg}@${ver}/dist/wasm/${name}.base.wasm`
421
+ simdUrl = `"https://cdn.jsdelivr.net/npm/${pkg}@${ver}/dist/wasm/${name}.simd.wasm"`
422
+ baseUrl = `"https://cdn.jsdelivr.net/npm/${pkg}@${ver}/dist/wasm/${name}.base.wasm"`
245
423
  } else {
246
424
  simdUrl = `new URL("./wasm/${name}.simd.wasm", import.meta.url)`
247
425
  baseUrl = `new URL("./wasm/${name}.base.wasm", import.meta.url)`
248
426
  }
249
427
 
250
- return `import { setInstance, registerInit } from "./core.js";
251
- import { instantiateWithFallback } from "./util.js";
252
-
253
- const simdUrl = ${wasmDelivery.type === 'jsdelivr' ? `"${simdUrl}"` : simdUrl};
254
- const baseUrl = ${wasmDelivery.type === 'jsdelivr' ? `"${baseUrl}"` : baseUrl};
255
-
256
- let _ready = null;
257
-
258
- export function init(imports = {}) {
259
- return (_ready ??= (async () => {
260
- const [simdRes, baseRes] = await Promise.all([
261
- fetch(simdUrl),
262
- fetch(baseUrl)
263
- ]);
264
-
265
- const [simdBytes, baseBytes] = await Promise.all([
266
- simdRes.arrayBuffer(),
267
- baseRes.arrayBuffer()
268
- ]);
428
+ const getBytesSrc = `
429
+ const simdUrl = ${simdUrl};
430
+ const baseUrl = ${baseUrl};
269
431
 
270
- const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
271
- setInstance(instance);
272
- })());
432
+ async function getWasmBytes() {
433
+ const [simdRes, baseRes] = await Promise.all([fetch(simdUrl), fetch(baseUrl)]);
434
+ const [simdBytes, baseBytes] = await Promise.all([simdRes.arrayBuffer(), baseRes.arrayBuffer()]);
435
+ return { simdBytes, baseBytes };
273
436
  }
274
- ${eager}
275
- export * from "${exportFrom}";
276
437
  `
438
+ return createLoader({ exportFrom, autoInit, getBytesSrc })
277
439
  }
278
440
 
279
441
  function createNodeLoader({ name, autoInit, customJs }) {
280
- const eager =
281
- autoInit === 'eager'
282
- ? '\nregisterInit(init);\ninit();'
283
- : '\nregisterInit(init);'
284
442
  const exportFrom = customJs ? './custom.js' : './core.js'
285
- return `import { readFile } from "node:fs/promises";
443
+ const getBytesSrc = `
444
+ import { readFile } from "node:fs/promises";
286
445
  import { fileURLToPath } from "node:url";
287
- import { setInstance, registerInit } from "./core.js";
288
- import { instantiateWithFallback } from "./util.js";
289
446
 
290
447
  const simdPath = fileURLToPath(new URL("./wasm/${name}.simd.wasm", import.meta.url));
291
448
  const basePath = fileURLToPath(new URL("./wasm/${name}.base.wasm", import.meta.url));
292
449
 
293
- let _ready = null;
294
-
295
- export function init(imports = {}) {
296
- return (_ready ??= (async () => {
297
- const [simdBytes, baseBytes] = await Promise.all([
298
- readFile(simdPath),
299
- readFile(basePath)
300
- ]);
301
- const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
302
- setInstance(instance);
303
- })());
450
+ async function getWasmBytes() {
451
+ const [simdBytes, baseBytes] = await Promise.all([readFile(simdPath), readFile(basePath)]);
452
+ return { simdBytes, baseBytes };
304
453
  }
305
- ${eager}
306
- export * from "${exportFrom}";
307
454
  `
455
+ return createLoader({ exportFrom, autoInit, getBytesSrc })
308
456
  }
309
457
 
310
458
  function createInlineLoader({ name, autoInit, customJs }) {
311
- const eager =
312
- autoInit === 'eager'
313
- ? '\nregisterInit(init);\ninit();'
314
- : '\nregisterInit(init);'
315
459
  const exportFrom = customJs ? './custom.js' : './core.js'
316
- return `import { wasmBytes as simdBytes } from "./wasm-inline/${name}.simd.wasm.js";
460
+ const getBytesSrc = `
461
+ import { wasmBytes as simdBytes } from "./wasm-inline/${name}.simd.wasm.js";
317
462
  import { wasmBytes as baseBytes } from "./wasm-inline/${name}.base.wasm.js";
318
- import { setInstance, registerInit } from "./core.js";
319
- import { instantiateWithFallback } from "./util.js";
320
-
321
- let _ready = null;
322
463
 
323
- export function init(imports = {}) {
324
- return (_ready ??= (async () => {
325
- const { instance } = await instantiateWithFallback(simdBytes, baseBytes, imports);
326
- setInstance(instance);
327
- })());
464
+ async function getWasmBytes() {
465
+ return { simdBytes, baseBytes };
328
466
  }
329
- ${eager}
330
- export * from "${exportFrom}";
331
467
  `
468
+ return createLoader({ exportFrom, autoInit, getBytesSrc })
332
469
  }
333
470
 
334
471
  function createInlineModule(bytes) {
package/src/cli/index.js CHANGED
@@ -36,7 +36,6 @@ export async function runBuild(cliOpts) {
36
36
  updatePackageJson({
37
37
  crateDir: cfg.crateDir,
38
38
  outDir: cfg.outDir,
39
- artifactBaseName: cfg.artifactBaseName,
40
39
  js: cfg.js,
41
40
  inline: cfg.inline,
42
41
  })
package/src/cli/pkg.js CHANGED
@@ -1,13 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'node:fs'
2
2
  import { join, relative } from 'node:path'
3
3
 
4
- export function updatePackageJson({
5
- crateDir,
6
- outDir,
7
- artifactBaseName,
8
- js,
9
- inline,
10
- }) {
4
+ export function updatePackageJson({ crateDir, outDir, js, inline }) {
11
5
  const pkgPath = join(crateDir, 'package.json')
12
6
  if (!existsSync(pkgPath)) return
13
7
 
package/src/js/util.js CHANGED
@@ -6,7 +6,7 @@ export async function instantiateWithFallback(
6
6
  try {
7
7
  const { instance } = await WebAssembly.instantiate(trySimdBytes, imports)
8
8
  return { instance, backend: 'wasm-simd' }
9
- } catch (e) {
9
+ } catch {
10
10
  // If SIMD fails (not supported), try baseline
11
11
  const { instance } = await WebAssembly.instantiate(baseBytes, imports)
12
12
  return { instance, backend: 'wasm' }