typst-wasm 0.0.1-alpha
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/index.d.ts +186 -0
- package/dist/index.js +746 -0
- package/dist/index.js.map +1 -0
- package/dist/types-BoGtHbqQ.d.ts +95 -0
- package/dist/typst_wasm-Bjm0p5IN.js +372 -0
- package/dist/wasm.d.ts +14 -0
- package/dist/wasm.js +26 -0
- package/dist/wasm.js.map +1 -0
- package/dist/worker.js +200 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { loadWasmModule, toWasmCompileOptions } from "./wasm.js";
|
|
2
|
+
import { parseTarGzip } from "nanotar";
|
|
3
|
+
|
|
4
|
+
export * from "@typst-wasm/fonts"
|
|
5
|
+
|
|
6
|
+
//#region src/webassembly-jspi.ts
|
|
7
|
+
const getJspiWebAssembly = () => WebAssembly;
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/backend-support.ts
|
|
11
|
+
const supportsWorkerBackend = () => typeof Worker !== "undefined" && typeof SharedArrayBuffer !== "undefined" && typeof Atomics !== "undefined" && typeof Atomics.wait === "function";
|
|
12
|
+
const supportsJspiBackend = () => {
|
|
13
|
+
const wasm = getJspiWebAssembly();
|
|
14
|
+
return typeof wasm.Suspending === "function" && typeof wasm.promising === "function";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/errors.ts
|
|
19
|
+
var TypstError = class extends Error {
|
|
20
|
+
constructor(message, options) {
|
|
21
|
+
super(message, options);
|
|
22
|
+
this.name = new.target.name;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var CompileError = class extends TypstError {
|
|
26
|
+
diagnostics;
|
|
27
|
+
constructor(message, options = {}) {
|
|
28
|
+
super(message, { cause: options.cause });
|
|
29
|
+
this.diagnostics = options.diagnostics ?? [];
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var CompilerNotInitializedError = class extends TypstError {};
|
|
33
|
+
var CompilerDisposedError = class extends TypstError {};
|
|
34
|
+
var FontLoadError = class extends TypstError {
|
|
35
|
+
fontName;
|
|
36
|
+
constructor(fontName, cause) {
|
|
37
|
+
super(`Failed to load font "${fontName}"`, { cause });
|
|
38
|
+
this.fontName = fontName;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var FetchError = class extends TypstError {
|
|
42
|
+
path;
|
|
43
|
+
constructor(path, cause) {
|
|
44
|
+
super(`Failed to fetch "${path}"`, { cause });
|
|
45
|
+
this.path = path;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var PackageParseError = class extends TypstError {
|
|
49
|
+
spec;
|
|
50
|
+
constructor(spec, message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.spec = spec;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var PackageFetchError = class extends TypstError {
|
|
56
|
+
url;
|
|
57
|
+
constructor(url, cause) {
|
|
58
|
+
super(`Failed to fetch package: ${url}`, { cause });
|
|
59
|
+
this.url = url;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var FileNotFoundError = class extends TypstError {
|
|
63
|
+
filePath;
|
|
64
|
+
constructor(filePath) {
|
|
65
|
+
super(`File not found: ${filePath}`);
|
|
66
|
+
this.filePath = filePath;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var WorkerError = class extends TypstError {};
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/direct-service.ts
|
|
73
|
+
const MAX_FETCH_ATTEMPTS = 3;
|
|
74
|
+
const textDecoder = new TextDecoder();
|
|
75
|
+
const retry = async (task, maxAttempts) => {
|
|
76
|
+
let lastError;
|
|
77
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) try {
|
|
78
|
+
return await task();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
}
|
|
82
|
+
throw lastError ?? /* @__PURE__ */ new Error("Unknown host fetch error");
|
|
83
|
+
};
|
|
84
|
+
var DirectService = class {
|
|
85
|
+
disposed = false;
|
|
86
|
+
initPromise = null;
|
|
87
|
+
compiler = null;
|
|
88
|
+
wasmExports = null;
|
|
89
|
+
compileAsync = null;
|
|
90
|
+
constructor(packageManager, fetchImpl = fetch) {
|
|
91
|
+
this.packageManager = packageManager;
|
|
92
|
+
this.fetchImpl = fetchImpl;
|
|
93
|
+
}
|
|
94
|
+
async init(moduleOrPath) {
|
|
95
|
+
this.assertNotDisposed();
|
|
96
|
+
this.initPromise ??= this.initDirect(moduleOrPath);
|
|
97
|
+
await this.initPromise;
|
|
98
|
+
}
|
|
99
|
+
async dispose() {
|
|
100
|
+
this.disposed = true;
|
|
101
|
+
if (this.compiler) this.compiler.free();
|
|
102
|
+
this.compiler = null;
|
|
103
|
+
this.wasmExports = null;
|
|
104
|
+
this.compileAsync = null;
|
|
105
|
+
}
|
|
106
|
+
async addFont(data) {
|
|
107
|
+
this.withCompiler((compiler) => void compiler.add_font(data), "add_font");
|
|
108
|
+
}
|
|
109
|
+
async addFile(path, data) {
|
|
110
|
+
this.withCompiler((compiler) => void compiler.add_file(path, data), "add_file");
|
|
111
|
+
}
|
|
112
|
+
async addSource(path, text) {
|
|
113
|
+
this.withCompiler((compiler) => void compiler.add_source(path, text), "add_source");
|
|
114
|
+
}
|
|
115
|
+
async removeFile(path) {
|
|
116
|
+
this.withCompiler((compiler) => void compiler.remove_file(path), "remove_file");
|
|
117
|
+
}
|
|
118
|
+
async clearFiles() {
|
|
119
|
+
this.withCompiler((compiler) => void compiler.clear_files(), "clear_files");
|
|
120
|
+
}
|
|
121
|
+
async listFiles() {
|
|
122
|
+
return this.withCompiler((compiler) => compiler.list_files(), "list_files");
|
|
123
|
+
}
|
|
124
|
+
async hasFile(path) {
|
|
125
|
+
return this.withCompiler((compiler) => compiler.has_file(path), "has_file");
|
|
126
|
+
}
|
|
127
|
+
async setMain(path) {
|
|
128
|
+
this.withCompiler((compiler) => void compiler.set_main(path), "set_main");
|
|
129
|
+
}
|
|
130
|
+
async compile(options) {
|
|
131
|
+
this.assertNotDisposed();
|
|
132
|
+
const compiler = this.compiler;
|
|
133
|
+
const wasmExports = this.wasmExports;
|
|
134
|
+
const compile = this.compileAsync;
|
|
135
|
+
if (!compiler || !wasmExports || !compile) throw new CompilerNotInitializedError("Compiler not initialized");
|
|
136
|
+
let ret;
|
|
137
|
+
try {
|
|
138
|
+
ret = await compile(compiler.__wbg_ptr, options);
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
throw new WorkerError("Direct command failed: compile", { cause });
|
|
141
|
+
}
|
|
142
|
+
if (ret[2]) throw this.takeExternref(wasmExports, ret[1]);
|
|
143
|
+
return this.takeExternref(wasmExports, ret[0]);
|
|
144
|
+
}
|
|
145
|
+
async initDirect(moduleOrPath) {
|
|
146
|
+
if (!supportsJspiBackend()) throw new WorkerError("JSPI is unavailable in this runtime");
|
|
147
|
+
const wasmModule = await loadWasmModule();
|
|
148
|
+
const { Suspending, promising } = getJspiWebAssembly();
|
|
149
|
+
if (!Suspending || !promising) throw new WorkerError("JSPI is unavailable in this runtime");
|
|
150
|
+
const suspending = new Suspending(this.hostFetch);
|
|
151
|
+
const wasmExports = await wasmModule.default({
|
|
152
|
+
module_or_path: moduleOrPath,
|
|
153
|
+
imports: { bridge: { host_fetch: suspending } }
|
|
154
|
+
});
|
|
155
|
+
this.wasmExports = wasmExports;
|
|
156
|
+
this.compileAsync = promising(wasmExports.typstcompiler_compile);
|
|
157
|
+
this.compiler = new wasmModule.TypstCompiler();
|
|
158
|
+
}
|
|
159
|
+
hostFetch = async (pathPtr, pathLen, resultLenPtr) => {
|
|
160
|
+
const wasmExports = this.wasmExports;
|
|
161
|
+
if (!wasmExports) throw new Error("WASM exports not initialized");
|
|
162
|
+
const path = textDecoder.decode(new Uint8Array(wasmExports.memory.buffer, pathPtr, pathLen));
|
|
163
|
+
try {
|
|
164
|
+
const bytes = await retry(() => this.fetchBytes(path), MAX_FETCH_ATTEMPTS);
|
|
165
|
+
const resultPtr = wasmExports.__wbindgen_malloc(bytes.length, 1);
|
|
166
|
+
new Uint8Array(wasmExports.memory.buffer, resultPtr, bytes.length).set(bytes);
|
|
167
|
+
new DataView(wasmExports.memory.buffer).setUint32(resultLenPtr, bytes.length, true);
|
|
168
|
+
return resultPtr;
|
|
169
|
+
} catch {
|
|
170
|
+
new DataView(wasmExports.memory.buffer).setUint32(resultLenPtr, 0, true);
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
async fetchBytes(path) {
|
|
175
|
+
if (path.startsWith("@")) return this.packageManager.getFile(path);
|
|
176
|
+
const response = await this.fetchImpl(path);
|
|
177
|
+
if (!response.ok) throw new Error(`Failed to fetch "${path}" with status ${response.status}`);
|
|
178
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
179
|
+
}
|
|
180
|
+
withCompiler(run, name) {
|
|
181
|
+
this.assertNotDisposed();
|
|
182
|
+
if (!this.compiler) throw new CompilerNotInitializedError("Compiler not initialized");
|
|
183
|
+
try {
|
|
184
|
+
return run(this.compiler);
|
|
185
|
+
} catch (cause) {
|
|
186
|
+
throw new WorkerError(`Direct command failed: ${name}`, { cause });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
assertNotDisposed() {
|
|
190
|
+
if (this.disposed) throw new CompilerDisposedError("Compiler has been disposed");
|
|
191
|
+
}
|
|
192
|
+
takeExternref(wasmExports, idx) {
|
|
193
|
+
const value = wasmExports.__wbindgen_externrefs.get(idx);
|
|
194
|
+
wasmExports.__externref_table_dealloc(idx);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/worker.ts?worker
|
|
201
|
+
function WorkerWrapper(options) {
|
|
202
|
+
return new Worker(new URL("worker.js", import.meta.url), {
|
|
203
|
+
type: "module",
|
|
204
|
+
name: options?.name
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/messages.ts
|
|
210
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
211
|
+
const isRpcResponseMessage = (value) => {
|
|
212
|
+
if (!isRecord$1(value)) return false;
|
|
213
|
+
if (typeof value.requestId !== "number") return false;
|
|
214
|
+
return "result" in value !== "error" in value;
|
|
215
|
+
};
|
|
216
|
+
const isWorkerEventMessage = (value) => {
|
|
217
|
+
if (!isRecord$1(value)) return false;
|
|
218
|
+
if (typeof value.kind !== "string") return false;
|
|
219
|
+
if (value.kind === "web_fetch") {
|
|
220
|
+
if (!("payload" in value) || !isRecord$1(value.payload)) return false;
|
|
221
|
+
return typeof value.payload.path === "string";
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
const isWorkerToMainMessage = (value) => isRpcResponseMessage(value) || isWorkerEventMessage(value);
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/rpc.ts
|
|
229
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
230
|
+
const extractRpcErrorMessage = (error) => {
|
|
231
|
+
if (typeof error === "string") return error;
|
|
232
|
+
if (isRecord(error) && typeof error.message === "string") return error.message;
|
|
233
|
+
try {
|
|
234
|
+
return JSON.stringify(error);
|
|
235
|
+
} catch {
|
|
236
|
+
return String(error);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const makeRpcClient = (sender) => {
|
|
240
|
+
let requestIdCounter = 0;
|
|
241
|
+
const pending = /* @__PURE__ */ new Map();
|
|
242
|
+
const call = (kind, ...args) => {
|
|
243
|
+
const requestId = requestIdCounter;
|
|
244
|
+
requestIdCounter += 1;
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
pending.set(requestId, {
|
|
247
|
+
resolve,
|
|
248
|
+
reject,
|
|
249
|
+
kind
|
|
250
|
+
});
|
|
251
|
+
const payload = args[0];
|
|
252
|
+
try {
|
|
253
|
+
sender(payload === void 0 ? {
|
|
254
|
+
kind,
|
|
255
|
+
requestId
|
|
256
|
+
} : {
|
|
257
|
+
kind,
|
|
258
|
+
requestId,
|
|
259
|
+
payload
|
|
260
|
+
});
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
pending.delete(requestId);
|
|
263
|
+
reject(new WorkerError(`Failed to send worker command "${String(kind)}"`, { cause }));
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
const receive = (response) => {
|
|
268
|
+
if (!isRpcResponseMessage(response)) return;
|
|
269
|
+
const req = pending.get(response.requestId);
|
|
270
|
+
if (!req) return;
|
|
271
|
+
pending.delete(response.requestId);
|
|
272
|
+
if ("result" in response) req.resolve(response.result);
|
|
273
|
+
else req.reject(new WorkerError(`Worker command failed: ${req.kind}`, { cause: response.error ?? extractRpcErrorMessage(response.error) }));
|
|
274
|
+
};
|
|
275
|
+
const rejectAll = (cause) => {
|
|
276
|
+
for (const [requestId, req] of pending) {
|
|
277
|
+
pending.delete(requestId);
|
|
278
|
+
req.reject(new WorkerError(`Worker command failed: ${req.kind}`, { cause }));
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
call,
|
|
283
|
+
notify: sender,
|
|
284
|
+
receive,
|
|
285
|
+
rejectAll
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/protocol.ts
|
|
291
|
+
const INITIAL_SAB_SIZE = 1024 * 1024;
|
|
292
|
+
const MAX_SAB_SIZE = 4 * 1024 * 1024 * 1024;
|
|
293
|
+
const DEFAULT_FETCH_TIMEOUT = 3e4;
|
|
294
|
+
const SharedMemoryCommunicationStatus = {
|
|
295
|
+
None: 0,
|
|
296
|
+
Pending: 1,
|
|
297
|
+
Error: 2,
|
|
298
|
+
Success: 3
|
|
299
|
+
};
|
|
300
|
+
var SharedMemoryCommunication = class SharedMemoryCommunication {
|
|
301
|
+
dataBuf;
|
|
302
|
+
statusBuf;
|
|
303
|
+
sizeBuf;
|
|
304
|
+
constructor() {
|
|
305
|
+
this.dataBuf = new SharedArrayBuffer(INITIAL_SAB_SIZE, { maxByteLength: MAX_SAB_SIZE });
|
|
306
|
+
this.statusBuf = new SharedArrayBuffer(4);
|
|
307
|
+
this.sizeBuf = new SharedArrayBuffer(4);
|
|
308
|
+
}
|
|
309
|
+
getStatus() {
|
|
310
|
+
return Atomics.load(new Int32Array(this.statusBuf), 0);
|
|
311
|
+
}
|
|
312
|
+
setStatus(status) {
|
|
313
|
+
const statusView = new Int32Array(this.statusBuf);
|
|
314
|
+
Atomics.store(statusView, 0, status);
|
|
315
|
+
Atomics.notify(statusView, 0, 1);
|
|
316
|
+
}
|
|
317
|
+
setBuffer(buf) {
|
|
318
|
+
const needed = buf.byteLength;
|
|
319
|
+
if (needed > MAX_SAB_SIZE) throw new Error(`File too large: ${needed} bytes. Maximum allowed: ${MAX_SAB_SIZE} bytes.`);
|
|
320
|
+
if (needed > this.dataBuf.byteLength) this.dataBuf.grow(needed);
|
|
321
|
+
new Uint8Array(this.dataBuf).set(buf);
|
|
322
|
+
Atomics.store(new Int32Array(this.sizeBuf), 0, needed);
|
|
323
|
+
}
|
|
324
|
+
getBuffer() {
|
|
325
|
+
const size = Atomics.load(new Int32Array(this.sizeBuf), 0);
|
|
326
|
+
return new Uint8Array(this.dataBuf, 0, size);
|
|
327
|
+
}
|
|
328
|
+
waitForStatusChange(expectedStatus, timeoutMs = DEFAULT_FETCH_TIMEOUT) {
|
|
329
|
+
return Atomics.wait(new Int32Array(this.statusBuf), 0, expectedStatus, timeoutMs) === "ok";
|
|
330
|
+
}
|
|
331
|
+
static hydrateObj(obj) {
|
|
332
|
+
const instantiation = new SharedMemoryCommunication();
|
|
333
|
+
instantiation.dataBuf = obj.dataBuf;
|
|
334
|
+
instantiation.statusBuf = obj.statusBuf;
|
|
335
|
+
instantiation.sizeBuf = obj.sizeBuf;
|
|
336
|
+
return instantiation;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/fetch-bridge.ts
|
|
342
|
+
const makeFetchBridge = (packageLoader, isDisposed, fetchImpl = fetch) => {
|
|
343
|
+
const sharedMemoryCommunication = new SharedMemoryCommunication();
|
|
344
|
+
const handleFetchRequest = async (path) => {
|
|
345
|
+
if (isDisposed()) return;
|
|
346
|
+
try {
|
|
347
|
+
const bytes = path.startsWith("@") ? await packageLoader.getFile(path) : await fetchPath(path, fetchImpl);
|
|
348
|
+
if (isDisposed()) return;
|
|
349
|
+
sharedMemoryCommunication.setBuffer(bytes);
|
|
350
|
+
sharedMemoryCommunication.setStatus(SharedMemoryCommunicationStatus.Success);
|
|
351
|
+
} catch {
|
|
352
|
+
if (!isDisposed()) sharedMemoryCommunication.setStatus(SharedMemoryCommunicationStatus.Error);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
sharedMemoryCommunication,
|
|
357
|
+
handleFetchRequest
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
const fetchPath = async (path, fetchImpl) => {
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetchImpl(path);
|
|
363
|
+
if (!response.ok) throw new Error(`Status ${response.status}`);
|
|
364
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
365
|
+
} catch (cause) {
|
|
366
|
+
throw new FetchError(path, cause);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/worker-transport.ts
|
|
372
|
+
const makeWorkerTransport = (worker, onMessage, onError) => {
|
|
373
|
+
worker.onmessage = (event) => {
|
|
374
|
+
if (isWorkerToMainMessage(event.data)) onMessage(event.data);
|
|
375
|
+
};
|
|
376
|
+
if ("onerror" in worker) worker.onerror = (event) => {
|
|
377
|
+
onError(event.error ?? event.message);
|
|
378
|
+
};
|
|
379
|
+
return {
|
|
380
|
+
post: (message) => {
|
|
381
|
+
worker.postMessage(message);
|
|
382
|
+
},
|
|
383
|
+
close: () => {
|
|
384
|
+
worker.onmessage = null;
|
|
385
|
+
if ("onerror" in worker) worker.onerror = null;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/worker-service.ts
|
|
392
|
+
var WorkerService = class {
|
|
393
|
+
disposed = false;
|
|
394
|
+
initPromise = null;
|
|
395
|
+
worker;
|
|
396
|
+
rpcClient;
|
|
397
|
+
transport;
|
|
398
|
+
constructor(packageManager, fetchImpl = fetch) {
|
|
399
|
+
this.worker = new WorkerWrapper();
|
|
400
|
+
const fetchBridge = makeFetchBridge(packageManager, () => this.disposed, fetchImpl);
|
|
401
|
+
this.rpcClient = makeRpcClient((msg) => {
|
|
402
|
+
this.transport.post(msg);
|
|
403
|
+
});
|
|
404
|
+
this.transport = makeWorkerTransport(this.worker, (msg) => {
|
|
405
|
+
this.handleMessage(msg, fetchBridge.handleFetchRequest);
|
|
406
|
+
}, (cause) => {
|
|
407
|
+
this.rpcClient.rejectAll(cause);
|
|
408
|
+
});
|
|
409
|
+
this.initWorker = async (moduleOrPath) => {
|
|
410
|
+
await this.rpcClient.call("init", {
|
|
411
|
+
sharedMemoryCommunication: fetchBridge.sharedMemoryCommunication,
|
|
412
|
+
moduleOrPath
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
initWorker;
|
|
417
|
+
async init(moduleOrPath) {
|
|
418
|
+
this.assertNotDisposed();
|
|
419
|
+
this.initPromise ??= this.initWorker(moduleOrPath);
|
|
420
|
+
try {
|
|
421
|
+
await this.initPromise;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.initPromise = null;
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async dispose() {
|
|
428
|
+
if (this.disposed) return;
|
|
429
|
+
this.disposed = true;
|
|
430
|
+
this.rpcClient.rejectAll(new CompilerDisposedError("Compiler has been disposed"));
|
|
431
|
+
this.transport.close();
|
|
432
|
+
this.worker.terminate();
|
|
433
|
+
}
|
|
434
|
+
addFont(data) {
|
|
435
|
+
return this.rpcClient.call("add_font", { data });
|
|
436
|
+
}
|
|
437
|
+
addFile(path, data) {
|
|
438
|
+
return this.rpcClient.call("add_file", {
|
|
439
|
+
path,
|
|
440
|
+
data
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
addSource(path, text) {
|
|
444
|
+
return this.rpcClient.call("add_source", {
|
|
445
|
+
path,
|
|
446
|
+
text
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
removeFile(path) {
|
|
450
|
+
return this.rpcClient.call("remove_file", { path });
|
|
451
|
+
}
|
|
452
|
+
clearFiles() {
|
|
453
|
+
return this.rpcClient.call("clear_files");
|
|
454
|
+
}
|
|
455
|
+
listFiles() {
|
|
456
|
+
return this.rpcClient.call("list_files");
|
|
457
|
+
}
|
|
458
|
+
hasFile(path) {
|
|
459
|
+
return this.rpcClient.call("has_file", { path });
|
|
460
|
+
}
|
|
461
|
+
setMain(path) {
|
|
462
|
+
return this.rpcClient.call("set_main", { path });
|
|
463
|
+
}
|
|
464
|
+
compile(options) {
|
|
465
|
+
return this.rpcClient.call("compile", { options });
|
|
466
|
+
}
|
|
467
|
+
async handleMessage(msg, handleFetchRequest) {
|
|
468
|
+
if (isRpcResponseMessage(msg)) {
|
|
469
|
+
this.rpcClient.receive(msg);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
switch (msg.kind) {
|
|
473
|
+
case "web_fetch":
|
|
474
|
+
await handleFetchRequest(msg.payload.path);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
assertNotDisposed() {
|
|
479
|
+
if (this.disposed) throw new CompilerDisposedError("Compiler has been disposed");
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/compiler-backend.ts
|
|
485
|
+
const selectAutomaticBackendKind = () => {
|
|
486
|
+
if (supportsWorkerBackend()) return "worker";
|
|
487
|
+
if (supportsJspiBackend()) return "jspi";
|
|
488
|
+
return "none";
|
|
489
|
+
};
|
|
490
|
+
const createCompilerBackend = (backend, options) => {
|
|
491
|
+
switch (backend === "auto" ? selectAutomaticBackendKind() : backend) {
|
|
492
|
+
case "worker": return new WorkerService(options.packageManager, options.fetch);
|
|
493
|
+
case "jspi": return new DirectService(options.packageManager, options.fetch);
|
|
494
|
+
case "none": throw new Error("No compatible typst-wasm backend available. Requires Worker+SharedArrayBuffer+Atomics.wait or JSPI.");
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/cache-abstraction.ts
|
|
500
|
+
const makeBrowserCacheStorage = () => {
|
|
501
|
+
let cachePromise = null;
|
|
502
|
+
const openCache = async () => {
|
|
503
|
+
cachePromise ??= caches.open("typst-packages").catch(() => null);
|
|
504
|
+
return cachePromise;
|
|
505
|
+
};
|
|
506
|
+
return {
|
|
507
|
+
async get(key) {
|
|
508
|
+
const cache = await openCache();
|
|
509
|
+
if (!cache) return null;
|
|
510
|
+
try {
|
|
511
|
+
const response = await cache.match(key);
|
|
512
|
+
if (!response) return null;
|
|
513
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
514
|
+
} catch {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
async set(key, value) {
|
|
519
|
+
const cache = await openCache();
|
|
520
|
+
if (!cache) return;
|
|
521
|
+
try {
|
|
522
|
+
const response = new Response(value.slice(), { headers: { "Content-Type": "application/octet-stream" } });
|
|
523
|
+
await cache.put(key, response);
|
|
524
|
+
} catch {}
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
const makeMemoryCacheStorage = (capacity) => {
|
|
529
|
+
const storage = /* @__PURE__ */ new Map();
|
|
530
|
+
return {
|
|
531
|
+
async get(key) {
|
|
532
|
+
const value = storage.get(key);
|
|
533
|
+
if (!value) return null;
|
|
534
|
+
storage.delete(key);
|
|
535
|
+
storage.set(key, value);
|
|
536
|
+
return value;
|
|
537
|
+
},
|
|
538
|
+
async set(key, value) {
|
|
539
|
+
if (storage.has(key)) storage.delete(key);
|
|
540
|
+
storage.set(key, value);
|
|
541
|
+
while (storage.size > capacity) {
|
|
542
|
+
const oldest = storage.keys().next().value;
|
|
543
|
+
if (!oldest) break;
|
|
544
|
+
storage.delete(oldest);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
};
|
|
549
|
+
const makeDefaultPackageCache = (capacity = 400) => typeof caches !== "undefined" ? makeBrowserCacheStorage() : makeMemoryCacheStorage(capacity);
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/package-manager.ts
|
|
553
|
+
const parseSpec = (spec) => {
|
|
554
|
+
const match = spec.match(/^@([a-z0-9-]+)\/([a-z0-9_-]+):([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.-]+)?)\/(.+)$/);
|
|
555
|
+
if (!match) throw new PackageParseError(spec, "Expected format: @namespace/name:version/path where namespace is lowercase alphanumeric with hyphens, name is lowercase alphanumeric with hyphens/underscores, version is semver, and path is the file path.");
|
|
556
|
+
const [, namespace, name, version, filePath] = match;
|
|
557
|
+
if (namespace.startsWith("-") || namespace.endsWith("-")) throw new PackageParseError(spec, `Invalid package namespace: "${namespace}" cannot start or end with hyphen`);
|
|
558
|
+
if (name.startsWith("_") || name.endsWith("_")) throw new PackageParseError(spec, `Invalid package name: "${name}" cannot start or end with underscore`);
|
|
559
|
+
return {
|
|
560
|
+
namespace,
|
|
561
|
+
name,
|
|
562
|
+
version,
|
|
563
|
+
filePath
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const getFileCacheKey = (spec, filePath) => `@${spec.namespace}/${spec.name}:${spec.version}/${filePath}`;
|
|
567
|
+
const getCacheKey = (spec) => getFileCacheKey(spec, spec.filePath);
|
|
568
|
+
const getPackageKey = (spec) => `@${spec.namespace}/${spec.name}:${spec.version}`;
|
|
569
|
+
var PackageManager = class {
|
|
570
|
+
fetchImpl;
|
|
571
|
+
packageBaseUrl;
|
|
572
|
+
cache;
|
|
573
|
+
loadedPackages = /* @__PURE__ */ new Set();
|
|
574
|
+
loadingPackages = /* @__PURE__ */ new Map();
|
|
575
|
+
constructor(options = {}) {
|
|
576
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
577
|
+
this.packageBaseUrl = options.packageBaseUrl ?? "https://packages.typst.org";
|
|
578
|
+
this.cache = options.cache ?? makeDefaultPackageCache(options.memoryPackageCacheCapacity);
|
|
579
|
+
}
|
|
580
|
+
async getFile(spec) {
|
|
581
|
+
const parsed = parseSpec(spec);
|
|
582
|
+
const cacheKey = getCacheKey(parsed);
|
|
583
|
+
const cached = await this.cache.get(cacheKey);
|
|
584
|
+
if (cached) return cached;
|
|
585
|
+
const packageKey = getPackageKey(parsed);
|
|
586
|
+
if (!this.loadedPackages.has(packageKey)) await this.loadPackageDeduped(parsed);
|
|
587
|
+
const file = await this.cache.get(cacheKey);
|
|
588
|
+
if (!file) throw new FileNotFoundError(parsed.filePath);
|
|
589
|
+
return file;
|
|
590
|
+
}
|
|
591
|
+
async loadPackageDeduped(spec) {
|
|
592
|
+
const packageKey = getPackageKey(spec);
|
|
593
|
+
const existing = this.loadingPackages.get(packageKey);
|
|
594
|
+
if (existing) {
|
|
595
|
+
await existing;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const load = this.loadPackage(spec);
|
|
599
|
+
this.loadingPackages.set(packageKey, load);
|
|
600
|
+
try {
|
|
601
|
+
await load;
|
|
602
|
+
this.loadedPackages.add(packageKey);
|
|
603
|
+
} finally {
|
|
604
|
+
this.loadingPackages.delete(packageKey);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async loadPackage(spec) {
|
|
608
|
+
const url = `${this.packageBaseUrl}/${spec.namespace}/${spec.name}-${spec.version}.tar.gz`;
|
|
609
|
+
try {
|
|
610
|
+
const response = await this.fetchImpl(url);
|
|
611
|
+
if (!response.ok) throw new Error(`Status ${response.status}`);
|
|
612
|
+
const files = await parseTarGzip(new Uint8Array(await response.arrayBuffer()));
|
|
613
|
+
await Promise.all(files.map(async (file) => {
|
|
614
|
+
if (file.type === "file" && file.data) await this.cache.set(getFileCacheKey(spec, file.name), file.data);
|
|
615
|
+
}));
|
|
616
|
+
} catch (cause) {
|
|
617
|
+
throw new PackageFetchError(url, cause);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/index.ts
|
|
624
|
+
const hasErrorDiagnostics = (diagnostics) => diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
625
|
+
const extractErrorMessage = (error) => {
|
|
626
|
+
if (error instanceof Error) return error.message;
|
|
627
|
+
if (typeof error === "string") return error;
|
|
628
|
+
try {
|
|
629
|
+
return JSON.stringify(error);
|
|
630
|
+
} catch {
|
|
631
|
+
return String(error);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const normalizeCompileResult = (result) => {
|
|
635
|
+
const diagnostics = result.diagnostics;
|
|
636
|
+
const deps = result.deps ?? void 0;
|
|
637
|
+
const timings = result.timings;
|
|
638
|
+
switch (result.format) {
|
|
639
|
+
case "pdf": return {
|
|
640
|
+
format: "pdf",
|
|
641
|
+
output: result.output_bytes ?? new Uint8Array(),
|
|
642
|
+
diagnostics,
|
|
643
|
+
deps,
|
|
644
|
+
timings
|
|
645
|
+
};
|
|
646
|
+
case "png": return {
|
|
647
|
+
format: "png",
|
|
648
|
+
pages: result.pages.map((page) => ({
|
|
649
|
+
page: page.page,
|
|
650
|
+
output: page.output_bytes ?? new Uint8Array()
|
|
651
|
+
})),
|
|
652
|
+
diagnostics,
|
|
653
|
+
deps,
|
|
654
|
+
timings
|
|
655
|
+
};
|
|
656
|
+
case "svg": return {
|
|
657
|
+
format: "svg",
|
|
658
|
+
pages: result.pages.map((page) => ({
|
|
659
|
+
page: page.page,
|
|
660
|
+
output: page.output_text ?? ""
|
|
661
|
+
})),
|
|
662
|
+
diagnostics,
|
|
663
|
+
deps,
|
|
664
|
+
timings
|
|
665
|
+
};
|
|
666
|
+
case "html": return {
|
|
667
|
+
format: "html",
|
|
668
|
+
output: result.output_text ?? "",
|
|
669
|
+
diagnostics,
|
|
670
|
+
deps,
|
|
671
|
+
timings
|
|
672
|
+
};
|
|
673
|
+
case "bundle": return {
|
|
674
|
+
format: "bundle",
|
|
675
|
+
files: result.files.map((file) => ({
|
|
676
|
+
path: file.path,
|
|
677
|
+
data: file.data,
|
|
678
|
+
mediaType: file.media_type ?? void 0
|
|
679
|
+
})),
|
|
680
|
+
diagnostics,
|
|
681
|
+
deps,
|
|
682
|
+
timings
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
var PromiseTypstCompiler = class {
|
|
687
|
+
constructor(backend) {
|
|
688
|
+
this.backend = backend;
|
|
689
|
+
}
|
|
690
|
+
addFont(data) {
|
|
691
|
+
return this.backend.addFont(data);
|
|
692
|
+
}
|
|
693
|
+
addFile(path, data) {
|
|
694
|
+
return this.backend.addFile(path, data);
|
|
695
|
+
}
|
|
696
|
+
addSource(path, text) {
|
|
697
|
+
return this.backend.addSource(path, text);
|
|
698
|
+
}
|
|
699
|
+
removeFile(path) {
|
|
700
|
+
return this.backend.removeFile(path);
|
|
701
|
+
}
|
|
702
|
+
clearFiles() {
|
|
703
|
+
return this.backend.clearFiles();
|
|
704
|
+
}
|
|
705
|
+
listFiles() {
|
|
706
|
+
return this.backend.listFiles();
|
|
707
|
+
}
|
|
708
|
+
hasFile(path) {
|
|
709
|
+
return this.backend.hasFile(path);
|
|
710
|
+
}
|
|
711
|
+
setMain(path) {
|
|
712
|
+
return this.backend.setMain(path);
|
|
713
|
+
}
|
|
714
|
+
async compile(options = {}) {
|
|
715
|
+
if (options.main) await this.setMain(options.main);
|
|
716
|
+
let rawResult;
|
|
717
|
+
try {
|
|
718
|
+
rawResult = await this.backend.compile(toWasmCompileOptions(options));
|
|
719
|
+
} catch (cause) {
|
|
720
|
+
throw new CompileError(extractErrorMessage(cause), { cause });
|
|
721
|
+
}
|
|
722
|
+
if (hasErrorDiagnostics(rawResult.diagnostics) || !rawResult.success) throw new CompileError(rawResult.internal_error ?? "Compilation failed", { diagnostics: rawResult.diagnostics });
|
|
723
|
+
return normalizeCompileResult(rawResult);
|
|
724
|
+
}
|
|
725
|
+
dispose() {
|
|
726
|
+
return this.backend.dispose();
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
const createTypstCompiler = async (options) => {
|
|
730
|
+
const packageManager = new PackageManager({
|
|
731
|
+
fetch: options.fetch,
|
|
732
|
+
packageBaseUrl: options.packageBaseUrl,
|
|
733
|
+
cache: options.packageCache,
|
|
734
|
+
memoryPackageCacheCapacity: options.memoryPackageCacheCapacity
|
|
735
|
+
});
|
|
736
|
+
const backend = createCompilerBackend(options.backend ?? "auto", {
|
|
737
|
+
packageManager,
|
|
738
|
+
fetch: options.fetch
|
|
739
|
+
});
|
|
740
|
+
await backend.init(options.moduleOrPath);
|
|
741
|
+
return new PromiseTypstCompiler(backend);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
//#endregion
|
|
745
|
+
export { CompileError, CompilerDisposedError, CompilerNotInitializedError, FetchError, FileNotFoundError, FontLoadError, PackageFetchError, PackageManager, PackageParseError, SharedMemoryCommunication, TypstError, WorkerError, createTypstCompiler, makeBrowserCacheStorage, makeDefaultPackageCache, makeMemoryCacheStorage, selectAutomaticBackendKind, supportsJspiBackend, supportsWorkerBackend };
|
|
746
|
+
//# sourceMappingURL=index.js.map
|