wasm-html-to-image 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/browser-workers.d.ts +12 -0
  4. package/dist/browser-workers.js +26 -0
  5. package/dist/child-workers.d.ts +11 -0
  6. package/dist/child-workers.js +19 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +87 -0
  9. package/dist/core.d.ts +168 -0
  10. package/dist/core.js +824 -0
  11. package/dist/diagnostics.d.ts +93 -0
  12. package/dist/diagnostics.js +28 -0
  13. package/dist/edge-light.d.ts +18 -0
  14. package/dist/edge-light.js +70 -0
  15. package/dist/encode-args.d.ts +10 -0
  16. package/dist/encode-args.js +30 -0
  17. package/dist/html-to-image-single.js +0 -0
  18. package/dist/html-to-image-wasm.js +3 -0
  19. package/dist/html-to-image.js +2 -0
  20. package/dist/html-to-image.wasm +0 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +14 -0
  23. package/dist/input.d.ts +23 -0
  24. package/dist/input.js +90 -0
  25. package/dist/loader.d.ts +132 -0
  26. package/dist/loader.js +199 -0
  27. package/dist/preact.d.ts +10 -0
  28. package/dist/preact.js +13 -0
  29. package/dist/react.d.ts +9 -0
  30. package/dist/react.js +11 -0
  31. package/dist/single.d.ts +27 -0
  32. package/dist/single.js +22 -0
  33. package/dist/tailwind.d.ts +1 -0
  34. package/dist/tailwind.js +6 -0
  35. package/dist/wasm-payload.d.ts +2 -0
  36. package/dist/wasm-payload.js +39 -0
  37. package/dist/web-workers.js +4411 -0
  38. package/dist/worker-actions.d.ts +10 -0
  39. package/dist/worker-actions.js +13 -0
  40. package/dist/worker-lib-loader.d.ts +19 -0
  41. package/dist/worker-lib-loader.js +34 -0
  42. package/dist/workerd.d.ts +18 -0
  43. package/dist/workerd.js +65 -0
  44. package/dist/workers-dummy.d.ts +12 -0
  45. package/dist/workers-dummy.js +6 -0
  46. package/dist/workers-parent.js +602 -0
  47. package/dist/workers.d.ts +86 -0
  48. package/dist/workers.js +163 -0
  49. package/package.json +166 -0
package/dist/loader.js ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Single-WASM loader for the unified `html-to-image` Emscripten module.
3
+ *
4
+ * Build assumptions (see `src/cpp/main.cpp`, C++ lane owns them):
5
+ * - Emscripten `MODULARIZE` + `EXPORT_ES6` → `dist/html-to-image.js`
6
+ * with `export default createHtmlToImageModule`.
7
+ * - Single `EMSCRIPTEN_BINDINGS(html_to_image)` with `satoru_*` /
8
+ * `converter_*` prefixed JS names (no bare `create_instance` etc.).
9
+ *
10
+ * Registered bindings: instance lifecycle + log level, render/encode
11
+ * (`satoru_render`, `converter_encode`, ...), resource discovery/injection
12
+ * (`satoru_collect_resources`, `satoru_get_pending_resources`,
13
+ * `satoru_add_resource`, `satoru_load_font`, `satoru_load_fallback_font`,
14
+ * `satoru_set_font_map`, ...), and the unified `html_to_image` call.
15
+ * Bindings added over time stay optional in the type below; callers must
16
+ * feature-check at runtime (see `./core.ts`).
17
+ *
18
+ * Node and browser share this minimal shape: dynamic `import()` of the glue
19
+ * file URL, then invoking the factory. No `node:` builtins are used so the
20
+ * same file works in both runtimes.
21
+ */
22
+ /**
23
+ * Process-wide module cache, keyed by factory then moduleArg: different
24
+ * factories, or different WASM payloads (e.g. workerd/edge-light calls
25
+ * with distinct `WebAssembly.Module`s), must not share an instance.
26
+ * Callers with their own scoping (workerd/edge-light per-wasm maps,
27
+ * single.ts default path, `*-workers` lazy vars) are unaffected.
28
+ */
29
+ const moduleCache = new Map();
30
+ /** Default glue URL: `./html-to-image.js` next to the compiled loader. */
31
+ export function defaultGlueUrl() {
32
+ return new URL("./html-to-image.js", import.meta.url);
33
+ }
34
+ function isModuleNotFound(e) {
35
+ const code = e?.code;
36
+ if (code === "ERR_MODULE_NOT_FOUND")
37
+ return true;
38
+ const msg = e instanceof Error ? e.message : String(e);
39
+ return /Cannot find (module|package)/.test(msg);
40
+ }
41
+ /**
42
+ * Import glue by URL, tolerating bundlers that hijack dynamic `import()`.
43
+ * Webpack rewrites even variable `import(url)` into chunk loading, which
44
+ * cannot resolve runtime URLs ("Cannot find module"). The native attempt
45
+ * runs first (unchanged behavior everywhere it already works, including
46
+ * strict-CSP pages); only on failure is an indirect import (invisible to
47
+ * bundlers' static analysis) retried. The original error is rethrown when
48
+ * both attempts fail, preserving existing error semantics (e.g. the
49
+ * src→dist dev fallback branching below).
50
+ */
51
+ async function importGlueModule(glue) {
52
+ // `webpackIgnore` keeps Turbopack (Next.js 16 default builder) from trying
53
+ // to resolve the runtime URL at build time; webpack/vite honor it too.
54
+ try {
55
+ return (await import(
56
+ /* @vite-ignore, webpackIgnore: true */ glue));
57
+ }
58
+ catch (first) {
59
+ try {
60
+ const indirect = new Function("u", "return import(u)");
61
+ return await indirect(glue);
62
+ }
63
+ catch {
64
+ throw first;
65
+ }
66
+ }
67
+ }
68
+ /** `file://` directory URL to a plain fs path (browser URLs pass through). */
69
+ function glueDirToFsPath(glueDir) {
70
+ if (!glueDir.startsWith("file://"))
71
+ return glueDir;
72
+ let p = decodeURIComponent(glueDir.slice("file://".length));
73
+ if (/^\/[A-Za-z]:\//.test(p))
74
+ p = p.slice(1); // Windows drive letter
75
+ return p;
76
+ }
77
+ function isNodeRuntime() {
78
+ const proc = globalThis
79
+ .process;
80
+ return typeof proc?.versions?.node === "string";
81
+ }
82
+ /**
83
+ * Runtime-only load of a `node:` builtin (e.g. `importNode("fs/promises")`).
84
+ * Uses `process.getBuiltinModule` (Node 22.3+/20.16+) so there is NO `import`
85
+ * syntax at all: bundlers with static analysis (webpack/vite/rollup, and
86
+ * Turbopack — the Next.js 16 default builder, which rejects both dynamic
87
+ * `` `node:${name}` `` requests and literal `import("node:*")` inside Edge
88
+ * graphs) have nothing to resolve or rewrite. Only the builtins actually
89
+ * used by this package are supported. All call sites are guarded to run on
90
+ * Node only; other runtimes never evaluate these branches. A `new Function`
91
+ * indirect-import fallback keeps older Node working without adding any
92
+ * statically analyzable import.
93
+ */
94
+ export function importNode(name) {
95
+ if (name !== "fs/promises" && name !== "path" && name !== "module") {
96
+ throw new Error(`wasm-html-to-image: unsupported node builtin "${name}"`);
97
+ }
98
+ const proc = globalThis.process;
99
+ try {
100
+ const mod = proc?.getBuiltinModule?.(`node:${name}`);
101
+ if (mod)
102
+ return Promise.resolve(mod);
103
+ }
104
+ catch {
105
+ // fall through to the indirect dynamic import below
106
+ }
107
+ const indirect = new Function("s", "return import(s)");
108
+ return indirect(`node:${name}`);
109
+ }
110
+ /**
111
+ * Node-only: pre-read the sibling `.wasm` bytes. The web/worker-oriented
112
+ * glue has no `fs` reader, so `file://` fetching fails under Node; handing
113
+ * over `wasmBinary` bypasses fetching entirely. Browsers skip this (fetch
114
+ * works there). No static `node:` import keeps browser bundles clean.
115
+ */
116
+ async function readWasmBinaryNode(glueDir) {
117
+ if (!isNodeRuntime())
118
+ return undefined;
119
+ try {
120
+ const fs = await importNode("fs/promises");
121
+ const data = await fs.readFile(glueDirToFsPath(glueDir) + "html-to-image.wasm");
122
+ return new Uint8Array(data);
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ /**
129
+ * Load and instantiate the unified module (cached per process, keyed by
130
+ * factory then moduleArg — see `moduleCache`).
131
+ * Works in Node (file URL import) and browsers (served URL import).
132
+ */
133
+ export async function loadHtmlToImageModule(options = {}) {
134
+ const cacheKeyFactory = options.factory ?? undefined;
135
+ const cacheKeyModuleArg = options.moduleArg ?? undefined;
136
+ if (!options.noCache) {
137
+ const hit = moduleCache.get(cacheKeyFactory)?.get(cacheKeyModuleArg);
138
+ if (hit)
139
+ return hit;
140
+ }
141
+ let factory = options.factory;
142
+ let glueLabel = "(pre-bundled factory)";
143
+ let glueDir = "";
144
+ if (!factory) {
145
+ let glue = String(options.glueUrl ?? defaultGlueUrl());
146
+ let ns;
147
+ try {
148
+ ns = await importGlueModule(glue);
149
+ }
150
+ catch (e) {
151
+ if (options.glueUrl || !isModuleNotFound(e))
152
+ throw e;
153
+ // Dev-layout fallback: running from `src/` (tsx) while the glue
154
+ // lives in `../dist/` next to the compiled loader.
155
+ const fallback = String(new URL("../dist/html-to-image.js", import.meta.url));
156
+ try {
157
+ ns = await importGlueModule(fallback);
158
+ }
159
+ catch {
160
+ throw e; // report the original error
161
+ }
162
+ glue = fallback;
163
+ }
164
+ glueLabel = glue;
165
+ glueDir = glue.slice(0, glue.lastIndexOf("/") + 1);
166
+ factory = ns.default ?? ns.createHtmlToImageModule;
167
+ }
168
+ if (typeof factory !== "function") {
169
+ throw new Error(`wasm-html-to-image: no MODULARIZE factory in ${glueLabel} ` +
170
+ `(expected default export createHtmlToImageModule)`);
171
+ }
172
+ // Node-only: hand over pre-read `.wasm` bytes (factory path, e.g.
173
+ // workerd with `instantiateWasm`, is left untouched).
174
+ let moduleArg = options.moduleArg;
175
+ if (!options.factory && glueDir && moduleArg?.wasmBinary === undefined) {
176
+ const wasmBinary = await readWasmBinaryNode(glueDir);
177
+ if (wasmBinary)
178
+ moduleArg = { ...moduleArg, wasmBinary };
179
+ }
180
+ const module = await factory({
181
+ ...moduleArg,
182
+ locateFile: options.locateFile ??
183
+ moduleArg?.locateFile ??
184
+ ((path) => glueDir + path),
185
+ });
186
+ if (!options.noCache) {
187
+ let inner = moduleCache.get(cacheKeyFactory);
188
+ if (!inner) {
189
+ inner = new Map();
190
+ moduleCache.set(cacheKeyFactory, inner);
191
+ }
192
+ inner.set(cacheKeyModuleArg, module);
193
+ }
194
+ return module;
195
+ }
196
+ /** Drop all cached instances (tests / HMR). */
197
+ export function dropCachedModule() {
198
+ moduleCache.clear();
199
+ }
@@ -0,0 +1,10 @@
1
+ import { h, type VNode } from "preact";
2
+ export { h };
3
+ /**
4
+ * Converts a React element to a static HTML string.
5
+ * This is a thin wrapper around react-dom/server's renderToStaticMarkup.
6
+ *
7
+ * @param element - The React element to convert.
8
+ * @returns The resulting HTML string.
9
+ */
10
+ export declare function toHtml(element: VNode): string;
package/dist/preact.js ADDED
@@ -0,0 +1,13 @@
1
+ import { h } from "preact";
2
+ import render from "preact-render-to-string";
3
+ export { h };
4
+ /**
5
+ * Converts a React element to a static HTML string.
6
+ * This is a thin wrapper around react-dom/server's renderToStaticMarkup.
7
+ *
8
+ * @param element - The React element to convert.
9
+ * @returns The resulting HTML string.
10
+ */
11
+ export function toHtml(element) {
12
+ return render(element);
13
+ }
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from "react";
2
+ /**
3
+ * Converts a React element to a static HTML string.
4
+ * This is a thin wrapper around react-dom/server's renderToStaticMarkup.
5
+ *
6
+ * @param element - The React element to convert.
7
+ * @returns The resulting HTML string.
8
+ */
9
+ export declare function toHtml(element: ReactNode): string;
package/dist/react.js ADDED
@@ -0,0 +1,11 @@
1
+ import { renderToStaticMarkup } from "react-dom/server";
2
+ /**
3
+ * Converts a React element to a static HTML string.
4
+ * This is a thin wrapper around react-dom/server's renderToStaticMarkup.
5
+ *
6
+ * @param element - The React element to convert.
7
+ * @returns The resulting HTML string.
8
+ */
9
+ export function toHtml(element) {
10
+ return renderToStaticMarkup(element);
11
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Single-WASM entry point.
3
+ *
4
+ * Loads the unified `html-to-image` module once (local `./loader.js`)
5
+ * and reuses the single connection for all calls.
6
+ * `render()` accepts HTML (`string` / `string[]` / URL) or image input
7
+ * (`Uint8Array` / `ArrayBuffer` / `data:image/` URL); image input skips
8
+ * rendering and goes straight to `converter_encode`.
9
+ */
10
+ import { type HtmlToImageModule } from "./loader.js";
11
+ import { type HtmlToImageOptions, type RenderResult } from "./core.js";
12
+ import { isImageInput } from "./input.js";
13
+ export type { OutputFormat, RenderOptions, HtmlToImageOptions, RenderResult, DiagnosticMessage, FontDiagnostic, RenderDiagnostics, RenderLimits, ResolveResourceHook, ResourceDiagnostic, } from "./core.js";
14
+ export { DIAGNOSTIC_CODES, LogLevel, htmlToImage } from "./core.js";
15
+ export { isImageInput };
16
+ export type { HtmlToImageModule } from "./loader.js";
17
+ export { dropCachedModule, loadHtmlToImageModule } from "./loader.js";
18
+ /** Default connection: thin delegation to the loader-owned cache. */
19
+ export declare function getDefaultModule(): Promise<HtmlToImageModule>;
20
+ /**
21
+ * Render HTML — or convert an image — with the default single-WASM
22
+ * connection. Returns RenderResult containing data and metadata.
23
+ */
24
+ export declare function render(options: HtmlToImageOptions & {
25
+ format: "svg";
26
+ }): Promise<RenderResult<string>>;
27
+ export declare function render(options: HtmlToImageOptions): Promise<RenderResult<Uint8Array | string>>;
package/dist/single.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Single-WASM entry point.
3
+ *
4
+ * Loads the unified `html-to-image` module once (local `./loader.js`)
5
+ * and reuses the single connection for all calls.
6
+ * `render()` accepts HTML (`string` / `string[]` / URL) or image input
7
+ * (`Uint8Array` / `ArrayBuffer` / `data:image/` URL); image input skips
8
+ * rendering and goes straight to `converter_encode`.
9
+ */
10
+ import { loadHtmlToImageModule } from "./loader.js";
11
+ import { htmlToImage, } from "./core.js";
12
+ import { isImageInput } from "./input.js";
13
+ export { DIAGNOSTIC_CODES, LogLevel, htmlToImage } from "./core.js";
14
+ export { isImageInput };
15
+ export { dropCachedModule, loadHtmlToImageModule } from "./loader.js";
16
+ /** Default connection: thin delegation to the loader-owned cache. */
17
+ export function getDefaultModule() {
18
+ return loadHtmlToImageModule();
19
+ }
20
+ export async function render(options) {
21
+ return htmlToImage(await getDefaultModule(), options);
22
+ }
@@ -0,0 +1 @@
1
+ export declare const createCSS: (html: string) => Promise<string>;
@@ -0,0 +1,6 @@
1
+ import { createGenerator } from "@unocss/core";
2
+ import presetUno from "@unocss/preset-wind4";
3
+ export const createCSS = async (html) => {
4
+ const uno = await createGenerator({ presets: [presetUno()] });
5
+ return uno.generate(html).then((v) => v.css);
6
+ };
@@ -0,0 +1,2 @@
1
+ /** Decompress the embedded WASM payload and return the raw bytes. */
2
+ export declare function loadCompressedWasmBinary(): Promise<Uint8Array>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Compressed embedded WASM payload.
3
+ *
4
+ * `scripts/compress-wasm.mjs` compresses `dist/html-to-image.wasm`
5
+ * (gzip vs brotli, smaller wins) and writes the base64 payload to
6
+ * `dist/html-to-image-wasm.js`. This loader restores the raw bytes at
7
+ * runtime via `DecompressionStream`, so the browser worker bundle embeds
8
+ * ~4MB instead of the 12MB SINGLE_FILE glue.
9
+ */
10
+ // @ts-expect-error — dist/html-to-image-wasm.js is generated at build time
11
+ import { wasmBase64, wasmEncoding } from "../dist/html-to-image-wasm.js";
12
+ /** Decompress the embedded WASM payload and return the raw bytes. */
13
+ export async function loadCompressedWasmBinary() {
14
+ const encoding = wasmEncoding;
15
+ try {
16
+ // gzip only: the W3C Compression Standard (DecompressionStream) has no
17
+ // "br" — see scripts/compress-wasm.mjs.
18
+ if (encoding !== "gzip") {
19
+ throw new Error(`unsupported wasmEncoding ${JSON.stringify(encoding)}`);
20
+ }
21
+ const bin = atob(wasmBase64);
22
+ const compressed = new Uint8Array(bin.length);
23
+ for (let i = 0; i < bin.length; i++)
24
+ compressed[i] = bin.charCodeAt(i);
25
+ const stream = new Blob([compressed])
26
+ .stream()
27
+ .pipeThrough(new DecompressionStream(encoding));
28
+ const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
29
+ if (bytes.length === 0) {
30
+ throw new Error("decompressed to 0 bytes");
31
+ }
32
+ return bytes;
33
+ }
34
+ catch (e) {
35
+ throw new Error(`wasm-html-to-image: failed to restore embedded WASM ` +
36
+ `(encoding=${JSON.stringify(encoding)}): ` +
37
+ (e instanceof Error ? e.message : String(e)));
38
+ }
39
+ }