@formepdf/cli 0.6.2 → 0.7.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/dist/build.js +7 -70
- package/dist/dev.js +21 -159
- package/dist/template-build.js +4 -23
- package/package.json +3 -5
package/dist/build.js
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve
|
|
3
|
-
import {
|
|
4
|
-
import { isValidElement } from 'react';
|
|
5
|
-
import { bundleFile, BUNDLE_DIR } from './bundle.js';
|
|
6
|
-
import { renderDocumentWithLayout } from '@formepdf/core';
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { renderFromFile } from '@formepdf/renderer';
|
|
7
4
|
export async function buildPdf(inputPath, options) {
|
|
8
5
|
const absoluteInput = resolve(inputPath);
|
|
9
6
|
console.log(`Building ${absoluteInput}...`);
|
|
10
7
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const mod = await import(pathToFileURL(tmpFile).href);
|
|
17
|
-
const element = await resolveElement(mod, options.dataPath);
|
|
18
|
-
const { pdf } = await renderDocumentWithLayout(element);
|
|
19
|
-
const outputPath = resolve(options.output);
|
|
20
|
-
await writeFile(outputPath, pdf);
|
|
21
|
-
console.log(`Written ${pdf.length} bytes to ${outputPath}`);
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
// Clean up temp file
|
|
25
|
-
const { unlink } = await import('node:fs/promises');
|
|
26
|
-
await unlink(tmpFile).catch(() => { });
|
|
27
|
-
}
|
|
8
|
+
const { pdf } = await renderFromFile(absoluteInput, { dataPath: options.dataPath });
|
|
9
|
+
const outputPath = resolve(options.output);
|
|
10
|
+
await writeFile(outputPath, pdf);
|
|
11
|
+
console.log(`Written ${pdf.length} bytes to ${outputPath}`);
|
|
28
12
|
}
|
|
29
13
|
catch (err) {
|
|
30
14
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -32,50 +16,3 @@ export async function buildPdf(inputPath, options) {
|
|
|
32
16
|
process.exit(1);
|
|
33
17
|
}
|
|
34
18
|
}
|
|
35
|
-
async function resolveElement(mod, dataPath) {
|
|
36
|
-
const exported = mod.default;
|
|
37
|
-
if (exported === undefined) {
|
|
38
|
-
throw new Error(`No default export found.\n\n` +
|
|
39
|
-
` Your file must export a Forme element or a function that returns one:\n\n` +
|
|
40
|
-
` export default (\n` +
|
|
41
|
-
` <Document>\n` +
|
|
42
|
-
` <Text>Hello</Text>\n` +
|
|
43
|
-
` </Document>\n` +
|
|
44
|
-
` );\n\n` +
|
|
45
|
-
` Or with data:\n\n` +
|
|
46
|
-
` export default function Report(data) {\n` +
|
|
47
|
-
` return <Document><Text>{data.title}</Text></Document>\n` +
|
|
48
|
-
` }`);
|
|
49
|
-
}
|
|
50
|
-
if (typeof exported === 'function') {
|
|
51
|
-
const data = dataPath ? await loadJsonData(dataPath) : {};
|
|
52
|
-
const result = await exported(data);
|
|
53
|
-
if (!isValidElement(result)) {
|
|
54
|
-
throw new Error(`Default export function did not return a valid Forme element.\n` +
|
|
55
|
-
` Got: ${typeof result}\n` +
|
|
56
|
-
` Make sure your function returns a <Document> element.`);
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
if (isValidElement(exported)) {
|
|
61
|
-
if (dataPath) {
|
|
62
|
-
console.warn(`Warning: --data flag provided but default export is a static element, not a function.\n` +
|
|
63
|
-
` The data file will be ignored. Export a function to use --data.`);
|
|
64
|
-
}
|
|
65
|
-
return exported;
|
|
66
|
-
}
|
|
67
|
-
throw new Error(`Default export is not a valid Forme element.\n` +
|
|
68
|
-
` Got: ${typeof exported}\n` +
|
|
69
|
-
` Expected: a <Document> element or a function that returns one.`);
|
|
70
|
-
}
|
|
71
|
-
async function loadJsonData(dataPath) {
|
|
72
|
-
const absolutePath = resolve(dataPath);
|
|
73
|
-
const raw = await readFile(absolutePath, 'utf-8');
|
|
74
|
-
try {
|
|
75
|
-
return JSON.parse(raw);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
throw new Error(`Failed to parse data file as JSON: ${absolutePath}\n` +
|
|
79
|
-
` Make sure the file contains valid JSON.`);
|
|
80
|
-
}
|
|
81
|
-
}
|
package/dist/dev.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { readFile, writeFile
|
|
3
|
-
import { resolve, basename
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, basename } from 'node:path';
|
|
5
4
|
import { watch } from 'chokidar';
|
|
6
5
|
import { WebSocketServer } from 'ws';
|
|
7
6
|
import open from 'open';
|
|
8
|
-
import {
|
|
9
|
-
import { bundleFile, BUNDLE_DIR } from './bundle.js';
|
|
10
|
-
import { renderPdfWithLayout } from '@formepdf/core';
|
|
7
|
+
import { renderFromFile } from '@formepdf/renderer';
|
|
11
8
|
export function startDevServer(inputPath, options) {
|
|
12
9
|
const absoluteInput = resolve(inputPath);
|
|
13
10
|
const fileName = basename(absoluteInput);
|
|
@@ -124,66 +121,20 @@ export function startDevServer(inputPath, options) {
|
|
|
124
121
|
let buildCounter = 0;
|
|
125
122
|
async function rebuild() {
|
|
126
123
|
const buildId = ++buildCounter;
|
|
127
|
-
const start = performance.now();
|
|
128
124
|
try {
|
|
129
|
-
const
|
|
125
|
+
const result = await renderFromFile(absoluteInput, {
|
|
126
|
+
dataPath,
|
|
127
|
+
data: useInMemoryData ? inMemoryData : undefined,
|
|
128
|
+
pageSize: pageSizeOverride ?? undefined,
|
|
129
|
+
});
|
|
130
130
|
// Skip if a newer build started
|
|
131
131
|
if (buildId !== buildCounter)
|
|
132
132
|
return;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await writeFile(tmpFile, code);
|
|
136
|
-
let mod;
|
|
137
|
-
try {
|
|
138
|
-
mod = await import(pathToFileURL(tmpFile).href);
|
|
139
|
-
}
|
|
140
|
-
finally {
|
|
141
|
-
await unlink(tmpFile).catch(() => { });
|
|
142
|
-
}
|
|
143
|
-
const overrideData = useInMemoryData ? inMemoryData : undefined;
|
|
144
|
-
const element = await resolveElement(mod, dataPath, overrideData);
|
|
145
|
-
// Serialize JSX to document JSON, apply overrides, then render
|
|
146
|
-
const { serialize } = await import('@formepdf/react');
|
|
147
|
-
const doc = serialize(element);
|
|
148
|
-
// Apply page size override
|
|
149
|
-
if (pageSizeOverride) {
|
|
150
|
-
const { width, height } = pageSizeOverride;
|
|
151
|
-
const customSize = { Custom: { width, height } };
|
|
152
|
-
// Override defaultPage size
|
|
153
|
-
if (doc.defaultPage && typeof doc.defaultPage === 'object') {
|
|
154
|
-
doc.defaultPage.size = customSize;
|
|
155
|
-
}
|
|
156
|
-
// Override each Page node's config size
|
|
157
|
-
if (Array.isArray(doc.children)) {
|
|
158
|
-
for (const child of doc.children) {
|
|
159
|
-
if (child && typeof child === 'object' && child.kind && typeof child.kind === 'object') {
|
|
160
|
-
const kind = child.kind;
|
|
161
|
-
if (kind.type === 'Page' && kind.config && typeof kind.config === 'object') {
|
|
162
|
-
kind.config.size = customSize;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
// Resolve font file paths and image URLs before rendering
|
|
169
|
-
await Promise.all([
|
|
170
|
-
resolveFontPaths(doc, absoluteInput),
|
|
171
|
-
resolveImageUrls(doc),
|
|
172
|
-
]);
|
|
173
|
-
const { pdf, layout } = await renderPdfWithLayout(JSON.stringify(doc));
|
|
174
|
-
// Skip if a newer build started
|
|
175
|
-
if (buildId !== buildCounter)
|
|
176
|
-
return;
|
|
177
|
-
currentPdf = pdf;
|
|
178
|
-
currentLayout = layout;
|
|
133
|
+
currentPdf = result.pdf;
|
|
134
|
+
currentLayout = result.layout;
|
|
179
135
|
lastError = null;
|
|
180
|
-
lastRenderTime =
|
|
181
|
-
const pageCount = layout?.pages?.length ?? 0;
|
|
182
|
-
// Warn once if no source locations found (click-to-inspect won't work)
|
|
183
|
-
if (firstRender && !hasAnySourceLocation(doc)) {
|
|
184
|
-
console.warn(`\n Warning: No source locations found — click-to-inspect is disabled.\n` +
|
|
185
|
-
` Ensure your tsconfig.json has "jsx": "react-jsx" (not "react").\n`);
|
|
186
|
-
}
|
|
136
|
+
lastRenderTime = result.renderTimeMs;
|
|
137
|
+
const pageCount = result.layout?.pages?.length ?? 0;
|
|
187
138
|
if (firstRender) {
|
|
188
139
|
firstRender = false;
|
|
189
140
|
const url = `http://localhost:${port}`;
|
|
@@ -239,52 +190,19 @@ export function startDevServer(inputPath, options) {
|
|
|
239
190
|
rebuild();
|
|
240
191
|
});
|
|
241
192
|
}
|
|
242
|
-
async function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
` <Text>Hello</Text>\n` +
|
|
250
|
-
` </Document>\n` +
|
|
251
|
-
` );`);
|
|
252
|
-
}
|
|
253
|
-
if (typeof exported === 'function') {
|
|
254
|
-
let data = {};
|
|
255
|
-
if (overrideData !== undefined) {
|
|
256
|
-
data = overrideData;
|
|
257
|
-
}
|
|
258
|
-
else if (dataPath) {
|
|
259
|
-
const raw = await readFile(dataPath, 'utf-8');
|
|
260
|
-
try {
|
|
261
|
-
data = JSON.parse(raw);
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
throw new Error(`Failed to parse data file as JSON: ${dataPath}\n` +
|
|
265
|
-
` Make sure the file contains valid JSON.`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const result = await exported(data);
|
|
269
|
-
if (!isValidElement(result)) {
|
|
270
|
-
throw new Error(`Default export function did not return a valid Forme element.\n` +
|
|
271
|
-
` Got: ${typeof result}`);
|
|
272
|
-
}
|
|
273
|
-
return result;
|
|
193
|
+
async function getPreviewHtml() {
|
|
194
|
+
// Try to load preview HTML from @formepdf/renderer
|
|
195
|
+
try {
|
|
196
|
+
const { createRequire } = await import('node:module');
|
|
197
|
+
const require = createRequire(import.meta.url);
|
|
198
|
+
const previewPath = require.resolve('@formepdf/renderer/preview');
|
|
199
|
+
return await readFile(previewPath, 'utf-8');
|
|
274
200
|
}
|
|
275
|
-
|
|
276
|
-
|
|
201
|
+
catch {
|
|
202
|
+
// Fallback: try local paths
|
|
277
203
|
}
|
|
278
|
-
throw new Error(`Default export is not a valid Forme element.\n` +
|
|
279
|
-
` Got: ${typeof exported}\n` +
|
|
280
|
-
` Expected: a <Document> element or a function that returns one.`);
|
|
281
|
-
}
|
|
282
|
-
async function getPreviewHtml() {
|
|
283
|
-
// Try to load the preview HTML from the package's own file tree
|
|
284
204
|
const possiblePaths = [
|
|
285
|
-
// When running from dist/ (built)
|
|
286
205
|
new URL('./preview/index.html', import.meta.url),
|
|
287
|
-
// When running from src/ (dev with ts-node)
|
|
288
206
|
new URL('../src/preview/index.html', import.meta.url),
|
|
289
207
|
];
|
|
290
208
|
for (const p of possiblePaths) {
|
|
@@ -295,61 +213,5 @@ async function getPreviewHtml() {
|
|
|
295
213
|
continue;
|
|
296
214
|
}
|
|
297
215
|
}
|
|
298
|
-
// Inline fallback
|
|
299
216
|
return `<!DOCTYPE html><html><body><h1>Preview HTML not found</h1></body></html>`;
|
|
300
217
|
}
|
|
301
|
-
function hasAnySourceLocation(doc) {
|
|
302
|
-
const children = doc.children;
|
|
303
|
-
if (!children)
|
|
304
|
-
return false;
|
|
305
|
-
function check(node) {
|
|
306
|
-
if (node.sourceLocation)
|
|
307
|
-
return true;
|
|
308
|
-
const kids = node.children;
|
|
309
|
-
return kids?.some(check) ?? false;
|
|
310
|
-
}
|
|
311
|
-
return children.some(check);
|
|
312
|
-
}
|
|
313
|
-
function uint8ArrayToBase64(bytes) {
|
|
314
|
-
return Buffer.from(bytes).toString('base64');
|
|
315
|
-
}
|
|
316
|
-
async function resolveImageUrls(doc) {
|
|
317
|
-
const children = doc.children;
|
|
318
|
-
if (!children?.length)
|
|
319
|
-
return;
|
|
320
|
-
await Promise.all(children.map(resolveImageUrlsInNode));
|
|
321
|
-
}
|
|
322
|
-
async function resolveImageUrlsInNode(node) {
|
|
323
|
-
const kind = node.kind;
|
|
324
|
-
if (kind?.type === 'Image' && typeof kind.src === 'string') {
|
|
325
|
-
const src = kind.src;
|
|
326
|
-
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
327
|
-
const res = await fetch(src);
|
|
328
|
-
if (!res.ok)
|
|
329
|
-
throw new Error(`Failed to fetch image: ${src} (${res.status})`);
|
|
330
|
-
const contentType = res.headers.get('content-type') || 'image/png';
|
|
331
|
-
const buf = new Uint8Array(await res.arrayBuffer());
|
|
332
|
-
kind.src = `data:${contentType};base64,${uint8ArrayToBase64(buf)}`;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const children = node.children;
|
|
336
|
-
if (children?.length) {
|
|
337
|
-
await Promise.all(children.map(resolveImageUrlsInNode));
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
async function resolveFontPaths(doc, templatePath) {
|
|
341
|
-
const fonts = doc.fonts;
|
|
342
|
-
if (!fonts?.length)
|
|
343
|
-
return;
|
|
344
|
-
const templateDir = dirname(templatePath);
|
|
345
|
-
for (const font of fonts) {
|
|
346
|
-
if (font.src instanceof Uint8Array) {
|
|
347
|
-
font.src = uint8ArrayToBase64(font.src);
|
|
348
|
-
}
|
|
349
|
-
else if (typeof font.src === 'string' && !font.src.startsWith('data:')) {
|
|
350
|
-
const fontPath = resolve(templateDir, font.src);
|
|
351
|
-
const bytes = await readFile(fontPath);
|
|
352
|
-
font.src = uint8ArrayToBase64(new Uint8Array(bytes));
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
package/dist/template-build.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { resolve,
|
|
2
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { isValidElement } from 'react';
|
|
5
|
-
import { bundleFile,
|
|
5
|
+
import { bundleFile, resolveFontSources } from '@formepdf/renderer';
|
|
6
6
|
export async function buildTemplate(inputPath, options) {
|
|
7
7
|
const absoluteInput = resolve(inputPath);
|
|
8
8
|
console.log(`Building template from ${absoluteInput}...`);
|
|
9
9
|
try {
|
|
10
10
|
const code = await bundleFile(absoluteInput);
|
|
11
|
-
const tmpFile =
|
|
11
|
+
const tmpFile = resolve(dirname(absoluteInput), `.forme-template-${Date.now()}.mjs`);
|
|
12
12
|
await writeFile(tmpFile, code);
|
|
13
13
|
try {
|
|
14
14
|
const mod = await import(pathToFileURL(tmpFile).href);
|
|
@@ -39,7 +39,7 @@ export async function buildTemplate(inputPath, options) {
|
|
|
39
39
|
}
|
|
40
40
|
const templateJson = serializeTemplate(element);
|
|
41
41
|
// Resolve fonts to base64 for portability
|
|
42
|
-
await
|
|
42
|
+
await resolveFontSources(templateJson, dirname(absoluteInput));
|
|
43
43
|
const outputPath = resolve(options.output ?? defaultTemplateName(inputPath));
|
|
44
44
|
const json = JSON.stringify(templateJson, null, 2);
|
|
45
45
|
await writeFile(outputPath, json);
|
|
@@ -60,22 +60,3 @@ function defaultTemplateName(inputPath) {
|
|
|
60
60
|
const base = basename(inputPath).replace(/\.(tsx|jsx|ts|js)$/, '');
|
|
61
61
|
return `${base}.template.json`;
|
|
62
62
|
}
|
|
63
|
-
async function resolveFontPaths(doc, templatePath) {
|
|
64
|
-
const { dirname } = await import('node:path');
|
|
65
|
-
const { readFile } = await import('node:fs/promises');
|
|
66
|
-
const { resolve: resolvePath } = await import('node:path');
|
|
67
|
-
const templateDir = dirname(templatePath);
|
|
68
|
-
const fonts = doc.fonts;
|
|
69
|
-
if (!fonts?.length)
|
|
70
|
-
return;
|
|
71
|
-
for (const font of fonts) {
|
|
72
|
-
if (font.src instanceof Uint8Array) {
|
|
73
|
-
font.src = Buffer.from(font.src).toString('base64');
|
|
74
|
-
}
|
|
75
|
-
else if (typeof font.src === 'string' && !font.src.startsWith('data:')) {
|
|
76
|
-
const fontPath = resolvePath(templateDir, font.src);
|
|
77
|
-
const bytes = await readFile(fontPath);
|
|
78
|
-
font.src = Buffer.from(bytes).toString('base64');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formepdf/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI for Forme PDF rendering engine — dev server with live preview and build command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"forme": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc
|
|
10
|
+
"build": "tsc",
|
|
11
11
|
"test": "vitest run"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@formepdf/
|
|
18
|
-
"@formepdf/react": "0.6.2",
|
|
19
|
-
"esbuild": "^0.24.0",
|
|
17
|
+
"@formepdf/renderer": "0.7.0",
|
|
20
18
|
"chokidar": "^4.0.0",
|
|
21
19
|
"ws": "^8.18.0",
|
|
22
20
|
"open": "^10.1.0"
|