@lofcz/tikz 0.2.0 → 0.2.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/NOTICE.md +2 -1
- package/README.md +8 -0
- package/package.json +3 -2
- package/runtime/dist/core.d.ts +3 -1
- package/runtime/dist/core.js +18 -7
- package/runtime/dist/index.js +21 -7
- package/runtime/dist/types.d.ts +3 -0
- package/src/browser-worker.js +2 -2
- package/src/worker.js +2 -2
package/NOTICE.md
CHANGED
|
@@ -16,7 +16,8 @@ Only the ten upstream default bundles are shipped. Optional Classico (AFPL),
|
|
|
16
16
|
Ghostscript, latex-doc, OpenType and otf-fonts bundles are excluded. No Ghostscript
|
|
17
17
|
engine is included. Native binaries and macro contents are unchanged. Bundle payloads are Brotli-compressed
|
|
18
18
|
without changing their contents. Our Node I/O decompresses them on demand and
|
|
19
|
-
uses portable file URL handling.
|
|
19
|
+
uses portable file URL handling. Engine glue supports lazy optional module loading
|
|
20
|
+
and TeX-only execution without initializing MetaPost. Browser I/O still uses uncompressed assets.
|
|
20
21
|
|
|
21
22
|
Corresponding upstream source, patches and reproducible build instructions:
|
|
22
23
|
https://github.com/jmckalex/mp-tikz-wasm/tree/d44aeeba98b0e60d13c35dfd080076e6b05c68b5
|
package/README.md
CHANGED
|
@@ -28,6 +28,12 @@ are accepted unchanged. Snippets receive a standalone wrapper and the standard
|
|
|
28
28
|
calc, intersections, angles, quotes, arrows.meta and positioning libraries.
|
|
29
29
|
pgfplots is available through `\usepackage{pgfplots}` in a complete document.
|
|
30
30
|
|
|
31
|
+
Both entries enable the fork's lazy engine loading: TikZ skips MetaPost WASM,
|
|
32
|
+
optional glue loads only when used, and graph-drawing documents automatically
|
|
33
|
+
select LuaTeX. Each document still gets a fresh TeX/WASM execution state. These
|
|
34
|
+
savings do not require cross-request caches, including on Vercel Fluid Node
|
|
35
|
+
functions. The server entry keeps CPU-bound TeX outside the shared request loop.
|
|
36
|
+
|
|
31
37
|
## Limits and cancellation
|
|
32
38
|
|
|
33
39
|
One page; 12,000 source characters; 30 seconds per execution and queue wait;
|
|
@@ -96,6 +102,8 @@ the full package including `runtime/`; copying only JavaScript is insufficient.
|
|
|
96
102
|
```sh
|
|
97
103
|
npm ci
|
|
98
104
|
npm run check
|
|
105
|
+
npm run test:browser
|
|
106
|
+
npm run bench:cold
|
|
99
107
|
```
|
|
100
108
|
|
|
101
109
|
The engine source lives in `packages/engine`, imported from upstream v0.5.0 as a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lofcz/tikz",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Self-contained, process-isolated TikZ to SVG rendering powered by mp-tikz-wasm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"check": "npm run build && npm test && npm run test:package",
|
|
48
48
|
"prepack": "npm run build",
|
|
49
49
|
"build:engine": "tsc -p packages/engine/tsconfig.build.json",
|
|
50
|
-
"test:browser": "node test/browser.mjs"
|
|
50
|
+
"test:browser": "node test/browser.mjs",
|
|
51
|
+
"bench:cold": "node test/bench-cold.mjs"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/node": "^22.20.5",
|
package/runtime/dist/core.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ export declare class MetaPostCore {
|
|
|
45
45
|
build: string;
|
|
46
46
|
};
|
|
47
47
|
private userFiles;
|
|
48
|
+
private mplibReady?;
|
|
48
49
|
/**
|
|
49
50
|
* luaotfload's font cache (TEXMF_VAR), carried from one LuaTeX run to the
|
|
50
51
|
* next. Parsing an OpenType face costs about a second, and every TeX run
|
|
@@ -53,7 +54,8 @@ export declare class MetaPostCore {
|
|
|
53
54
|
private texmfVar;
|
|
54
55
|
private get logger();
|
|
55
56
|
constructor(env: CoreEnv);
|
|
56
|
-
init(): Promise<void>;
|
|
57
|
+
init(eager?: boolean): Promise<void>;
|
|
58
|
+
private initMetaPost;
|
|
57
59
|
private progress;
|
|
58
60
|
/** The errors and warnings of a finished run, one record each; a MetaPost help paragraph is indented under its error. */
|
|
59
61
|
private report;
|
package/runtime/dist/core.js
CHANGED
|
@@ -69,7 +69,8 @@ export class MetaPostCore {
|
|
|
69
69
|
cache = new MemorySnippetCache();
|
|
70
70
|
bridge;
|
|
71
71
|
version = { metapost: '', tex: 'pdfTeX 1.40.27 (DVI)', build: '' };
|
|
72
|
-
userFiles = new
|
|
72
|
+
userFiles = new Map();
|
|
73
|
+
mplibReady;
|
|
73
74
|
/**
|
|
74
75
|
* luaotfload's font cache (TEXMF_VAR), carried from one LuaTeX run to the
|
|
75
76
|
* next. Parsing an OpenType face costs about a second, and every TeX run
|
|
@@ -80,7 +81,13 @@ export class MetaPostCore {
|
|
|
80
81
|
constructor(env) {
|
|
81
82
|
this.env = env;
|
|
82
83
|
}
|
|
83
|
-
async init() {
|
|
84
|
+
async init(eager = !this.env.options.lazy) {
|
|
85
|
+
if (!eager)
|
|
86
|
+
return;
|
|
87
|
+
this.mplibReady ??= this.initMetaPost();
|
|
88
|
+
await this.mplibReady;
|
|
89
|
+
}
|
|
90
|
+
async initMetaPost() {
|
|
84
91
|
const o = this.env.options;
|
|
85
92
|
this.progress({ phase: 'loading', detail: 'mplib.wasm' });
|
|
86
93
|
// mplib's own stdout/stderr (dvitomp, the runtime) and, through the termLine hook, MetaPost's terminal
|
|
@@ -96,6 +103,8 @@ export class MetaPostCore {
|
|
|
96
103
|
mkdirp(FS, '/work');
|
|
97
104
|
this.installTexmf(FS, this.M.NODEFS);
|
|
98
105
|
FS.chdir('/work');
|
|
106
|
+
for (const [path, bytes] of this.userFiles)
|
|
107
|
+
writeFileDeep(FS, path, bytes);
|
|
99
108
|
mpxAddPath(this.M, `${TEXMF_ROOT}/fonts/tfm`);
|
|
100
109
|
mpxAddPath(this.M, `${TEXMF_ROOT}/fonts/vf`);
|
|
101
110
|
this.M.mpwasmHooks = {
|
|
@@ -193,9 +202,9 @@ export class MetaPostCore {
|
|
|
193
202
|
}
|
|
194
203
|
copyUserFiles(T) {
|
|
195
204
|
// TeX may \input files the user supplied next to the job
|
|
196
|
-
for (const p of this.userFiles) {
|
|
205
|
+
for (const [p, bytes] of this.userFiles) {
|
|
197
206
|
try {
|
|
198
|
-
writeFileDeep(T.FS, p, this.M.FS.readFile(p, { encoding: 'binary' }));
|
|
207
|
+
writeFileDeep(T.FS, p, this.M ? this.M.FS.readFile(p, { encoding: 'binary' }) : bytes);
|
|
199
208
|
}
|
|
200
209
|
catch { /* ignore */ }
|
|
201
210
|
}
|
|
@@ -288,8 +297,9 @@ export class MetaPostCore {
|
|
|
288
297
|
addFiles(files) {
|
|
289
298
|
for (const [name, data] of Object.entries(files)) {
|
|
290
299
|
const p = name.startsWith('/') ? name : `/work/${name}`;
|
|
291
|
-
|
|
292
|
-
|
|
300
|
+
if (this.M)
|
|
301
|
+
writeFileDeep(this.M.FS, p, data);
|
|
302
|
+
this.userFiles.set(p, typeof data === 'string' ? data : data.slice());
|
|
293
303
|
}
|
|
294
304
|
}
|
|
295
305
|
/**
|
|
@@ -298,7 +308,7 @@ export class MetaPostCore {
|
|
|
298
308
|
* typesetting one document after another would hand the first one's .aux to the next.
|
|
299
309
|
*/
|
|
300
310
|
removeFiles(names) {
|
|
301
|
-
const paths = names ? names.map((n) => (n.startsWith('/') ? n : `/work/${n}`)) : [...this.userFiles];
|
|
311
|
+
const paths = names ? names.map((n) => (n.startsWith('/') ? n : `/work/${n}`)) : [...this.userFiles.keys()];
|
|
302
312
|
for (const p of paths) {
|
|
303
313
|
if (!this.userFiles.delete(p))
|
|
304
314
|
continue;
|
|
@@ -310,6 +320,7 @@ export class MetaPostCore {
|
|
|
310
320
|
}
|
|
311
321
|
clearCache() { this.cache.clear(); }
|
|
312
322
|
async run(source, ro = {}) {
|
|
323
|
+
await this.init(true);
|
|
313
324
|
const o = this.env.options;
|
|
314
325
|
const t0 = now();
|
|
315
326
|
this.stats = { totalMs: 0, metapostMs: 0, texMs: 0, texRuns: 0, metapostRuns: 0, snippetCacheHits: 0, snippetCacheMisses: 0 };
|
package/runtime/dist/index.js
CHANGED
|
@@ -34,16 +34,22 @@ class InProcessBackend {
|
|
|
34
34
|
this.bundles.logger = this.logger;
|
|
35
35
|
const base = o.bundleBaseUrl ?? new URL('./bundles/', here).href;
|
|
36
36
|
await this.bundles.addAll(resolveBundleSpecs(o.bundles ?? DEFAULT_BUNDLES, base));
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if (!o.lazy || !this.bundles.canFetchSync)
|
|
38
|
+
await this.bundles.prefetchEager();
|
|
39
|
+
if (!o.lazy)
|
|
40
|
+
await this.bundles.loadHot((base.endsWith('/') ? base : base + '/') + 'hot.json');
|
|
39
41
|
if (!this.bundles.canFetchSync)
|
|
40
42
|
await this.bundles.prefetchAll();
|
|
41
43
|
}
|
|
42
|
-
const
|
|
44
|
+
const lazyFactory = (name) => async (opts) => {
|
|
45
|
+
const module = await import(/* @vite-ignore */ new URL(`./${name}.mjs`, here).href);
|
|
46
|
+
return module.default(opts);
|
|
47
|
+
};
|
|
48
|
+
const mplibFactory = o.modules?.mplib ?? (o.lazy ? lazyFactory('mplib') : (await import(/* @vite-ignore */ new URL('./mplib.mjs', here).href)).default);
|
|
43
49
|
let texFactory = o.modules?.tex;
|
|
44
50
|
if (!texFactory && (o.tex ?? 'auto') !== 'none') {
|
|
45
51
|
try {
|
|
46
|
-
texFactory = (await import(/* @vite-ignore */ new URL('./tex.mjs', here).href)).default;
|
|
52
|
+
texFactory = o.lazy ? lazyFactory('tex') : (await import(/* @vite-ignore */ new URL('./tex.mjs', here).href)).default;
|
|
47
53
|
}
|
|
48
54
|
catch {
|
|
49
55
|
texFactory = undefined;
|
|
@@ -52,7 +58,7 @@ class InProcessBackend {
|
|
|
52
58
|
let dvisvgmFactory = o.modules?.dvisvgm;
|
|
53
59
|
if (!dvisvgmFactory && texFactory) {
|
|
54
60
|
try {
|
|
55
|
-
dvisvgmFactory = (await import(/* @vite-ignore */ new URL('./dvisvgm.mjs', here).href)).default;
|
|
61
|
+
dvisvgmFactory = o.lazy ? lazyFactory('dvisvgm') : (await import(/* @vite-ignore */ new URL('./dvisvgm.mjs', here).href)).default;
|
|
56
62
|
}
|
|
57
63
|
catch {
|
|
58
64
|
dvisvgmFactory = undefined;
|
|
@@ -62,7 +68,7 @@ class InProcessBackend {
|
|
|
62
68
|
let luatexFactory = o.modules?.luatex;
|
|
63
69
|
if (!luatexFactory && texFactory) {
|
|
64
70
|
try {
|
|
65
|
-
luatexFactory = (await import(/* @vite-ignore */ new URL('./luatex.mjs', here).href)).default;
|
|
71
|
+
luatexFactory = o.lazy ? lazyFactory('luatex') : (await import(/* @vite-ignore */ new URL('./luatex.mjs', here).href)).default;
|
|
66
72
|
}
|
|
67
73
|
catch {
|
|
68
74
|
luatexFactory = undefined;
|
|
@@ -104,7 +110,15 @@ class InProcessBackend {
|
|
|
104
110
|
await this.bundles.add(spec);
|
|
105
111
|
await this.bundles.prefetchAll((f) => bundles.includes(f.bundle));
|
|
106
112
|
}
|
|
107
|
-
async prefetch(kinds) {
|
|
113
|
+
async prefetch(kinds) {
|
|
114
|
+
if (!this.bundles)
|
|
115
|
+
return 0;
|
|
116
|
+
if (!Object.keys(this.bundles.hot).length) {
|
|
117
|
+
const base = this.options.bundleBaseUrl ?? new URL('./bundles/', import.meta.url).href;
|
|
118
|
+
await this.bundles.loadHot((base.endsWith('/') ? base : base + '/') + 'hot.json');
|
|
119
|
+
}
|
|
120
|
+
return this.bundles.prefetchHot(kinds);
|
|
121
|
+
}
|
|
108
122
|
setLogLevel(level) { this.logger.level = level; }
|
|
109
123
|
dispose() { }
|
|
110
124
|
}
|
package/runtime/dist/types.d.ts
CHANGED
|
@@ -98,6 +98,9 @@ export interface MetaPostOptions {
|
|
|
98
98
|
};
|
|
99
99
|
/** Run in-process instead of in a Web Worker (default: worker in browsers, in-process in Node). */
|
|
100
100
|
worker?: boolean;
|
|
101
|
+
/** Defer engine glue and MetaPost WASM until used. MetaPost version fields in creation
|
|
102
|
+
* metadata remain empty in this mode. No cross-request cache is required. */
|
|
103
|
+
lazy?: boolean;
|
|
101
104
|
/** Default for latex()'s snapshot option. Node: 'auto' (tikz.fmt from local files is free). Browser: 'none',
|
|
102
105
|
* because the 5.8 MB format does not compress and costs more to download than the files it replaces
|
|
103
106
|
* (2.5 MB gzipped); set 'auto' when many TikZ figures amortise it or the format is already cached. */
|
package/src/browser-worker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MetaPost } from '../runtime/dist/index.js';
|
|
2
2
|
|
|
3
3
|
try {
|
|
4
|
-
const engine = await MetaPost.create({ worker: false, logLevel: 'silent' });
|
|
4
|
+
const engine = await MetaPost.create({ worker: false, lazy: true, logLevel: 'silent' });
|
|
5
5
|
self.onmessage = async ({ data: { source: code, maxOutputBytes } }) => {
|
|
6
6
|
try {
|
|
7
7
|
const source = /^\s*\\documentclass\b/m.test(code) ? code : String.raw`\documentclass[tikz,border=6pt]{standalone}
|
|
@@ -9,7 +9,7 @@ try {
|
|
|
9
9
|
\begin{document}
|
|
10
10
|
${code}
|
|
11
11
|
\end{document}`;
|
|
12
|
-
const result = await engine.latex(source, { fonts: 'paths' });
|
|
12
|
+
const result = await engine.latex(source, { engine: 'auto', fonts: 'paths' });
|
|
13
13
|
if (result.status === 'error' || result.status === 'fatal' || result.pages?.length !== 1)
|
|
14
14
|
throw new Error('TikZ requires one valid page. ' + JSON.stringify(result.diagnostics).slice(0, 2000));
|
|
15
15
|
const svg = result.pages[0], bytes = new TextEncoder().encode(svg);
|
package/src/worker.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { MetaPost } from '../runtime/dist/index.js';
|
|
5
5
|
const { source } = JSON.parse(readFileSync(0, 'utf8'));
|
|
6
|
-
const engine = await MetaPost.create({ worker: false, logLevel: 'silent' });
|
|
6
|
+
const engine = await MetaPost.create({ worker: false, lazy: true, logLevel: 'silent' });
|
|
7
7
|
try {
|
|
8
|
-
const result = await engine.latex(source, { fonts: 'paths' });
|
|
8
|
+
const result = await engine.latex(source, { engine: 'auto', fonts: 'paths' });
|
|
9
9
|
if (result.status === 'error' || result.status === 'fatal' || result.pages?.length !== 1)
|
|
10
10
|
throw new Error('TikZ requires one valid page. ' + JSON.stringify(result.diagnostics).slice(0, 2000));
|
|
11
11
|
process.stdout.write(JSON.stringify({ svg: result.pages[0] }));
|