@formepdf/cli 0.1.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.d.ts +5 -0
- package/dist/build.js +81 -0
- package/dist/bundle.d.ts +2 -0
- package/dist/bundle.js +87 -0
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +290 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +84 -0
- package/dist/preview/index.html +2344 -0
- package/package.json +37 -0
package/dist/build.d.ts
ADDED
package/dist/build.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
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';
|
|
7
|
+
export async function buildPdf(inputPath, options) {
|
|
8
|
+
const absoluteInput = resolve(inputPath);
|
|
9
|
+
console.log(`Building ${absoluteInput}...`);
|
|
10
|
+
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
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
console.error(`\n ${message.split('\n').join('\n ')}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
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/bundle.d.ts
ADDED
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
/// The temp directory for bundled output — placed inside CLI package
|
|
6
|
+
/// so that Node's module resolution finds @formepdf/react, @formepdf/core, react.
|
|
7
|
+
export const BUNDLE_DIR = join(__dirname, '..');
|
|
8
|
+
/// esbuild plugin that intercepts react/jsx-dev-runtime to capture source
|
|
9
|
+
/// locations in a global WeakMap. React 19 no longer stores _source on
|
|
10
|
+
/// elements, so we wrap jsxDEV to do it ourselves.
|
|
11
|
+
const formeJsxSourcePlugin = {
|
|
12
|
+
name: 'forme-jsx-source',
|
|
13
|
+
setup(pluginBuild) {
|
|
14
|
+
pluginBuild.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
|
15
|
+
path: 'forme-jsx-dev-runtime',
|
|
16
|
+
namespace: 'forme-jsx',
|
|
17
|
+
}));
|
|
18
|
+
pluginBuild.onLoad({ filter: /.*/, namespace: 'forme-jsx' }, () => {
|
|
19
|
+
const cwd = pluginBuild.initialOptions.absWorkingDir || process.cwd();
|
|
20
|
+
return {
|
|
21
|
+
contents: `
|
|
22
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
23
|
+
import { resolve, isAbsolute } from 'node:path';
|
|
24
|
+
export { Fragment };
|
|
25
|
+
if (!globalThis.__formeSourceMap) globalThis.__formeSourceMap = new WeakMap();
|
|
26
|
+
const _cwd = ${JSON.stringify(cwd)};
|
|
27
|
+
export function jsxDEV(type, props, key, isStaticChildren, source, self) {
|
|
28
|
+
const el = jsx(type, props, key);
|
|
29
|
+
if (source && source.fileName) {
|
|
30
|
+
try {
|
|
31
|
+
const file = isAbsolute(source.fileName) ? source.fileName : resolve(_cwd, source.fileName);
|
|
32
|
+
globalThis.__formeSourceMap.set(el, { file, line: source.lineNumber, column: source.columnNumber });
|
|
33
|
+
} catch(e) {}
|
|
34
|
+
}
|
|
35
|
+
return el;
|
|
36
|
+
}
|
|
37
|
+
`,
|
|
38
|
+
resolveDir: cwd,
|
|
39
|
+
loader: 'js',
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/// Bundle a TSX/JSX file into an ESM string that can be dynamically imported.
|
|
45
|
+
export async function bundleFile(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
const result = await build({
|
|
48
|
+
entryPoints: [filePath],
|
|
49
|
+
bundle: true,
|
|
50
|
+
format: 'esm',
|
|
51
|
+
platform: 'node',
|
|
52
|
+
write: false,
|
|
53
|
+
jsx: 'automatic',
|
|
54
|
+
jsxDev: true,
|
|
55
|
+
target: 'node20',
|
|
56
|
+
external: ['react', '@formepdf/react', '@formepdf/core'],
|
|
57
|
+
plugins: [formeJsxSourcePlugin],
|
|
58
|
+
});
|
|
59
|
+
return result.outputFiles[0].text;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
// Format esbuild errors with file location and source context
|
|
63
|
+
if (isBuildFailure(err)) {
|
|
64
|
+
const messages = [];
|
|
65
|
+
for (const error of err.errors) {
|
|
66
|
+
let loc = '';
|
|
67
|
+
if (error.location) {
|
|
68
|
+
const { file, line, column, lineText } = error.location;
|
|
69
|
+
loc = ` ${file}:${line}:${column}\n`;
|
|
70
|
+
if (lineText) {
|
|
71
|
+
loc += ` ${lineText}\n`;
|
|
72
|
+
loc += ` ${' '.repeat(column)}^\n`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
messages.push(`${error.text}\n${loc}`);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Build error:\n${messages.join('\n')}`);
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function isBuildFailure(err) {
|
|
83
|
+
return (err !== null &&
|
|
84
|
+
typeof err === 'object' &&
|
|
85
|
+
'errors' in err &&
|
|
86
|
+
Array.isArray(err.errors));
|
|
87
|
+
}
|
package/dist/dev.d.ts
ADDED
package/dist/dev.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { readFile, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { resolve, basename, join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { watch } from 'chokidar';
|
|
6
|
+
import { WebSocketServer } from 'ws';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import { isValidElement } from 'react';
|
|
9
|
+
import { bundleFile, BUNDLE_DIR } from './bundle.js';
|
|
10
|
+
import { renderPdfWithLayout } from '@formepdf/core';
|
|
11
|
+
export function startDevServer(inputPath, options) {
|
|
12
|
+
const absoluteInput = resolve(inputPath);
|
|
13
|
+
const fileName = basename(absoluteInput);
|
|
14
|
+
const port = options.port;
|
|
15
|
+
const dataPath = options.dataPath ? resolve(options.dataPath) : undefined;
|
|
16
|
+
let currentPdf = null;
|
|
17
|
+
let currentLayout = null;
|
|
18
|
+
let lastRenderTime = 0;
|
|
19
|
+
let lastError = null;
|
|
20
|
+
let firstRender = true;
|
|
21
|
+
// Override state
|
|
22
|
+
let pageSizeOverride = null;
|
|
23
|
+
let inMemoryData = null;
|
|
24
|
+
let useInMemoryData = false;
|
|
25
|
+
// ── HTTP Server ──────────────────────────────────────────────
|
|
26
|
+
const server = createServer(async (req, res) => {
|
|
27
|
+
const url = req.url ?? '/';
|
|
28
|
+
if (url === '/' || url === '/index.html') {
|
|
29
|
+
const html = await getPreviewHtml();
|
|
30
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
31
|
+
res.end(html);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (url === '/pdf') {
|
|
35
|
+
if (currentPdf) {
|
|
36
|
+
res.writeHead(200, {
|
|
37
|
+
'Content-Type': 'application/pdf',
|
|
38
|
+
'Content-Length': String(currentPdf.length),
|
|
39
|
+
});
|
|
40
|
+
res.end(currentPdf);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
44
|
+
res.end(lastError ?? 'PDF not yet rendered');
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (url === '/layout') {
|
|
49
|
+
if (currentLayout) {
|
|
50
|
+
const json = JSON.stringify(currentLayout);
|
|
51
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
52
|
+
res.end(json);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
56
|
+
res.end('Layout not yet available');
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
61
|
+
res.end('Not found');
|
|
62
|
+
});
|
|
63
|
+
// ── WebSocket ────────────────────────────────────────────────
|
|
64
|
+
const wss = new WebSocketServer({ server });
|
|
65
|
+
const clients = new Set();
|
|
66
|
+
wss.on('connection', async (ws) => {
|
|
67
|
+
clients.add(ws);
|
|
68
|
+
ws.on('close', () => clients.delete(ws));
|
|
69
|
+
// Send init message with data and page size state
|
|
70
|
+
let dataContent = null;
|
|
71
|
+
if (dataPath) {
|
|
72
|
+
try {
|
|
73
|
+
dataContent = await readFile(dataPath, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
}
|
|
77
|
+
const initMsg = {
|
|
78
|
+
type: 'init',
|
|
79
|
+
hasData: !!dataPath,
|
|
80
|
+
dataContent,
|
|
81
|
+
pageSizeOverride,
|
|
82
|
+
};
|
|
83
|
+
if (ws.readyState === ws.OPEN) {
|
|
84
|
+
ws.send(JSON.stringify(initMsg));
|
|
85
|
+
}
|
|
86
|
+
ws.on('message', (raw) => {
|
|
87
|
+
let msg;
|
|
88
|
+
try {
|
|
89
|
+
msg = JSON.parse(raw.toString());
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (msg.type === 'setPageSize') {
|
|
95
|
+
pageSizeOverride = { width: msg.width, height: msg.height };
|
|
96
|
+
triggerRebuild();
|
|
97
|
+
}
|
|
98
|
+
if (msg.type === 'clearPageSize') {
|
|
99
|
+
pageSizeOverride = null;
|
|
100
|
+
triggerRebuild();
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === 'updateData') {
|
|
103
|
+
inMemoryData = msg.data;
|
|
104
|
+
useInMemoryData = true;
|
|
105
|
+
triggerRebuild();
|
|
106
|
+
}
|
|
107
|
+
if (msg.type === 'saveData' && dataPath) {
|
|
108
|
+
const content = msg.content;
|
|
109
|
+
writeFile(dataPath, content, 'utf-8').catch((err) => {
|
|
110
|
+
console.error(`Failed to save data file: ${err}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
function broadcast(message) {
|
|
116
|
+
const data = JSON.stringify(message);
|
|
117
|
+
for (const ws of clients) {
|
|
118
|
+
if (ws.readyState === ws.OPEN) {
|
|
119
|
+
ws.send(data);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── Build + Render ───────────────────────────────────────────
|
|
124
|
+
let buildCounter = 0;
|
|
125
|
+
async function rebuild() {
|
|
126
|
+
const buildId = ++buildCounter;
|
|
127
|
+
const start = performance.now();
|
|
128
|
+
try {
|
|
129
|
+
const code = await bundleFile(absoluteInput);
|
|
130
|
+
// Skip if a newer build started
|
|
131
|
+
if (buildId !== buildCounter)
|
|
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
|
+
const { pdf, layout } = await renderPdfWithLayout(JSON.stringify(doc));
|
|
169
|
+
// Skip if a newer build started
|
|
170
|
+
if (buildId !== buildCounter)
|
|
171
|
+
return;
|
|
172
|
+
currentPdf = pdf;
|
|
173
|
+
currentLayout = layout;
|
|
174
|
+
lastError = null;
|
|
175
|
+
lastRenderTime = Math.round(performance.now() - start);
|
|
176
|
+
const pageCount = layout?.pages?.length ?? 0;
|
|
177
|
+
if (firstRender) {
|
|
178
|
+
firstRender = false;
|
|
179
|
+
const url = `http://localhost:${port}`;
|
|
180
|
+
console.log(`Forme dev server\n`);
|
|
181
|
+
console.log(` Watching: ${fileName}${dataPath ? ` + ${basename(dataPath)}` : ''}`);
|
|
182
|
+
console.log(` Rendered: ${pageCount} page${pageCount !== 1 ? 's' : ''} in ${lastRenderTime}ms`);
|
|
183
|
+
console.log(` Preview: ${url}\n`);
|
|
184
|
+
open(url);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
console.log(`Rebuilt in ${lastRenderTime}ms (${pageCount} page${pageCount !== 1 ? 's' : ''})`);
|
|
188
|
+
}
|
|
189
|
+
broadcast({ type: 'reload', renderTime: lastRenderTime });
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
193
|
+
lastError = message;
|
|
194
|
+
console.error(`Build error: ${message}`);
|
|
195
|
+
broadcast({ type: 'error', message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// ── File Watcher ─────────────────────────────────────────────
|
|
199
|
+
let debounceTimer = null;
|
|
200
|
+
function triggerRebuild() {
|
|
201
|
+
if (debounceTimer)
|
|
202
|
+
clearTimeout(debounceTimer);
|
|
203
|
+
debounceTimer = setTimeout(rebuild, 100);
|
|
204
|
+
}
|
|
205
|
+
const watchPaths = [absoluteInput, ...(dataPath ? [dataPath] : [])];
|
|
206
|
+
const watcher = watch(watchPaths, { ignoreInitial: true });
|
|
207
|
+
watcher.on('change', (changedPath) => {
|
|
208
|
+
// If data file changed on disk, reset in-memory override and push new content
|
|
209
|
+
if (dataPath && resolve(changedPath) === dataPath) {
|
|
210
|
+
useInMemoryData = false;
|
|
211
|
+
inMemoryData = null;
|
|
212
|
+
readFile(dataPath, 'utf-8').then((content) => {
|
|
213
|
+
broadcast({ type: 'dataUpdate', content });
|
|
214
|
+
}).catch(() => { });
|
|
215
|
+
}
|
|
216
|
+
triggerRebuild();
|
|
217
|
+
});
|
|
218
|
+
// ── Graceful shutdown ───────────────────────────────────────
|
|
219
|
+
process.on('SIGINT', () => {
|
|
220
|
+
console.log('\nShutting down...');
|
|
221
|
+
watcher.close();
|
|
222
|
+
wss.close();
|
|
223
|
+
server.close(() => process.exit(0));
|
|
224
|
+
// Force exit after 2s if graceful shutdown stalls
|
|
225
|
+
setTimeout(() => process.exit(0), 2000);
|
|
226
|
+
});
|
|
227
|
+
// ── Start ────────────────────────────────────────────────────
|
|
228
|
+
server.listen(port, () => {
|
|
229
|
+
rebuild();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function resolveElement(mod, dataPath, overrideData) {
|
|
233
|
+
const exported = mod.default;
|
|
234
|
+
if (exported === undefined) {
|
|
235
|
+
throw new Error(`No default export found.\n\n` +
|
|
236
|
+
` Your file must export a Forme element or a function that returns one:\n\n` +
|
|
237
|
+
` export default (\n` +
|
|
238
|
+
` <Document>\n` +
|
|
239
|
+
` <Text>Hello</Text>\n` +
|
|
240
|
+
` </Document>\n` +
|
|
241
|
+
` );`);
|
|
242
|
+
}
|
|
243
|
+
if (typeof exported === 'function') {
|
|
244
|
+
let data = {};
|
|
245
|
+
if (overrideData !== undefined) {
|
|
246
|
+
data = overrideData;
|
|
247
|
+
}
|
|
248
|
+
else if (dataPath) {
|
|
249
|
+
const raw = await readFile(dataPath, 'utf-8');
|
|
250
|
+
try {
|
|
251
|
+
data = JSON.parse(raw);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
throw new Error(`Failed to parse data file as JSON: ${dataPath}\n` +
|
|
255
|
+
` Make sure the file contains valid JSON.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const result = await exported(data);
|
|
259
|
+
if (!isValidElement(result)) {
|
|
260
|
+
throw new Error(`Default export function did not return a valid Forme element.\n` +
|
|
261
|
+
` Got: ${typeof result}`);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
if (isValidElement(exported)) {
|
|
266
|
+
return exported;
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`Default export is not a valid Forme element.\n` +
|
|
269
|
+
` Got: ${typeof exported}\n` +
|
|
270
|
+
` Expected: a <Document> element or a function that returns one.`);
|
|
271
|
+
}
|
|
272
|
+
async function getPreviewHtml() {
|
|
273
|
+
// Try to load the preview HTML from the package's own file tree
|
|
274
|
+
const possiblePaths = [
|
|
275
|
+
// When running from dist/ (built)
|
|
276
|
+
new URL('./preview/index.html', import.meta.url),
|
|
277
|
+
// When running from src/ (dev with ts-node)
|
|
278
|
+
new URL('../src/preview/index.html', import.meta.url),
|
|
279
|
+
];
|
|
280
|
+
for (const p of possiblePaths) {
|
|
281
|
+
try {
|
|
282
|
+
return await readFile(p, 'utf-8');
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Inline fallback
|
|
289
|
+
return `<!DOCTYPE html><html><body><h1>Preview HTML not found</h1></body></html>`;
|
|
290
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { startDevServer } from './dev.js';
|
|
6
|
+
import { buildPdf } from './build.js';
|
|
7
|
+
const USAGE = `
|
|
8
|
+
forme - Page-native PDF rendering engine
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
forme dev <file.tsx> Start dev server with live preview
|
|
12
|
+
forme build <file.tsx> Render PDF to disk
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
-o, --output <path> Output PDF path (build only, default: output.pdf)
|
|
16
|
+
-d, --data <path> JSON data file to pass to template function
|
|
17
|
+
-p, --port <number> Dev server port (default: 4242)
|
|
18
|
+
-h, --help Show this help message
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
forme dev src/invoice.tsx
|
|
22
|
+
forme build src/invoice.tsx -o invoice.pdf
|
|
23
|
+
forme build src/report.tsx --data data.json -o report.pdf
|
|
24
|
+
|
|
25
|
+
Data flag:
|
|
26
|
+
If your template exports a function instead of a JSX element,
|
|
27
|
+
use --data to pass a JSON file as the function argument:
|
|
28
|
+
|
|
29
|
+
// report.tsx
|
|
30
|
+
export default function Report(data: { title: string }) {
|
|
31
|
+
return <Document><Text>{data.title}</Text></Document>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
forme build report.tsx --data '{"title": "Q4 Report"}'
|
|
35
|
+
`;
|
|
36
|
+
function main() {
|
|
37
|
+
const { values, positionals } = parseArgs({
|
|
38
|
+
allowPositionals: true,
|
|
39
|
+
options: {
|
|
40
|
+
output: { type: 'string', short: 'o', default: 'output.pdf' },
|
|
41
|
+
data: { type: 'string', short: 'd' },
|
|
42
|
+
port: { type: 'string', short: 'p', default: '4242' },
|
|
43
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (values.help || positionals.length === 0) {
|
|
47
|
+
console.log(USAGE.trim());
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
const [command, inputPath] = positionals;
|
|
51
|
+
if (!inputPath) {
|
|
52
|
+
console.error(`Error: Missing input file.\n`);
|
|
53
|
+
console.log(USAGE.trim());
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
// Validate input file exists
|
|
57
|
+
const absoluteInput = resolve(inputPath);
|
|
58
|
+
if (!existsSync(absoluteInput)) {
|
|
59
|
+
console.error(`Error: Input file not found: ${absoluteInput}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
// Validate data file exists if provided
|
|
63
|
+
const dataPath = values.data;
|
|
64
|
+
if (dataPath) {
|
|
65
|
+
const absoluteData = resolve(dataPath);
|
|
66
|
+
if (!existsSync(absoluteData)) {
|
|
67
|
+
console.error(`Error: Data file not found: ${absoluteData}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
switch (command) {
|
|
72
|
+
case 'dev':
|
|
73
|
+
startDevServer(inputPath, { port: Number(values.port), dataPath });
|
|
74
|
+
break;
|
|
75
|
+
case 'build':
|
|
76
|
+
buildPdf(inputPath, { output: values.output, dataPath });
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
console.error(`Unknown command: ${command}\n`);
|
|
80
|
+
console.log(USAGE.trim());
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main();
|