@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 +22 -1
- package/dist/index.js +11 -3
- package/dist/preview/index.html +46 -0
- package/dist/template-build.d.ts +4 -0
- package/dist/template-build.js +81 -0
- package/package.json +3 -3
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
|
|
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'
|
|
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
|
-
|
|
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`);
|
package/dist/preview/index.html
CHANGED
|
@@ -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,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.
|
|
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.
|
|
18
|
-
"@formepdf/react": "0.
|
|
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",
|