@kupcad/wasm 0.0.0-stage → 0.1.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/LICENSE +661 -0
- package/README.md +73 -2
- package/dist/kupcad.wasm +0 -0
- package/package.json +43 -3
- package/src/client.js +291 -0
- package/src/errors.js +25 -0
- package/src/files.js +33 -0
- package/src/index.js +6 -0
- package/src/kupcad.js +260 -0
- package/src/load.js +36 -0
- package/src/read-file.browser.js +12 -0
- package/src/read-file.node.js +11 -0
- package/src/serve.js +84 -0
- package/src/testing.js +39 -0
- package/src/types.d.ts +141 -0
- package/src/wasi.js +127 -0
- package/src/worker.js +8 -0
package/src/kupcad.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// The core in this thread: the module instantiated once, its exports given a typed face. Every
|
|
2
|
+
// string and buffer goes in through `wasm_alloc` and is freed after the call; every result is
|
|
3
|
+
// copied out and freed (`wasm_free`), and an export's null result is the core's refusal, its
|
|
4
|
+
// message read from `get_last_error`.
|
|
5
|
+
|
|
6
|
+
import { isCompatible, PROTOCOL_VERSION, ProtocolError, readFrame } from '@kupcad/protocol';
|
|
7
|
+
import { KupcadCrashError, KupcadError } from './errors.js';
|
|
8
|
+
import { packFiles } from './files.js';
|
|
9
|
+
import { loadModule } from './load.js';
|
|
10
|
+
import { createWasi, WasiExit } from './wasi.js';
|
|
11
|
+
|
|
12
|
+
/** @import { Finding, Preview, UiSchema, VersionInfo } from '@kupcad/protocol/types' */
|
|
13
|
+
/** @import { BuildOptions, CallOptions, CreateOptions, KupcadApi, KupcadExports, PreviewOptions } from './types.js' */
|
|
14
|
+
|
|
15
|
+
/** The module this package ships, built by core/build.zig and copied in by `pnpm build`. */
|
|
16
|
+
const DEFAULT_WASM = new URL('../dist/kupcad.wasm', import.meta.url);
|
|
17
|
+
/** A `usize` of wasm32: the slot an export writes its result's length to. */
|
|
18
|
+
const USIZE_BYTES = 4;
|
|
19
|
+
/** The longest message get_last_error holds (core/src/wasm.zig's MAX_ERROR_MSG_LEN). */
|
|
20
|
+
const MAX_ERROR_BYTES = 1024;
|
|
21
|
+
const DEFAULT_FORMAT = 'glb';
|
|
22
|
+
/** What a call without files gives the core. */
|
|
23
|
+
const NO_FILES = new Uint8Array();
|
|
24
|
+
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
const decoder = new TextDecoder();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A wasm32 pointer an export returned: a u32 that JavaScript gets as an i32, so an address
|
|
30
|
+
* past 2 GiB comes negative, as does an empty block's (0xFFFFFFFF, which `wasm_alloc(0)` gives).
|
|
31
|
+
* @param {number} ptr
|
|
32
|
+
*/
|
|
33
|
+
const address = (ptr) => ptr >>> 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The core, instantiated in this thread. A long build blocks it: a page runs the core in a
|
|
37
|
+
* worker (`createKupcadWorker`).
|
|
38
|
+
* @param {CreateOptions} [options]
|
|
39
|
+
* @returns {Promise<Kupcad>}
|
|
40
|
+
* @throws {ProtocolError} when the module speaks a protocol version this package doesn't read
|
|
41
|
+
*/
|
|
42
|
+
export async function createKupcad(options = {}) {
|
|
43
|
+
const module = await loadModule(options.wasm ?? DEFAULT_WASM);
|
|
44
|
+
const wasi = createWasi(options.log ?? ((line) => console.debug(line)));
|
|
45
|
+
const instance = await WebAssembly.instantiate(module, {
|
|
46
|
+
wasi_snapshot_preview1: wasi.imports(module),
|
|
47
|
+
});
|
|
48
|
+
const exports = /** @type {KupcadExports} */ (/** @type {unknown} */ (instance.exports));
|
|
49
|
+
wasi.bind(exports.memory);
|
|
50
|
+
// oxlint-disable-next-line no-underscore-dangle -- the WASI reactor's initializer, by that name
|
|
51
|
+
exports._initialize();
|
|
52
|
+
return Kupcad.fromExports(exports);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @implements {KupcadApi} */
|
|
56
|
+
export class Kupcad {
|
|
57
|
+
/**
|
|
58
|
+
* The core over a module a host instantiated itself (its own WASI, its own memory), once it
|
|
59
|
+
* is initialized.
|
|
60
|
+
* @param {KupcadExports} exports
|
|
61
|
+
* @returns {Promise<Kupcad>}
|
|
62
|
+
* @throws {ProtocolError} when the module speaks a protocol version this package doesn't read
|
|
63
|
+
*/
|
|
64
|
+
static async fromExports(exports) {
|
|
65
|
+
const kupcad = new Kupcad(exports);
|
|
66
|
+
const { version, protocolVersion } = await kupcad.version();
|
|
67
|
+
if (!isCompatible(protocolVersion)) {
|
|
68
|
+
throw new ProtocolError(
|
|
69
|
+
`kupcad.wasm ${version} speaks protocol version ${protocolVersion}; this package reads version ${PROTOCOL_VERSION}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return kupcad;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @type {KupcadExports} */
|
|
76
|
+
#exports;
|
|
77
|
+
/** Where every export writes its result's length. */
|
|
78
|
+
#outLen;
|
|
79
|
+
/** Why the module can't be used again, once it stopped. @type {KupcadCrashError | null} */
|
|
80
|
+
#crashed = null;
|
|
81
|
+
|
|
82
|
+
/** @param {KupcadExports} exports an instantiated, initialized kupcad.wasm's */
|
|
83
|
+
constructor(exports) {
|
|
84
|
+
this.#exports = exports;
|
|
85
|
+
this.#outLen = address(exports.wasm_alloc(USIZE_BYTES));
|
|
86
|
+
if (this.#outLen === 0) throw new KupcadError('kupcad.wasm has no memory to start in');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The module's linear memory, in bytes: it grows as builds need, and stays grown. */
|
|
90
|
+
get memoryBytes() {
|
|
91
|
+
return this.#exports.memory.buffer.byteLength;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} source
|
|
96
|
+
* @param {PreviewOptions} [options]
|
|
97
|
+
* @returns {Promise<Preview>}
|
|
98
|
+
*/
|
|
99
|
+
async preview(source, options = {}) {
|
|
100
|
+
options.signal?.throwIfAborted();
|
|
101
|
+
const params = options.params ? JSON.stringify(options.params) : '';
|
|
102
|
+
const files = options.files ? packFiles(options.files) : NO_FILES;
|
|
103
|
+
return this.#run(() =>
|
|
104
|
+
readFrame(
|
|
105
|
+
this.#withInputs([source, params, files], ([s, p, f]) =>
|
|
106
|
+
this.#take(
|
|
107
|
+
this.#exports.preview_wasm(s.ptr, s.len, p.ptr, p.len, f.ptr, f.len, this.#outLen),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} source
|
|
116
|
+
* @param {BuildOptions} [options]
|
|
117
|
+
* @returns {Promise<Uint8Array>}
|
|
118
|
+
*/
|
|
119
|
+
async build(source, options = {}) {
|
|
120
|
+
options.signal?.throwIfAborted();
|
|
121
|
+
const params = options.params ? JSON.stringify(options.params) : '';
|
|
122
|
+
const files = options.files ? packFiles(options.files) : NO_FILES;
|
|
123
|
+
const format = options.format ?? DEFAULT_FORMAT;
|
|
124
|
+
const draco = options.draco ? 1 : 0;
|
|
125
|
+
return this.#run(() =>
|
|
126
|
+
this.#withInputs([source, params, files, format], ([s, p, fs, f]) =>
|
|
127
|
+
this.#take(
|
|
128
|
+
this.#exports.build_model_wasm(
|
|
129
|
+
s.ptr,
|
|
130
|
+
s.len,
|
|
131
|
+
p.ptr,
|
|
132
|
+
p.len,
|
|
133
|
+
fs.ptr,
|
|
134
|
+
fs.len,
|
|
135
|
+
f.ptr,
|
|
136
|
+
f.len,
|
|
137
|
+
draco,
|
|
138
|
+
this.#outLen,
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {string} source
|
|
147
|
+
* @param {CallOptions} [options]
|
|
148
|
+
* @returns {Promise<Finding[]>}
|
|
149
|
+
*/
|
|
150
|
+
async check(source, options = {}) {
|
|
151
|
+
options.signal?.throwIfAborted();
|
|
152
|
+
return this.#run(() => JSON.parse(this.#text(this.#exports.check_code_wasm, source)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {string} source
|
|
157
|
+
* @param {CallOptions} [options]
|
|
158
|
+
* @returns {Promise<string>}
|
|
159
|
+
*/
|
|
160
|
+
async format(source, options = {}) {
|
|
161
|
+
options.signal?.throwIfAborted();
|
|
162
|
+
return this.#run(() => this.#text(this.#exports.format_code_wasm, source));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {string} source
|
|
167
|
+
* @param {CallOptions} [options]
|
|
168
|
+
* @returns {Promise<UiSchema>}
|
|
169
|
+
*/
|
|
170
|
+
async params(source, options = {}) {
|
|
171
|
+
options.signal?.throwIfAborted();
|
|
172
|
+
return this.#run(() => JSON.parse(this.#text(this.#exports.extract_params_wasm, source)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @returns {Promise<VersionInfo>} */
|
|
176
|
+
async version() {
|
|
177
|
+
return this.#run(() =>
|
|
178
|
+
JSON.parse(decoder.decode(this.#take(this.#exports.version_wasm(this.#outLen)))),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Runs `work` unless the module stopped; a trap or an exit in it stops the module for good.
|
|
184
|
+
* @template T
|
|
185
|
+
* @param {() => T} work
|
|
186
|
+
* @returns {T}
|
|
187
|
+
*/
|
|
188
|
+
#run(work) {
|
|
189
|
+
if (this.#crashed) throw this.#crashed;
|
|
190
|
+
try {
|
|
191
|
+
return work();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (!(err instanceof WebAssembly.RuntimeError || err instanceof WasiExit)) throw err;
|
|
194
|
+
this.#crashed = new KupcadCrashError(`kupcad.wasm stopped: ${err.message}`, { cause: err });
|
|
195
|
+
throw this.#crashed;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The text an export of one source gives (check, format, params).
|
|
201
|
+
* @param {(source: number, sourceLen: number, outLen: number) => number} exported
|
|
202
|
+
* @param {string} source
|
|
203
|
+
* @returns {string}
|
|
204
|
+
*/
|
|
205
|
+
#text(exported, source) {
|
|
206
|
+
return decoder.decode(
|
|
207
|
+
this.#withInputs([source], ([s]) => this.#take(exported(s.ptr, s.len, this.#outLen))),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Copies `inputs` into the module's memory for `call`, and frees them after.
|
|
213
|
+
* @template T
|
|
214
|
+
* @param {(string | Uint8Array)[]} inputs
|
|
215
|
+
* @param {(placed: { ptr: number, len: number }[]) => T} call
|
|
216
|
+
* @returns {T}
|
|
217
|
+
*/
|
|
218
|
+
#withInputs(inputs, call) {
|
|
219
|
+
/** @type {{ ptr: number, len: number }[]} */
|
|
220
|
+
const placed = [];
|
|
221
|
+
try {
|
|
222
|
+
for (const input of inputs) {
|
|
223
|
+
const bytes = typeof input === 'string' ? encoder.encode(input) : input;
|
|
224
|
+
const ptr = address(this.#exports.wasm_alloc(bytes.byteLength));
|
|
225
|
+
if (ptr === 0)
|
|
226
|
+
throw new KupcadError(`kupcad.wasm has no room for ${bytes.byteLength} bytes`);
|
|
227
|
+
placed.push({ ptr, len: bytes.byteLength });
|
|
228
|
+
if (bytes.byteLength > 0) {
|
|
229
|
+
new Uint8Array(this.#exports.memory.buffer, ptr, bytes.byteLength).set(bytes);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return call(placed);
|
|
233
|
+
} finally {
|
|
234
|
+
for (const { ptr, len } of placed) this.#exports.wasm_free(ptr, len);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* An export's result, copied out of the module's memory and freed there; its null is the
|
|
240
|
+
* core's refusal.
|
|
241
|
+
* @param {number} result
|
|
242
|
+
* @returns {Uint8Array}
|
|
243
|
+
*/
|
|
244
|
+
#take(result) {
|
|
245
|
+
const ptr = address(result);
|
|
246
|
+
if (ptr === 0) throw new KupcadError(this.#lastError());
|
|
247
|
+
const len = new DataView(this.#exports.memory.buffer).getUint32(this.#outLen, true);
|
|
248
|
+
const bytes =
|
|
249
|
+
len > 0 ? new Uint8Array(this.#exports.memory.buffer, ptr, len).slice() : new Uint8Array();
|
|
250
|
+
this.#exports.wasm_free(ptr, len);
|
|
251
|
+
return bytes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#lastError() {
|
|
255
|
+
const at = address(this.#exports.get_last_error());
|
|
256
|
+
const bytes = new Uint8Array(this.#exports.memory.buffer, at, MAX_ERROR_BYTES);
|
|
257
|
+
const end = bytes.indexOf(0);
|
|
258
|
+
return decoder.decode(bytes.subarray(0, end === -1 ? MAX_ERROR_BYTES : end));
|
|
259
|
+
}
|
|
260
|
+
}
|
package/src/load.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The compiled module from what a host gives: a compiled WebAssembly.Module, its bytes, a
|
|
2
|
+
// fetch Response, or a URL (a `file:` one read from the disk in Node).
|
|
3
|
+
|
|
4
|
+
import { readFile } from '#read-file';
|
|
5
|
+
|
|
6
|
+
/** @import { WasmSource } from './types.js' */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {WasmSource} source
|
|
10
|
+
* @returns {Promise<WebAssembly.Module>}
|
|
11
|
+
*/
|
|
12
|
+
export async function loadModule(source) {
|
|
13
|
+
if (source instanceof WebAssembly.Module) return source;
|
|
14
|
+
if (source instanceof ArrayBuffer || ArrayBuffer.isView(source))
|
|
15
|
+
return WebAssembly.compile(source);
|
|
16
|
+
if (source instanceof Response) return compileResponse(source);
|
|
17
|
+
const url = new URL(source, globalThis.location?.href);
|
|
18
|
+
if (url.protocol === 'file:') return WebAssembly.compile(await readFile(url));
|
|
19
|
+
return compileResponse(await fetch(url));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compiles while it downloads when the server says it is wasm; compileStreaming refuses
|
|
24
|
+
* another content type, so then the bytes are compiled once they are all there.
|
|
25
|
+
* @param {Response} response
|
|
26
|
+
* @returns {Promise<WebAssembly.Module>}
|
|
27
|
+
*/
|
|
28
|
+
async function compileResponse(response) {
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`kupcad.wasm: ${response.status} ${response.statusText} from ${response.url}`);
|
|
31
|
+
}
|
|
32
|
+
if (response.headers.get('content-type')?.startsWith('application/wasm')) {
|
|
33
|
+
return WebAssembly.compileStreaming(response);
|
|
34
|
+
}
|
|
35
|
+
return WebAssembly.compile(await response.arrayBuffer());
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A browser can't read a `file:` URL (#read-file); a page gives the module by an http(s) URL,
|
|
2
|
+
// its bytes or a compiled WebAssembly.Module.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {URL} url
|
|
6
|
+
* @returns {Promise<Uint8Array<ArrayBuffer>>}
|
|
7
|
+
*/
|
|
8
|
+
export async function readFile(url) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`kupcad.wasm: a browser can't read ${url.href}; give the module by URL or as bytes`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Node's reader of a `file:` URL, for a module loaded from the disk (#read-file).
|
|
2
|
+
|
|
3
|
+
import { readFile as readFromDisk } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {URL} url
|
|
7
|
+
* @returns {Promise<Uint8Array<ArrayBuffer>>}
|
|
8
|
+
*/
|
|
9
|
+
export async function readFile(url) {
|
|
10
|
+
return readFromDisk(url);
|
|
11
|
+
}
|
package/src/serve.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The worker's side: the core instantiated in the worker, answering the client's calls on
|
|
2
|
+
// `port` in the order they come. A result's buffer is transferred, not copied.
|
|
3
|
+
|
|
4
|
+
import { createKupcad } from './kupcad.js';
|
|
5
|
+
import { KupcadCrashError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
/** @import { Preview } from '@kupcad/protocol/types' */
|
|
8
|
+
/** @import { CreateOptions, FromWorker, KupcadApi, Method, PortLike, SentError, ToWorker } from './types.js' */
|
|
9
|
+
|
|
10
|
+
/** @type {Record<Method, (kupcad: KupcadApi, args: any[]) => Promise<unknown>>} */
|
|
11
|
+
const CALLS = {
|
|
12
|
+
preview: (kupcad, [source, options]) => kupcad.preview(source, options),
|
|
13
|
+
build: (kupcad, [source, options]) => kupcad.build(source, options),
|
|
14
|
+
check: (kupcad, [source]) => kupcad.check(source),
|
|
15
|
+
format: (kupcad, [source]) => kupcad.format(source),
|
|
16
|
+
params: (kupcad, [source]) => kupcad.params(source),
|
|
17
|
+
version: (kupcad) => kupcad.version(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serves the core on `port`: an `init` message makes it (`create`, the module given), and each
|
|
22
|
+
* `call` is answered with a `reply`. The core's log goes as `log` messages.
|
|
23
|
+
* @param {PortLike} port
|
|
24
|
+
* @param {(options: CreateOptions) => Promise<KupcadApi>} [create]
|
|
25
|
+
*/
|
|
26
|
+
export function serve(port, create = createKupcad) {
|
|
27
|
+
/** @type {Promise<KupcadApi> | null} */
|
|
28
|
+
let kupcad = null;
|
|
29
|
+
/** @param {FromWorker} message @param {Transferable[]} [transfer] */
|
|
30
|
+
const send = (message, transfer = []) => port.postMessage(message, transfer);
|
|
31
|
+
|
|
32
|
+
/** @param {Extract<ToWorker, { type: 'init' }>} init */
|
|
33
|
+
const start = ({ wasm }) => create({ wasm, log: (line) => send({ type: 'log', line }) });
|
|
34
|
+
|
|
35
|
+
/** @param {Extract<ToWorker, { type: 'call' }>} call */
|
|
36
|
+
const answer = async ({ id, method, args }) => {
|
|
37
|
+
/** @type {KupcadApi} */
|
|
38
|
+
let ready;
|
|
39
|
+
try {
|
|
40
|
+
if (!kupcad) throw new Error('the kupcad worker was called before it was given its module');
|
|
41
|
+
ready = await kupcad;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
send({ type: 'reply', id, ok: false, error: sent(err, true) });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const value = await CALLS[method](ready, args);
|
|
48
|
+
send({ type: 'reply', id, ok: true, value }, transferOf(value));
|
|
49
|
+
} catch (err) {
|
|
50
|
+
send({ type: 'reply', id, ok: false, error: sent(err, err instanceof KupcadCrashError) });
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
port.addEventListener('message', ({ data }) => {
|
|
55
|
+
if (data.type === 'init') {
|
|
56
|
+
kupcad = start(data);
|
|
57
|
+
// A core that didn't start is told to each call, which awaits it
|
|
58
|
+
kupcad.catch(() => {});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
void answer(data);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {unknown} err
|
|
67
|
+
* @param {boolean} fatal
|
|
68
|
+
* @returns {SentError}
|
|
69
|
+
*/
|
|
70
|
+
function sent(err, fatal) {
|
|
71
|
+
if (err instanceof Error) return { name: err.name, message: err.message, fatal };
|
|
72
|
+
return { name: 'Error', message: String(err), fatal };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The buffers a result holds, to transfer: a build's bytes, a preview's GLB.
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @returns {Transferable[]}
|
|
79
|
+
*/
|
|
80
|
+
function transferOf(value) {
|
|
81
|
+
if (value instanceof Uint8Array) return [value.buffer];
|
|
82
|
+
const model = /** @type {Partial<Preview> | null} */ (value)?.model;
|
|
83
|
+
return model?.kind === '3d' ? [model.glb.buffer] : [];
|
|
84
|
+
}
|
package/src/testing.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// For the tests of a package that shows what the core makes: the real module, made once a test
|
|
2
|
+
// process and silent, and what it gives of a script. A script that fails fails the test.
|
|
3
|
+
|
|
4
|
+
import { createKupcad } from './kupcad.js';
|
|
5
|
+
|
|
6
|
+
/** @import { Model, UiSchema } from '@kupcad/protocol/types' */
|
|
7
|
+
/** @import { Kupcad } from './kupcad.js' */
|
|
8
|
+
|
|
9
|
+
/** @type {Promise<Kupcad> | null} */
|
|
10
|
+
let core = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The core, made on first use, its log dropped.
|
|
14
|
+
* @returns {Promise<Kupcad>}
|
|
15
|
+
*/
|
|
16
|
+
export function testCore() {
|
|
17
|
+
core ??= createKupcad({ log: () => {} });
|
|
18
|
+
return core;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The model the core's preview of `source` shows.
|
|
23
|
+
* @param {string} source
|
|
24
|
+
* @returns {Promise<Model>}
|
|
25
|
+
*/
|
|
26
|
+
export async function modelOf(source) {
|
|
27
|
+
const { header, model } = await (await testCore()).preview(source);
|
|
28
|
+
if (header.error) throw new Error(`the test's script failed: ${header.error.message}`);
|
|
29
|
+
return model;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The schema of `source`'s parameters.
|
|
34
|
+
* @param {string} source
|
|
35
|
+
* @returns {Promise<UiSchema>}
|
|
36
|
+
*/
|
|
37
|
+
export async function schemaOf(source) {
|
|
38
|
+
return (await testCore()).params(source);
|
|
39
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The shapes of @kupcad/wasm's API. What the core gives back is @kupcad/protocol's.
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ExportFormat,
|
|
5
|
+
Finding,
|
|
6
|
+
ParamValues,
|
|
7
|
+
Preview,
|
|
8
|
+
UiSchema,
|
|
9
|
+
VersionInfo,
|
|
10
|
+
} from '@kupcad/protocol/types';
|
|
11
|
+
|
|
12
|
+
/** Where the module comes from: compiled, its bytes, a fetch Response, or a URL. */
|
|
13
|
+
export type WasmSource = WebAssembly.Module | BufferSource | Response | URL | string;
|
|
14
|
+
|
|
15
|
+
/** What a call may be stopped by. */
|
|
16
|
+
export interface CallOptions {
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PreviewOptions extends CallOptions {
|
|
21
|
+
/** The parameters' values; those not given take their defaults. */
|
|
22
|
+
params?: ParamValues;
|
|
23
|
+
/**
|
|
24
|
+
* The files the script reads, by the paths it names them by (`import { } from "./lib.kup"`
|
|
25
|
+
* reads `lib.kup`, `import_json("data/size.json")` reads `data/size.json`): a module it
|
|
26
|
+
* imports, data it reads. The core keeps them for this call alone.
|
|
27
|
+
*/
|
|
28
|
+
files?: Record<string, Uint8Array | string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BuildOptions extends PreviewOptions {
|
|
32
|
+
/** Default 'glb'. */
|
|
33
|
+
format?: ExportFormat;
|
|
34
|
+
/** Draco-compressed meshes, for glb and gltf. */
|
|
35
|
+
draco?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CreateOptions {
|
|
39
|
+
/** The module; by default the kupcad.wasm this package ships. */
|
|
40
|
+
wasm?: WasmSource;
|
|
41
|
+
/** Where the module's log goes, a line at a time (a failure is thrown too); by default console.debug. */
|
|
42
|
+
log?: (line: string) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What both `createKupcad` and `createKupcadWorker` give. */
|
|
46
|
+
export interface KupcadApi {
|
|
47
|
+
/** The script's preview frame: a run that fails, or values refused, is a frame saying why. */
|
|
48
|
+
preview(source: string, options?: PreviewOptions): Promise<Preview>;
|
|
49
|
+
/** The script's model written as `format`; a KupcadError says why it couldn't be. */
|
|
50
|
+
build(source: string, options?: BuildOptions): Promise<Uint8Array>;
|
|
51
|
+
/** `kupcad check`'s findings, syntax errors among them. */
|
|
52
|
+
check(source: string, options?: CallOptions): Promise<Finding[]>;
|
|
53
|
+
/** The source as `kupcad fmt` writes it; a KupcadError for one that doesn't parse. */
|
|
54
|
+
format(source: string, options?: CallOptions): Promise<string>;
|
|
55
|
+
/** The parameters' schema, as `kupcad doc` gives it, without running the script. */
|
|
56
|
+
params(source: string, options?: CallOptions): Promise<UiSchema>;
|
|
57
|
+
/** What the module says of itself. */
|
|
58
|
+
version(): Promise<VersionInfo>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The exports of kupcad.wasm (core/src/wasm.zig). Pointers and lengths are byte offsets. */
|
|
62
|
+
export interface KupcadExports {
|
|
63
|
+
memory: WebAssembly.Memory;
|
|
64
|
+
_initialize(): void;
|
|
65
|
+
wasm_alloc(len: number): number;
|
|
66
|
+
wasm_free(ptr: number, len: number): void;
|
|
67
|
+
get_last_error(): number;
|
|
68
|
+
preview_wasm(
|
|
69
|
+
source: number,
|
|
70
|
+
sourceLen: number,
|
|
71
|
+
params: number,
|
|
72
|
+
paramsLen: number,
|
|
73
|
+
files: number,
|
|
74
|
+
filesLen: number,
|
|
75
|
+
outLen: number,
|
|
76
|
+
): number;
|
|
77
|
+
build_model_wasm(
|
|
78
|
+
source: number,
|
|
79
|
+
sourceLen: number,
|
|
80
|
+
params: number,
|
|
81
|
+
paramsLen: number,
|
|
82
|
+
files: number,
|
|
83
|
+
filesLen: number,
|
|
84
|
+
format: number,
|
|
85
|
+
formatLen: number,
|
|
86
|
+
draco: number,
|
|
87
|
+
outLen: number,
|
|
88
|
+
): number;
|
|
89
|
+
check_code_wasm(source: number, sourceLen: number, outLen: number): number;
|
|
90
|
+
format_code_wasm(source: number, sourceLen: number, outLen: number): number;
|
|
91
|
+
extract_params_wasm(source: number, sourceLen: number, outLen: number): number;
|
|
92
|
+
version_wasm(outLen: number): number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Where a worker's module comes from: what can be posted to it (not a Response). */
|
|
96
|
+
export type WorkerWasmSource = Exclude<WasmSource, Response>;
|
|
97
|
+
|
|
98
|
+
export interface WorkerOptions extends Omit<CreateOptions, 'wasm'> {
|
|
99
|
+
/** The module; by default the kupcad.wasm this package ships. A relative URL is the page's. */
|
|
100
|
+
wasm?: WorkerWasmSource;
|
|
101
|
+
/** Starts a worker running src/worker.js; by default a module Web Worker. */
|
|
102
|
+
worker?: () => WorkerLike;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** What a client calls the worker's core with: the methods of KupcadApi. */
|
|
106
|
+
export type Method = 'preview' | 'build' | 'check' | 'format' | 'params' | 'version';
|
|
107
|
+
|
|
108
|
+
/** A failure, as it crosses to the client: `fatal` when the worker's core can't go on. */
|
|
109
|
+
export interface SentError {
|
|
110
|
+
name: string;
|
|
111
|
+
message: string;
|
|
112
|
+
fatal: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ToWorker =
|
|
116
|
+
| {
|
|
117
|
+
type: 'init';
|
|
118
|
+
/** A URL as its href. */
|
|
119
|
+
wasm: Exclude<WorkerWasmSource, URL> | undefined;
|
|
120
|
+
}
|
|
121
|
+
| { type: 'call'; id: number; method: Method; args: unknown[] };
|
|
122
|
+
|
|
123
|
+
export type FromWorker =
|
|
124
|
+
| { type: 'reply'; id: number; ok: true; value: unknown }
|
|
125
|
+
| { type: 'reply'; id: number; ok: false; error: SentError }
|
|
126
|
+
| { type: 'log'; line: string };
|
|
127
|
+
|
|
128
|
+
/** The client's end of a worker: a Web Worker, or a stand-in in tests. */
|
|
129
|
+
export interface WorkerLike {
|
|
130
|
+
postMessage(message: ToWorker): void;
|
|
131
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<FromWorker>) => void): void;
|
|
132
|
+
addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
|
|
133
|
+
addEventListener(type: 'messageerror', listener: (event: MessageEvent) => void): void;
|
|
134
|
+
terminate(): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The worker's end: its global scope, or a MessagePort in tests. */
|
|
138
|
+
export interface PortLike {
|
|
139
|
+
postMessage(message: FromWorker, transfer?: Transferable[]): void;
|
|
140
|
+
addEventListener(type: 'message', listener: (event: MessageEvent<ToWorker>) => void): void;
|
|
141
|
+
}
|