@formepdf/cli 0.6.1 → 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.
package/dist/build.js CHANGED
@@ -1,30 +1,14 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { resolve, join } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
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 code = await bundleFile(absoluteInput);
12
- // Write temp file inside CLI package dir so Node resolves @formepdf/* deps
13
- const tmpFile = join(BUNDLE_DIR, `.forme-build-${Date.now()}.mjs`);
14
- await writeFile(tmpFile, code);
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, unlink } from 'node:fs/promises';
3
- import { resolve, basename, dirname, join } from 'node:path';
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 { isValidElement } from 'react';
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 code = await bundleFile(absoluteInput);
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
- // Write temp file inside CLI package dir so Node resolves @formepdf/* deps
134
- const tmpFile = join(BUNDLE_DIR, `.forme-dev-${Date.now()}.mjs`);
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 = Math.round(performance.now() - start);
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 resolveElement(mod, dataPath, overrideData) {
243
- const exported = mod.default;
244
- if (exported === undefined) {
245
- throw new Error(`No default export found.\n\n` +
246
- ` Your file must export a Forme element or a function that returns one:\n\n` +
247
- ` export default (\n` +
248
- ` <Document>\n` +
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
- if (isValidElement(exported)) {
276
- return exported;
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
- }
@@ -1,14 +1,14 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
- import { resolve, join, basename } from 'node:path';
2
+ import { resolve, dirname, basename } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { isValidElement } from 'react';
5
- import { bundleFile, BUNDLE_DIR } from './bundle.js';
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 = join(BUNDLE_DIR, `.forme-template-${Date.now()}.mjs`);
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 resolveFontPaths(templateJson, absoluteInput);
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.6.1",
3
+ "version": "0.7.0",
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 && rm -rf dist/preview && cp -r src/preview dist/preview",
10
+ "build": "tsc",
11
11
  "test": "vitest run"
12
12
  },
13
13
  "files": [
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@formepdf/core": "0.6.1",
18
- "@formepdf/react": "0.6.1",
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"