@formepdf/cli 0.2.1 → 0.4.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/dev.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { readFile, writeFile, unlink } from 'node:fs/promises';
3
- import { resolve, basename, join } from 'node:path';
3
+ import { resolve, basename, dirname, join } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { watch } from 'chokidar';
6
6
  import { WebSocketServer } from 'ws';
@@ -165,6 +165,8 @@ export function startDevServer(inputPath, options) {
165
165
  }
166
166
  }
167
167
  }
168
+ // Resolve font file paths relative to the template directory
169
+ await resolveFontPaths(doc, absoluteInput);
168
170
  const { pdf, layout } = await renderPdfWithLayout(JSON.stringify(doc));
169
171
  // Skip if a newer build started
170
172
  if (buildId !== buildCounter)
@@ -305,3 +307,22 @@ function hasAnySourceLocation(doc) {
305
307
  }
306
308
  return children.some(check);
307
309
  }
310
+ function uint8ArrayToBase64(bytes) {
311
+ return Buffer.from(bytes).toString('base64');
312
+ }
313
+ async function resolveFontPaths(doc, templatePath) {
314
+ const fonts = doc.fonts;
315
+ if (!fonts?.length)
316
+ return;
317
+ const templateDir = dirname(templatePath);
318
+ for (const font of fonts) {
319
+ if (font.src instanceof Uint8Array) {
320
+ font.src = uint8ArrayToBase64(font.src);
321
+ }
322
+ else if (typeof font.src === 'string' && !font.src.startsWith('data:')) {
323
+ const fontPath = resolve(templateDir, font.src);
324
+ const bytes = await readFile(fontPath);
325
+ font.src = uint8ArrayToBase64(new Uint8Array(bytes));
326
+ }
327
+ }
328
+ }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { parseArgs } from 'node:util';
4
4
  import { resolve } from 'node:path';
5
5
  import { startDevServer } from './dev.js';
6
6
  import { buildPdf } from './build.js';
7
+ import { buildTemplate } from './template-build.js';
7
8
  const USAGE = `
8
9
  forme - Page-native PDF rendering engine
9
10
 
@@ -12,8 +13,9 @@ Usage:
12
13
  forme build <file.tsx> Render PDF to disk
13
14
 
14
15
  Options:
15
- -o, --output <path> Output PDF path (build only, default: output.pdf)
16
+ -o, --output <path> Output path (default: output.pdf, or <name>.template.json with -t)
16
17
  -d, --data <path> JSON data file to pass to template function
18
+ -t, --template Compile to template JSON instead of rendering PDF
17
19
  -p, --port <number> Dev server port (default: 4242)
18
20
  -h, --help Show this help message
19
21
 
@@ -37,8 +39,9 @@ function main() {
37
39
  const { values, positionals } = parseArgs({
38
40
  allowPositionals: true,
39
41
  options: {
40
- output: { type: 'string', short: 'o', default: 'output.pdf' },
42
+ output: { type: 'string', short: 'o' },
41
43
  data: { type: 'string', short: 'd' },
44
+ template: { type: 'boolean', short: 't', default: false },
42
45
  port: { type: 'string', short: 'p', default: '4242' },
43
46
  help: { type: 'boolean', short: 'h', default: false },
44
47
  },
@@ -73,7 +76,12 @@ function main() {
73
76
  startDevServer(inputPath, { port: Number(values.port), dataPath });
74
77
  break;
75
78
  case 'build':
76
- buildPdf(inputPath, { output: values.output, dataPath });
79
+ if (values.template) {
80
+ buildTemplate(inputPath, { output: values.output });
81
+ }
82
+ else {
83
+ buildPdf(inputPath, { output: values.output ?? 'output.pdf', dataPath });
84
+ }
77
85
  break;
78
86
  default:
79
87
  console.error(`Unknown command: ${command}\n`);
@@ -1226,6 +1226,17 @@
1226
1226
  // Computed styles
1227
1227
  let html = '';
1228
1228
 
1229
+ const sizeProps = [];
1230
+ if (s.width != null) sizeProps.push(['width', s.width + 'pt']);
1231
+ if (s.height != null) sizeProps.push(['height', s.height + 'pt']);
1232
+ if (s.minWidth != null) sizeProps.push(['min-width', fmt(s.minWidth) + 'pt']);
1233
+ if (s.minHeight != null) sizeProps.push(['min-height', fmt(s.minHeight) + 'pt']);
1234
+ if (s.maxWidth != null) sizeProps.push(['max-width', fmt(s.maxWidth) + 'pt']);
1235
+ if (s.maxHeight != null) sizeProps.push(['max-height', fmt(s.maxHeight) + 'pt']);
1236
+ if (sizeProps.length) {
1237
+ html += renderStyleSection('Sizing', sizeProps);
1238
+ }
1239
+
1229
1240
  const posProps = [];
1230
1241
  if (s.position && s.position !== 'Relative') {
1231
1242
  posProps.push(['position', s.position.toLowerCase()]);
@@ -1242,8 +1253,14 @@
1242
1253
  if (s.flexDirection !== 'Column') layoutProps.push(['flex-direction', s.flexDirection]);
1243
1254
  if (s.justifyContent !== 'FlexStart') layoutProps.push(['justify-content', s.justifyContent]);
1244
1255
  if (s.alignItems !== 'Stretch') layoutProps.push(['align-items', s.alignItems]);
1256
+ if (s.alignSelf) layoutProps.push(['align-self', s.alignSelf]);
1245
1257
  if (s.flexWrap !== 'NoWrap') layoutProps.push(['flex-wrap', s.flexWrap]);
1258
+ if (s.flexGrow > 0) layoutProps.push(['flex-grow', String(s.flexGrow)]);
1259
+ if (s.flexShrink !== 1) layoutProps.push(['flex-shrink', String(s.flexShrink)]);
1260
+ if (s.flexBasis != null) layoutProps.push(['flex-basis', s.flexBasis + 'pt']);
1246
1261
  if (s.gap > 0) layoutProps.push(['gap', fmt(s.gap) + 'pt']);
1262
+ if (s.rowGap > 0 && s.rowGap !== s.gap) layoutProps.push(['row-gap', fmt(s.rowGap) + 'pt']);
1263
+ if (s.columnGap > 0 && s.columnGap !== s.gap) layoutProps.push(['column-gap', fmt(s.columnGap) + 'pt']);
1247
1264
  if (layoutProps.length) {
1248
1265
  html += renderStyleSection('Layout', layoutProps);
1249
1266
  }
@@ -1266,6 +1283,9 @@
1266
1283
  if (s.fontStyle !== 'Normal') typoProps.push(['font-style', s.fontStyle]);
1267
1284
  if (s.lineHeight !== 1.4) typoProps.push(['line-height', fmt(s.lineHeight)]);
1268
1285
  if (s.textAlign !== 'Left') typoProps.push(['text-align', s.textAlign]);
1286
+ if (s.letterSpacing !== 0) typoProps.push(['letter-spacing', fmt(s.letterSpacing) + 'pt']);
1287
+ if (s.textDecoration !== 'None') typoProps.push(['text-decoration', s.textDecoration.toLowerCase()]);
1288
+ if (s.textTransform !== 'None') typoProps.push(['text-transform', s.textTransform.toLowerCase()]);
1269
1289
  html += renderStyleSection('Typography', typoProps);
1270
1290
 
1271
1291
  const visualProps = [];
@@ -1284,6 +1304,15 @@
1284
1304
  html += renderStyleSection('Link & Bookmark', linkProps);
1285
1305
  }
1286
1306
 
1307
+ const pageProps = [];
1308
+ if (s.breakable) pageProps.push(['breakable', 'true']);
1309
+ if (s.breakBefore) pageProps.push(['break-before', 'true']);
1310
+ if (s.minWidowLines !== 2) pageProps.push(['min-widow-lines', String(s.minWidowLines)]);
1311
+ if (s.minOrphanLines !== 2) pageProps.push(['min-orphan-lines', String(s.minOrphanLines)]);
1312
+ if (pageProps.length) {
1313
+ html += renderStyleSection('Page Behavior', pageProps);
1314
+ }
1315
+
1287
1316
  inspectorStyles.innerHTML = html;
1288
1317
 
1289
1318
  // Copy Style button
@@ -1352,6 +1381,8 @@
1352
1381
  'NoWrap': 'nowrap', 'Wrap': 'wrap',
1353
1382
  'Normal': 'normal', 'Italic': 'italic',
1354
1383
  'Left': 'left', 'Right': 'right', 'Justify': 'justify',
1384
+ 'None': 'none', 'Underline': 'underline', 'LineThrough': 'line-through',
1385
+ 'Uppercase': 'uppercase', 'Lowercase': 'lowercase', 'Capitalize': 'capitalize',
1355
1386
  };
1356
1387
  function mapEnum(v) { return enumMap[v] || v; }
1357
1388
 
@@ -1375,11 +1406,23 @@
1375
1406
 
1376
1407
  const props = [];
1377
1408
 
1409
+ if (s.width != null) props.push(`width: ${s.width}`);
1410
+ if (s.height != null) props.push(`height: ${s.height}`);
1411
+ if (s.minWidth != null) props.push(`minWidth: ${s.minWidth}`);
1412
+ if (s.minHeight != null) props.push(`minHeight: ${s.minHeight}`);
1413
+ if (s.maxWidth != null) props.push(`maxWidth: ${s.maxWidth}`);
1414
+ if (s.maxHeight != null) props.push(`maxHeight: ${s.maxHeight}`);
1378
1415
  if (s.flexDirection !== 'Column') props.push(`flexDirection: '${mapEnum(s.flexDirection)}'`);
1379
1416
  if (s.justifyContent !== 'FlexStart') props.push(`justifyContent: '${mapEnum(s.justifyContent)}'`);
1380
1417
  if (s.alignItems !== 'Stretch') props.push(`alignItems: '${mapEnum(s.alignItems)}'`);
1418
+ if (s.alignSelf) props.push(`alignSelf: '${mapEnum(s.alignSelf)}'`);
1381
1419
  if (s.flexWrap !== 'NoWrap') props.push(`flexWrap: '${mapEnum(s.flexWrap)}'`);
1420
+ if (s.flexGrow > 0) props.push(`flexGrow: ${s.flexGrow}`);
1421
+ if (s.flexShrink !== 1) props.push(`flexShrink: ${s.flexShrink}`);
1422
+ if (s.flexBasis != null) props.push(`flexBasis: ${s.flexBasis}`);
1382
1423
  if (s.gap > 0) props.push(`gap: ${s.gap}`);
1424
+ if (s.rowGap > 0) props.push(`rowGap: ${s.rowGap}`);
1425
+ if (s.columnGap > 0) props.push(`columnGap: ${s.columnGap}`);
1383
1426
 
1384
1427
  if (!edgesAllZero(s.margin)) props.push(`margin: ${fmtEdges(s.margin)}`);
1385
1428
  if (!edgesAllZero(s.padding)) props.push(`padding: ${fmtEdges(s.padding)}`);
@@ -1391,6 +1434,9 @@
1391
1434
  if (s.fontStyle !== 'Normal') props.push(`fontStyle: '${mapEnum(s.fontStyle)}'`);
1392
1435
  if (s.lineHeight !== 1.4) props.push(`lineHeight: ${s.lineHeight}`);
1393
1436
  if (s.textAlign !== 'Left') props.push(`textAlign: '${mapEnum(s.textAlign)}'`);
1437
+ if (s.letterSpacing !== 0) props.push(`letterSpacing: ${s.letterSpacing}`);
1438
+ if (s.textDecoration !== 'None') props.push(`textDecoration: '${mapEnum(s.textDecoration)}'`);
1439
+ if (s.textTransform !== 'None') props.push(`textTransform: '${mapEnum(s.textTransform)}'`);
1394
1440
 
1395
1441
  if (s.opacity < 1) props.push(`opacity: ${s.opacity}`);
1396
1442
  if (!colorEq(s.color, 0, 0, 0)) props.push(`color: '${colorToHex(s.color)}'`);
@@ -0,0 +1,4 @@
1
+ export interface TemplateBuildOptions {
2
+ output?: string;
3
+ }
4
+ export declare function buildTemplate(inputPath: string, options: TemplateBuildOptions): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { resolve, join, basename } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { isValidElement } from 'react';
5
+ import { bundleFile, BUNDLE_DIR } from './bundle.js';
6
+ export async function buildTemplate(inputPath, options) {
7
+ const absoluteInput = resolve(inputPath);
8
+ console.log(`Building template from ${absoluteInput}...`);
9
+ try {
10
+ const code = await bundleFile(absoluteInput);
11
+ const tmpFile = join(BUNDLE_DIR, `.forme-template-${Date.now()}.mjs`);
12
+ await writeFile(tmpFile, code);
13
+ try {
14
+ const mod = await import(pathToFileURL(tmpFile).href);
15
+ const exported = mod.default;
16
+ if (exported === undefined) {
17
+ throw new Error(`No default export found.\n\n` +
18
+ ` Your template file must export a function that takes data and returns JSX:\n\n` +
19
+ ` export default function Invoice(data) {\n` +
20
+ ` return <Document><Text>{data.title}</Text></Document>\n` +
21
+ ` }`);
22
+ }
23
+ if (typeof exported !== 'function') {
24
+ throw new Error(`Default export must be a function for template compilation.\n` +
25
+ ` Got: ${typeof exported}\n\n` +
26
+ ` Export a function that takes data and returns a <Document>:\n\n` +
27
+ ` export default function Template(data) {\n` +
28
+ ` return <Document>...</Document>\n` +
29
+ ` }`);
30
+ }
31
+ // Create a recording proxy and call the template function
32
+ const { createDataProxy, serializeTemplate } = await import('@formepdf/react');
33
+ const dataProxy = createDataProxy();
34
+ const element = exported(dataProxy);
35
+ if (!isValidElement(element)) {
36
+ throw new Error(`Template function did not return a valid Forme element.\n` +
37
+ ` Got: ${typeof element}\n` +
38
+ ` Make sure your function returns a <Document> element.`);
39
+ }
40
+ const templateJson = serializeTemplate(element);
41
+ // Resolve fonts to base64 for portability
42
+ await resolveFontPaths(templateJson, absoluteInput);
43
+ const outputPath = resolve(options.output ?? defaultTemplateName(inputPath));
44
+ const json = JSON.stringify(templateJson, null, 2);
45
+ await writeFile(outputPath, json);
46
+ console.log(`Written template (${json.length} bytes) to ${outputPath}`);
47
+ }
48
+ finally {
49
+ const { unlink } = await import('node:fs/promises');
50
+ await unlink(tmpFile).catch(() => { });
51
+ }
52
+ }
53
+ catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ console.error(`\n ${message.split('\n').join('\n ')}\n`);
56
+ process.exit(1);
57
+ }
58
+ }
59
+ function defaultTemplateName(inputPath) {
60
+ const base = basename(inputPath).replace(/\.(tsx|jsx|ts|js)$/, '');
61
+ return `${base}.template.json`;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@formepdf/cli",
3
- "version": "0.2.1",
3
+ "version": "0.4.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": {
@@ -14,8 +14,8 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@formepdf/core": "0.2.1",
18
- "@formepdf/react": "0.2.1",
17
+ "@formepdf/core": "0.4.0",
18
+ "@formepdf/react": "0.4.0",
19
19
  "esbuild": "^0.24.0",
20
20
  "chokidar": "^4.0.0",
21
21
  "ws": "^8.18.0",