@formepdf/renderer 0.7.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.
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from 'react';
2
+ import { type LayoutInfo } from '@formepdf/core';
3
+ export interface RenderOptions {
4
+ dataPath?: string;
5
+ data?: unknown;
6
+ pageSize?: {
7
+ width: number;
8
+ height: number;
9
+ };
10
+ }
11
+ export interface RenderResult {
12
+ pdf: Uint8Array;
13
+ layout: LayoutInfo;
14
+ renderTimeMs: number;
15
+ }
16
+ export declare function renderFromFile(filePath: string, options?: RenderOptions): Promise<RenderResult>;
17
+ export declare function renderFromCode(code: string, options?: RenderOptions): Promise<RenderResult>;
18
+ export declare function renderFromElement(element: ReactElement, options?: Pick<RenderOptions, 'pageSize'>): Promise<RenderResult>;
package/dist/render.js ADDED
@@ -0,0 +1,86 @@
1
+ import { writeFile, unlink } from 'node:fs/promises';
2
+ import { resolve, dirname, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { renderPdfWithLayout } from '@formepdf/core';
5
+ import { serialize as defaultSerialize } from '@formepdf/react';
6
+ import { bundleFile } from './bundle.js';
7
+ import { resolveElement } from './element.js';
8
+ import { resolveAllSources } from './resolve.js';
9
+ /// Full pipeline: bundle TSX file → resolve element → serialize → resolve assets → WASM render.
10
+ export async function renderFromFile(filePath, options) {
11
+ const absolutePath = resolve(filePath);
12
+ const code = await bundleFile(absolutePath);
13
+ return renderFromCode(code, {
14
+ ...options,
15
+ _basePath: dirname(absolutePath),
16
+ });
17
+ }
18
+ /// Render from pre-bundled ESM code string.
19
+ /// Handles the temp-file-and-import dance, then serializes and renders.
20
+ export async function renderFromCode(code, options) {
21
+ const start = performance.now();
22
+ const basePath = options?._basePath;
23
+ // Wrap the bundled code with a serialize re-export so it uses the same
24
+ // @formepdf/react instance as the template (avoids dual-instance issues
25
+ // when the renderer is bundled into a VS Code extension)
26
+ const wrappedCode = code + `\nexport { serialize as __formeSerialize } from '@formepdf/react';\n`;
27
+ // Write temp file in the source directory so Node resolves @formepdf/* from the user's node_modules
28
+ const tmpDir = basePath ?? process.cwd();
29
+ const tmpFile = join(tmpDir, `.forme-render-${Date.now()}.mjs`);
30
+ await writeFile(tmpFile, wrappedCode);
31
+ let mod;
32
+ try {
33
+ mod = await import(pathToFileURL(tmpFile).href);
34
+ }
35
+ finally {
36
+ await unlink(tmpFile).catch(() => { });
37
+ }
38
+ // Use the user's serialize if available (same React instance as the template)
39
+ const serializeFn = (typeof mod.__formeSerialize === 'function'
40
+ ? mod.__formeSerialize
41
+ : defaultSerialize);
42
+ const elementOpts = {};
43
+ if (options?.data !== undefined) {
44
+ elementOpts.data = options.data;
45
+ }
46
+ else if (options?.dataPath) {
47
+ elementOpts.dataPath = options.dataPath;
48
+ }
49
+ const element = await resolveElement(mod, elementOpts);
50
+ return renderFromElement(element, {
51
+ pageSize: options?.pageSize,
52
+ _basePath: basePath,
53
+ _renderStart: start,
54
+ _serialize: serializeFn,
55
+ });
56
+ }
57
+ /// Render from an already-resolved React element. Skips bundling entirely.
58
+ export async function renderFromElement(element, options) {
59
+ const start = options?._renderStart ?? performance.now();
60
+ const basePath = options?._basePath;
61
+ const serializeFn = options?._serialize ?? defaultSerialize;
62
+ const doc = serializeFn(element);
63
+ if (options?.pageSize) {
64
+ applyPageSizeOverride(doc, options.pageSize);
65
+ }
66
+ await resolveAllSources(doc, basePath);
67
+ const { pdf, layout } = await renderPdfWithLayout(JSON.stringify(doc));
68
+ const renderTimeMs = Math.round(performance.now() - start);
69
+ return { pdf, layout, renderTimeMs };
70
+ }
71
+ function applyPageSizeOverride(doc, size) {
72
+ const customSize = { Custom: { width: size.width, height: size.height } };
73
+ if (doc.defaultPage && typeof doc.defaultPage === 'object') {
74
+ doc.defaultPage.size = customSize;
75
+ }
76
+ if (Array.isArray(doc.children)) {
77
+ for (const child of doc.children) {
78
+ if (child && typeof child === 'object' && child.kind && typeof child.kind === 'object') {
79
+ const kind = child.kind;
80
+ if (kind.type === 'Page' && kind.config && typeof kind.config === 'object') {
81
+ kind.config.size = customSize;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,4 @@
1
+ export declare function uint8ArrayToBase64(bytes: Uint8Array): string;
2
+ export declare function resolveFontSources(doc: Record<string, unknown>, basePath?: string): Promise<void>;
3
+ export declare function resolveImageSources(doc: Record<string, unknown>): Promise<void>;
4
+ export declare function resolveAllSources(doc: Record<string, unknown>, basePath?: string): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ export function uint8ArrayToBase64(bytes) {
4
+ return Buffer.from(bytes).toString('base64');
5
+ }
6
+ /// Resolve font sources to base64 strings for the WASM engine.
7
+ /// File paths are resolved relative to `basePath` (defaults to cwd).
8
+ /// Uint8Array values are base64-encoded. Data URIs pass through as-is.
9
+ export async function resolveFontSources(doc, basePath) {
10
+ const fonts = doc.fonts;
11
+ if (!fonts?.length)
12
+ return;
13
+ const baseDir = basePath ?? process.cwd();
14
+ for (const font of fonts) {
15
+ if (font.src instanceof Uint8Array) {
16
+ font.src = uint8ArrayToBase64(font.src);
17
+ }
18
+ else if (typeof font.src === 'string' && !font.src.startsWith('data:')) {
19
+ const fontPath = resolve(baseDir, font.src);
20
+ const bytes = await readFile(fontPath);
21
+ font.src = uint8ArrayToBase64(new Uint8Array(bytes));
22
+ }
23
+ }
24
+ }
25
+ /// Resolve image sources — converts HTTP/HTTPS URLs to base64 data URIs.
26
+ /// Walks the document tree recursively.
27
+ export async function resolveImageSources(doc) {
28
+ const children = doc.children;
29
+ if (!children?.length)
30
+ return;
31
+ await Promise.all(children.map(resolveImageSourcesInNode));
32
+ }
33
+ async function resolveImageSourcesInNode(node) {
34
+ const kind = node.kind;
35
+ if (kind?.type === 'Image' && typeof kind.src === 'string') {
36
+ const src = kind.src;
37
+ if (src.startsWith('http://') || src.startsWith('https://')) {
38
+ const res = await fetch(src);
39
+ if (!res.ok)
40
+ throw new Error(`Failed to fetch image: ${src} (${res.status})`);
41
+ const contentType = res.headers.get('content-type') || 'image/png';
42
+ const buf = new Uint8Array(await res.arrayBuffer());
43
+ kind.src = `data:${contentType};base64,${uint8ArrayToBase64(buf)}`;
44
+ }
45
+ }
46
+ const children = node.children;
47
+ if (children?.length) {
48
+ await Promise.all(children.map(resolveImageSourcesInNode));
49
+ }
50
+ }
51
+ /// Resolve all asset sources (fonts + images) in parallel.
52
+ export async function resolveAllSources(doc, basePath) {
53
+ await Promise.all([
54
+ resolveFontSources(doc, basePath),
55
+ resolveImageSources(doc),
56
+ ]);
57
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@formepdf/renderer",
3
+ "version": "0.7.0",
4
+ "description": "File-to-PDF rendering pipeline for Forme — bundles TSX, resolves assets, renders via WASM",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./preview": "./dist/preview/index.html"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc && rm -rf dist/preview && cp -r src/preview dist/preview",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@formepdf/core": "0.7.0",
21
+ "@formepdf/react": "0.7.0",
22
+ "esbuild": "^0.24.0"
23
+ },
24
+ "peerDependencies": {
25
+ "react": "^18 || ^19"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "@types/react": "^19.0.0",
30
+ "react": "^19.0.0",
31
+ "typescript": "^5.7.0",
32
+ "vitest": "^3.0.0"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/formepdf/forme",
41
+ "directory": "packages/renderer"
42
+ }
43
+ }