pdf-smith 0.1.0 → 0.2.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/cli.js +72 -0
- package/dist/index.cjs +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/{preview-plugin-QWJEXDW2.js → preview-plugin-QDLEMAEE.js} +37 -17
- package/dist/preview-plugin-QDLEMAEE.js.map +1 -0
- package/dist/server.cjs +111 -41
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -7
- package/dist/server.d.ts +5 -7
- package/dist/server.js +75 -25
- package/dist/server.js.map +1 -1
- package/package.json +9 -2
- package/src/add.ts +72 -0
- package/src/cli.ts +24 -0
- package/src/config.ts +8 -0
- package/src/index.ts +2 -0
- package/src/preview/app.tsx +15 -4
- package/src/preview/entry.tsx +7 -3
- package/src/preview/home.tsx +95 -0
- package/src/preview/navigation.tsx +24 -2
- package/src/preview/preview-plugin.ts +38 -17
- package/src/preview/types.ts +0 -2
- package/src/preview/vite-env.d.ts +8 -2
- package/dist/preview-plugin-QWJEXDW2.js.map +0 -1
package/dist/server.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ interface PreviewOptions {
|
|
|
3
3
|
root: string;
|
|
4
4
|
/** Dev server port (default: 3000) */
|
|
5
5
|
port?: number;
|
|
6
|
-
pages?: string;
|
|
7
6
|
/** Auto-open browser (default: false) */
|
|
8
7
|
open?: boolean;
|
|
9
8
|
}
|
|
@@ -15,15 +14,14 @@ interface PreviewServer {
|
|
|
15
14
|
|
|
16
15
|
interface ExportOptions {
|
|
17
16
|
root: string;
|
|
17
|
+
document?: string;
|
|
18
18
|
output?: string;
|
|
19
|
-
pageNumbers?: {
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
template?: string;
|
|
22
|
-
};
|
|
23
|
-
pages?: string;
|
|
24
19
|
}
|
|
25
20
|
interface ExportResult {
|
|
26
|
-
|
|
21
|
+
outputs: Array<{
|
|
22
|
+
document: string;
|
|
23
|
+
outputPath: string;
|
|
24
|
+
}>;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
declare function exportPDF(options: ExportOptions): Promise<ExportResult>;
|
package/dist/server.js
CHANGED
|
@@ -5,10 +5,39 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
// src/export.ts
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import path from "path";
|
|
8
|
+
function discoverDocuments(root) {
|
|
9
|
+
const pdfsDir = path.join(root, "pdfs");
|
|
10
|
+
if (!fs.existsSync(pdfsDir)) return [];
|
|
11
|
+
const entries = fs.readdirSync(pdfsDir, { withFileTypes: true });
|
|
12
|
+
const documents = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (!entry.isDirectory()) continue;
|
|
15
|
+
const pagesDir = path.join(pdfsDir, entry.name, "pages");
|
|
16
|
+
if (!fs.existsSync(pagesDir)) continue;
|
|
17
|
+
const configPath = path.join(pdfsDir, entry.name, "config.ts");
|
|
18
|
+
documents.push({
|
|
19
|
+
slug: entry.name,
|
|
20
|
+
configPath: fs.existsSync(configPath) ? configPath : null
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return documents;
|
|
24
|
+
}
|
|
8
25
|
async function exportPDF(options) {
|
|
9
|
-
const { root,
|
|
10
|
-
const
|
|
11
|
-
|
|
26
|
+
const { root, document: documentSlug, output } = options;
|
|
27
|
+
const outputDir = path.resolve(root, output ?? "output");
|
|
28
|
+
let documents = discoverDocuments(root);
|
|
29
|
+
if (documentSlug) {
|
|
30
|
+
documents = documents.filter((d) => d.slug === documentSlug);
|
|
31
|
+
if (documents.length === 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Document "${documentSlug}" not found. Check that pdfs/${documentSlug}/pages/ exists.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (documents.length === 0) {
|
|
38
|
+
throw new Error("No documents found. Create at least one document in pdfs/<name>/pages/.");
|
|
39
|
+
}
|
|
40
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
12
41
|
let playwright;
|
|
13
42
|
try {
|
|
14
43
|
playwright = await import("playwright");
|
|
@@ -18,42 +47,58 @@ async function exportPDF(options) {
|
|
|
18
47
|
);
|
|
19
48
|
}
|
|
20
49
|
const { startPreview: startPreview2 } = await import("./server.js");
|
|
21
|
-
const server = await startPreview2({ root, port: 0,
|
|
50
|
+
const server = await startPreview2({ root, port: 0, open: false });
|
|
51
|
+
const vite = server._vite;
|
|
22
52
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
53
|
+
const outputs = [];
|
|
23
54
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
margin
|
|
55
|
+
for (const doc of documents) {
|
|
56
|
+
let config;
|
|
57
|
+
if (doc.configPath && vite) {
|
|
58
|
+
try {
|
|
59
|
+
const configModule = await vite.ssrLoadModule(doc.configPath);
|
|
60
|
+
config = configModule.default;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const filename = config?.output ?? `${doc.slug}.pdf`;
|
|
65
|
+
const outputPath = path.resolve(outputDir, filename);
|
|
66
|
+
const page = await browser.newPage();
|
|
67
|
+
await page.goto(`${server.url}/${doc.slug}`, { waitUntil: "networkidle" });
|
|
68
|
+
await page.waitForSelector("[data-pdf-smith-page]", { timeout: 3e4 });
|
|
69
|
+
const margin = { top: "0", bottom: "0", left: "0", right: "0" };
|
|
70
|
+
const pdfOptions = {
|
|
71
|
+
path: outputPath,
|
|
72
|
+
preferCSSPageSize: true,
|
|
73
|
+
printBackground: true,
|
|
74
|
+
margin
|
|
75
|
+
};
|
|
76
|
+
if (config?.pageNumbers?.enabled) {
|
|
77
|
+
pdfOptions.displayHeaderFooter = true;
|
|
78
|
+
pdfOptions.headerTemplate = "<span></span>";
|
|
79
|
+
pdfOptions.footerTemplate = config.pageNumbers.template ?? '<div style="font-size:10px;text-align:center;width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>';
|
|
80
|
+
margin.bottom = "40px";
|
|
81
|
+
}
|
|
82
|
+
await page.pdf(pdfOptions);
|
|
83
|
+
await page.close();
|
|
84
|
+
outputs.push({ document: doc.slug, outputPath });
|
|
39
85
|
}
|
|
40
|
-
await page.pdf(pdfOptions);
|
|
41
86
|
} finally {
|
|
42
87
|
await browser.close().catch(() => {
|
|
43
88
|
});
|
|
44
89
|
await server.close().catch(() => {
|
|
45
90
|
});
|
|
46
91
|
}
|
|
47
|
-
return {
|
|
92
|
+
return { outputs };
|
|
48
93
|
}
|
|
49
94
|
|
|
50
95
|
// src/server.ts
|
|
51
96
|
async function startPreview(options) {
|
|
52
|
-
const { root, port = 3e3,
|
|
97
|
+
const { root, port = 3e3, open = false } = options;
|
|
53
98
|
const { createServer } = await import("vite");
|
|
54
99
|
const react = (await import("@vitejs/plugin-react")).default;
|
|
55
100
|
const tailwindcss = (await import("@tailwindcss/vite")).default;
|
|
56
|
-
const { pdfSmithPreviewPlugin } = await import("./preview-plugin-
|
|
101
|
+
const { pdfSmithPreviewPlugin } = await import("./preview-plugin-QDLEMAEE.js");
|
|
57
102
|
const currentDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
58
103
|
const pkgRoot = path2.resolve(currentDir, "..");
|
|
59
104
|
const pkgSrcDir = path2.resolve(pkgRoot, "src");
|
|
@@ -75,7 +120,7 @@ async function startPreview(options) {
|
|
|
75
120
|
},
|
|
76
121
|
dedupe: ["react", "react-dom"]
|
|
77
122
|
},
|
|
78
|
-
plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir
|
|
123
|
+
plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir })],
|
|
79
124
|
optimizeDeps: {
|
|
80
125
|
include: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"]
|
|
81
126
|
}
|
|
@@ -87,7 +132,7 @@ async function startPreview(options) {
|
|
|
87
132
|
const address = server.httpServer?.address();
|
|
88
133
|
const resolvedPort = address?.port ?? port;
|
|
89
134
|
const url = `http://localhost:${resolvedPort}`;
|
|
90
|
-
|
|
135
|
+
const previewServer = {
|
|
91
136
|
close: async () => {
|
|
92
137
|
const httpServer = server.httpServer;
|
|
93
138
|
httpServer?.closeAllConnections?.();
|
|
@@ -96,6 +141,11 @@ async function startPreview(options) {
|
|
|
96
141
|
port: resolvedPort,
|
|
97
142
|
url
|
|
98
143
|
};
|
|
144
|
+
Object.defineProperty(previewServer, "_vite", {
|
|
145
|
+
value: server,
|
|
146
|
+
enumerable: false
|
|
147
|
+
});
|
|
148
|
+
return previewServer;
|
|
99
149
|
}
|
|
100
150
|
export {
|
|
101
151
|
exportPDF,
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.ts","../src/export.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport { exportPDF } from './export';\nexport type { ExportOptions, ExportResult } from './export-types';\nexport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport async function startPreview(options: PreviewOptions): Promise<PreviewServer> {\n const { root, port = 3000,
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/export.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport { exportPDF } from './export';\nexport type { ExportOptions, ExportResult } from './export-types';\nexport type { PreviewOptions, PreviewServer } from './preview/types';\n\nexport async function startPreview(options: PreviewOptions): Promise<PreviewServer> {\n const { root, port = 3000, open = false } = options;\n\n const { createServer } = await import('vite');\n const react = (await import('@vitejs/plugin-react')).default;\n const tailwindcss = (await import('@tailwindcss/vite')).default;\n const { pdfSmithPreviewPlugin } = await import('./preview/preview-plugin');\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.resolve(currentDir, '..');\n const pkgSrcDir = path.resolve(pkgRoot, 'src');\n\n const server = await createServer({\n configFile: false,\n appType: 'custom',\n root,\n server: {\n port,\n strictPort: port !== 0,\n open,\n fs: {\n allow: [root, pkgSrcDir, pkgRoot],\n },\n },\n resolve: {\n alias: {\n 'pdf-smith': path.resolve(pkgSrcDir, 'index.ts'),\n },\n dedupe: ['react', 'react-dom'],\n },\n plugins: [react(), tailwindcss(), pdfSmithPreviewPlugin({ pkgSrcDir })],\n optimizeDeps: {\n include: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],\n },\n });\n\n await server.listen();\n\n if (port !== 0) {\n server.printUrls();\n }\n\n const address = server.httpServer?.address() as AddressInfo | null;\n const resolvedPort = address?.port ?? port;\n const url = `http://localhost:${resolvedPort}`;\n\n const previewServer: PreviewServer = {\n close: async () => {\n const httpServer = server.httpServer as { closeAllConnections?: () => void } | null;\n httpServer?.closeAllConnections?.();\n await server.close();\n },\n port: resolvedPort,\n url,\n };\n\n Object.defineProperty(previewServer, '_vite', {\n value: server,\n enumerable: false,\n });\n\n return previewServer;\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport type { DocumentConfig } from './config';\nimport type { ExportOptions, ExportResult } from './export-types';\n\ninterface DiscoveredDocument {\n slug: string;\n configPath: string | null;\n}\n\nfunction discoverDocuments(root: string): DiscoveredDocument[] {\n const pdfsDir = path.join(root, 'pdfs');\n if (!fs.existsSync(pdfsDir)) return [];\n\n const entries = fs.readdirSync(pdfsDir, { withFileTypes: true });\n const documents: DiscoveredDocument[] = [];\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n const pagesDir = path.join(pdfsDir, entry.name, 'pages');\n if (!fs.existsSync(pagesDir)) continue;\n\n const configPath = path.join(pdfsDir, entry.name, 'config.ts');\n documents.push({\n slug: entry.name,\n configPath: fs.existsSync(configPath) ? configPath : null,\n });\n }\n\n return documents;\n}\n\nexport async function exportPDF(options: ExportOptions): Promise<ExportResult> {\n const { root, document: documentSlug, output } = options;\n const outputDir = path.resolve(root, output ?? 'output');\n\n let documents = discoverDocuments(root);\n\n if (documentSlug) {\n documents = documents.filter((d) => d.slug === documentSlug);\n if (documents.length === 0) {\n throw new Error(\n `Document \"${documentSlug}\" not found. Check that pdfs/${documentSlug}/pages/ exists.`,\n );\n }\n }\n\n if (documents.length === 0) {\n throw new Error('No documents found. Create at least one document in pdfs/<name>/pages/.');\n }\n\n fs.mkdirSync(outputDir, { recursive: true });\n\n let playwright: typeof import('playwright');\n try {\n playwright = await import('playwright');\n } catch {\n throw new Error(\n 'Playwright is required for PDF export. Install it with: npm install -D playwright && npx playwright install chromium',\n );\n }\n\n const { startPreview } = await import('./server');\n const server = await startPreview({ root, port: 0, open: false });\n\n const vite = (\n server as unknown as {\n _vite: { ssrLoadModule: (id: string) => Promise<{ default?: DocumentConfig }> };\n }\n )._vite;\n\n const browser = await playwright.chromium.launch({ headless: true });\n const outputs: ExportResult['outputs'] = [];\n\n try {\n for (const doc of documents) {\n let config: DocumentConfig | undefined;\n if (doc.configPath && vite) {\n try {\n const configModule = await vite.ssrLoadModule(doc.configPath);\n config = configModule.default;\n } catch {\n // Config loading failed, proceed without it\n }\n }\n\n const filename = config?.output ?? `${doc.slug}.pdf`;\n const outputPath = path.resolve(outputDir, filename);\n\n const page = await browser.newPage();\n await page.goto(`${server.url}/${doc.slug}`, { waitUntil: 'networkidle' });\n await page.waitForSelector('[data-pdf-smith-page]', { timeout: 30_000 });\n\n const margin = { top: '0', bottom: '0', left: '0', right: '0' };\n\n const pdfOptions: Parameters<typeof page.pdf>[0] = {\n path: outputPath,\n preferCSSPageSize: true,\n printBackground: true,\n margin,\n };\n\n if (config?.pageNumbers?.enabled) {\n pdfOptions.displayHeaderFooter = true;\n pdfOptions.headerTemplate = '<span></span>';\n pdfOptions.footerTemplate =\n config.pageNumbers.template ??\n '<div style=\"font-size:10px;text-align:center;width:100%;\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>';\n margin.bottom = '40px';\n }\n\n await page.pdf(pdfOptions);\n await page.close();\n\n outputs.push({ document: doc.slug, outputPath });\n }\n } finally {\n await browser.close().catch(() => {});\n await server.close().catch(() => {});\n }\n\n return { outputs };\n}\n"],"mappings":"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","names":["path","startPreview","path"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-smith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Build beautiful PDFs with React components and Tailwind CSS",
|
|
5
5
|
"author": "Kareem Elbahrawy",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
"react-pdf"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"pdf-smith": "./dist/cli.js"
|
|
25
|
+
},
|
|
23
26
|
"main": "./dist/index.cjs",
|
|
24
27
|
"module": "./dist/index.js",
|
|
25
28
|
"types": "./dist/index.d.ts",
|
|
@@ -47,13 +50,17 @@
|
|
|
47
50
|
},
|
|
48
51
|
"files": [
|
|
49
52
|
"dist",
|
|
53
|
+
"README.md",
|
|
50
54
|
"src/preview/",
|
|
51
55
|
"src/index.ts",
|
|
52
56
|
"src/page.tsx",
|
|
53
57
|
"src/document.tsx",
|
|
54
58
|
"src/types.ts",
|
|
55
59
|
"src/constants.ts",
|
|
56
|
-
"src/styles.ts"
|
|
60
|
+
"src/styles.ts",
|
|
61
|
+
"src/config.ts",
|
|
62
|
+
"src/add.ts",
|
|
63
|
+
"src/cli.ts"
|
|
57
64
|
],
|
|
58
65
|
"dependencies": {
|
|
59
66
|
"vite": "^7.0.0",
|
package/src/add.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SLUG_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
5
|
+
|
|
6
|
+
export interface AddDocumentOptions {
|
|
7
|
+
slug: string;
|
|
8
|
+
root: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AddDocumentResult {
|
|
12
|
+
paths: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toTitle(slug: string): string {
|
|
16
|
+
return slug
|
|
17
|
+
.split('-')
|
|
18
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
19
|
+
.join(' ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function addDocument({ slug, root }: AddDocumentOptions): AddDocumentResult {
|
|
23
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid slug "${slug}". Use lowercase letters, numbers, and hyphens (e.g. "my-report").`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const docDir = path.join(root, 'pdfs', slug);
|
|
30
|
+
|
|
31
|
+
if (fs.existsSync(docDir)) {
|
|
32
|
+
throw new Error(`Document "${slug}" already exists at ${docDir}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pagesDir = path.join(docDir, 'pages');
|
|
36
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const title = toTitle(slug);
|
|
39
|
+
|
|
40
|
+
const page1Content = `import '../../../styles.css';
|
|
41
|
+
|
|
42
|
+
export default function Page1() {
|
|
43
|
+
return (
|
|
44
|
+
<div className="h-full flex flex-col items-center justify-center p-12 font-sans">
|
|
45
|
+
<h1 className="text-4xl font-bold tracking-tight text-gray-900">${title}</h1>
|
|
46
|
+
<p className="mt-4 text-gray-500">Start editing this page to build your document.</p>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const configContent = `import { defineConfig } from 'pdf-smith';
|
|
53
|
+
|
|
54
|
+
export default defineConfig({
|
|
55
|
+
pageNumbers: { enabled: false },
|
|
56
|
+
});
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const page1Path = path.join(pagesDir, 'Page1.tsx');
|
|
60
|
+
const configPath = path.join(docDir, 'config.ts');
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(page1Path, page1Content);
|
|
63
|
+
fs.writeFileSync(configPath, configContent);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
paths: [
|
|
67
|
+
path.relative(root, pagesDir),
|
|
68
|
+
path.relative(root, page1Path),
|
|
69
|
+
path.relative(root, configPath),
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { addDocument } from './add';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const command = args[0];
|
|
5
|
+
|
|
6
|
+
if (command === 'add') {
|
|
7
|
+
const slug = args[1];
|
|
8
|
+
|
|
9
|
+
if (!slug || slug === '--help') {
|
|
10
|
+
console.log('Usage: pdf-smith add <slug>');
|
|
11
|
+
process.exit(slug ? 0 : 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
addDocument({ slug, root: process.cwd() });
|
|
16
|
+
console.log(`Created document '${slug}'. Start editing at pdfs/${slug}/pages/Page1.tsx`);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error((error as Error).message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
console.log('Usage: pdf-smith add <slug>');
|
|
23
|
+
process.exit(command === '--help' || command === '-h' ? 0 : 1);
|
|
24
|
+
}
|
package/src/config.ts
ADDED
package/src/index.ts
CHANGED
package/src/preview/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getDocuments } from 'virtual:pdf-smith-documents';
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Document } from '../document';
|
|
4
4
|
import { Page } from '../page';
|
|
@@ -26,15 +26,26 @@ const pageLabelStyle: React.CSSProperties = {
|
|
|
26
26
|
width: 'fit-content',
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
interface PreviewAppProps {
|
|
30
|
+
slug: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PreviewApp({ slug }: PreviewAppProps) {
|
|
34
|
+
const documents = getDocuments();
|
|
35
|
+
const doc = documents[slug];
|
|
36
|
+
const pages = doc?.pages ?? {};
|
|
31
37
|
const [activePage, setActivePage] = useState<string | null>(null);
|
|
32
38
|
|
|
33
39
|
const visiblePages = activePage ? { [activePage]: pages[activePage] } : pages;
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
42
|
<>
|
|
37
|
-
<Navigation
|
|
43
|
+
<Navigation
|
|
44
|
+
pages={pages}
|
|
45
|
+
activePage={activePage}
|
|
46
|
+
onSelectPage={setActivePage}
|
|
47
|
+
documentSlug={slug}
|
|
48
|
+
/>
|
|
38
49
|
<div data-pdf-smith-container="" style={containerStyle}>
|
|
39
50
|
<Document>
|
|
40
51
|
{Object.entries(visiblePages).map(([name, PageComponent]) => (
|
package/src/preview/entry.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createRoot } from 'react-dom/client';
|
|
2
2
|
import { PreviewApp } from './app';
|
|
3
|
+
import { HomePage } from './home';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
-
if (
|
|
6
|
-
|
|
5
|
+
const rootEl = document.getElementById('root');
|
|
6
|
+
if (rootEl) {
|
|
7
|
+
const path = window.location.pathname;
|
|
8
|
+
const slug = path === '/' ? null : path.replace(/^\//, '').replace(/\/$/, '');
|
|
9
|
+
|
|
10
|
+
createRoot(rootEl).render(slug ? <PreviewApp slug={slug} /> : <HomePage />);
|
|
7
11
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getDocuments } from 'virtual:pdf-smith-documents';
|
|
2
|
+
|
|
3
|
+
const containerStyle: React.CSSProperties = {
|
|
4
|
+
minHeight: '100vh',
|
|
5
|
+
background: '#1a1a2e',
|
|
6
|
+
color: '#e0e0e0',
|
|
7
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
padding: '48px 24px',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const titleStyle: React.CSSProperties = {
|
|
15
|
+
fontSize: '28px',
|
|
16
|
+
fontWeight: 700,
|
|
17
|
+
color: '#ffffff',
|
|
18
|
+
marginBottom: '8px',
|
|
19
|
+
letterSpacing: '0.5px',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const subtitleStyle: React.CSSProperties = {
|
|
23
|
+
fontSize: '14px',
|
|
24
|
+
color: '#888',
|
|
25
|
+
marginBottom: '40px',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const gridStyle: React.CSSProperties = {
|
|
29
|
+
display: 'grid',
|
|
30
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
|
|
31
|
+
gap: '16px',
|
|
32
|
+
width: '100%',
|
|
33
|
+
maxWidth: '800px',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const cardStyle: React.CSSProperties = {
|
|
37
|
+
display: 'block',
|
|
38
|
+
background: '#16213e',
|
|
39
|
+
borderRadius: '8px',
|
|
40
|
+
padding: '24px',
|
|
41
|
+
textDecoration: 'none',
|
|
42
|
+
color: '#e0e0e0',
|
|
43
|
+
transition: 'background 0.15s',
|
|
44
|
+
border: '1px solid #2a2a4a',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const cardTitleStyle: React.CSSProperties = {
|
|
48
|
+
fontSize: '18px',
|
|
49
|
+
fontWeight: 600,
|
|
50
|
+
color: '#ffffff',
|
|
51
|
+
marginBottom: '8px',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const cardMetaStyle: React.CSSProperties = {
|
|
55
|
+
fontSize: '13px',
|
|
56
|
+
color: '#888',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function HomePage() {
|
|
60
|
+
const documents = getDocuments();
|
|
61
|
+
const slugs = Object.keys(documents);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div style={containerStyle}>
|
|
65
|
+
<h1 style={titleStyle}>pdf-smith</h1>
|
|
66
|
+
<p style={subtitleStyle}>
|
|
67
|
+
{slugs.length} document{slugs.length !== 1 ? 's' : ''}
|
|
68
|
+
</p>
|
|
69
|
+
<div style={gridStyle}>
|
|
70
|
+
{slugs.map((slug) => {
|
|
71
|
+
const doc = documents[slug];
|
|
72
|
+
const pageCount = Object.keys(doc.pages).length;
|
|
73
|
+
return (
|
|
74
|
+
<a
|
|
75
|
+
key={slug}
|
|
76
|
+
href={`/${slug}`}
|
|
77
|
+
style={cardStyle}
|
|
78
|
+
onMouseEnter={(e) => {
|
|
79
|
+
e.currentTarget.style.background = '#1e2d50';
|
|
80
|
+
}}
|
|
81
|
+
onMouseLeave={(e) => {
|
|
82
|
+
e.currentTarget.style.background = '#16213e';
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<div style={cardTitleStyle}>{slug}</div>
|
|
86
|
+
<div style={cardMetaStyle}>
|
|
87
|
+
{pageCount} page{pageCount !== 1 ? 's' : ''}
|
|
88
|
+
</div>
|
|
89
|
+
</a>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -2,6 +2,7 @@ interface NavigationProps {
|
|
|
2
2
|
pages: Record<string, React.ComponentType>;
|
|
3
3
|
activePage: string | null;
|
|
4
4
|
onSelectPage: (page: string | null) => void;
|
|
5
|
+
documentSlug: string;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
const navStyle: React.CSSProperties = {
|
|
@@ -28,6 +29,15 @@ const titleStyle: React.CSSProperties = {
|
|
|
28
29
|
letterSpacing: '0.5px',
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
const backLinkStyle: React.CSSProperties = {
|
|
33
|
+
display: 'block',
|
|
34
|
+
fontSize: '13px',
|
|
35
|
+
color: '#888',
|
|
36
|
+
textDecoration: 'none',
|
|
37
|
+
marginBottom: '12px',
|
|
38
|
+
transition: 'color 0.15s',
|
|
39
|
+
};
|
|
40
|
+
|
|
31
41
|
const buttonBaseStyle: React.CSSProperties = {
|
|
32
42
|
display: 'block',
|
|
33
43
|
width: '100%',
|
|
@@ -57,12 +67,24 @@ const separatorStyle: React.CSSProperties = {
|
|
|
57
67
|
margin: '12px 0',
|
|
58
68
|
};
|
|
59
69
|
|
|
60
|
-
export function Navigation({ pages, activePage, onSelectPage }: NavigationProps) {
|
|
70
|
+
export function Navigation({ pages, activePage, onSelectPage, documentSlug }: NavigationProps) {
|
|
61
71
|
const pageNames = Object.keys(pages);
|
|
62
72
|
|
|
63
73
|
return (
|
|
64
74
|
<nav data-pdf-smith-nav="" style={navStyle}>
|
|
65
|
-
<
|
|
75
|
+
<a
|
|
76
|
+
href="/"
|
|
77
|
+
style={backLinkStyle}
|
|
78
|
+
onMouseEnter={(e) => {
|
|
79
|
+
e.currentTarget.style.color = '#e0e0e0';
|
|
80
|
+
}}
|
|
81
|
+
onMouseLeave={(e) => {
|
|
82
|
+
e.currentTarget.style.color = '#888';
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
← All Documents
|
|
86
|
+
</a>
|
|
87
|
+
<div style={titleStyle}>{documentSlug}</div>
|
|
66
88
|
<button
|
|
67
89
|
type="button"
|
|
68
90
|
style={getButtonStyle(activePage === null)}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const VIRTUAL_DOCUMENTS_ID = 'virtual:pdf-smith-documents';
|
|
4
|
+
const RESOLVED_VIRTUAL_DOCUMENTS_ID = `\0${VIRTUAL_DOCUMENTS_ID}`;
|
|
5
5
|
|
|
6
6
|
interface PreviewPluginOptions {
|
|
7
7
|
pkgSrcDir: string;
|
|
8
|
-
pagesGlob: string;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
export function pdfSmithPreviewPlugin({ pkgSrcDir
|
|
10
|
+
export function pdfSmithPreviewPlugin({ pkgSrcDir }: PreviewPluginOptions): Plugin {
|
|
12
11
|
return {
|
|
13
12
|
name: 'pdf-smith-preview',
|
|
14
13
|
|
|
15
14
|
configureServer(server) {
|
|
16
15
|
return () => {
|
|
17
16
|
server.middlewares.use((req, res, next) => {
|
|
18
|
-
|
|
17
|
+
const url = req.url ?? '';
|
|
18
|
+
|
|
19
|
+
// Skip file requests, Vite internals, and node_modules
|
|
20
|
+
if (url.includes('.') || url.startsWith('/@') || url.startsWith('/node_modules/')) {
|
|
19
21
|
next();
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
@@ -65,25 +67,44 @@ window.__vite_plugin_react_preamble_installed__ = true
|
|
|
65
67
|
},
|
|
66
68
|
|
|
67
69
|
resolveId(id) {
|
|
68
|
-
if (id ===
|
|
69
|
-
return
|
|
70
|
+
if (id === VIRTUAL_DOCUMENTS_ID) {
|
|
71
|
+
return RESOLVED_VIRTUAL_DOCUMENTS_ID;
|
|
70
72
|
}
|
|
71
73
|
},
|
|
72
74
|
|
|
73
75
|
load(id) {
|
|
74
|
-
if (id ===
|
|
76
|
+
if (id === RESOLVED_VIRTUAL_DOCUMENTS_ID) {
|
|
75
77
|
return `
|
|
76
|
-
const
|
|
78
|
+
const pageModules = import.meta.glob('/pdfs/*/pages/**/*.tsx', { eager: true });
|
|
79
|
+
const configModules = import.meta.glob('/pdfs/*/config.ts', { eager: true });
|
|
80
|
+
|
|
81
|
+
export function getDocuments() {
|
|
82
|
+
const documents = {};
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
for (const [path, mod] of Object.entries(pageModules)) {
|
|
85
|
+
const match = path.match(/^\\/pdfs\\/([^/]+)\\/pages\\/(.+)\\.[^.]+$/);
|
|
86
|
+
if (!match) continue;
|
|
87
|
+
const [, slug, pageName] = match;
|
|
88
|
+
if (!documents[slug]) {
|
|
89
|
+
documents[slug] = { slug, pages: {}, config: undefined };
|
|
90
|
+
}
|
|
91
|
+
documents[slug].pages[pageName] = mod.default;
|
|
85
92
|
}
|
|
86
|
-
|
|
93
|
+
|
|
94
|
+
for (const [path, mod] of Object.entries(configModules)) {
|
|
95
|
+
const match = path.match(/^\\/pdfs\\/([^/]+)\\/config\\.ts$/);
|
|
96
|
+
if (!match) continue;
|
|
97
|
+
const slug = match[1];
|
|
98
|
+
if (documents[slug]) {
|
|
99
|
+
documents[slug].config = mod.default;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return documents;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getDocumentSlugs() {
|
|
107
|
+
return Object.keys(getDocuments());
|
|
87
108
|
}
|
|
88
109
|
`;
|
|
89
110
|
}
|
package/src/preview/types.ts
CHANGED