pdfx-cli 0.0.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/LICENSE +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4429 -0
- package/dist/mcp/index.d.ts +38 -0
- package/dist/mcp/index.js +1261 -0
- package/package.json +60 -12
- package/README.md +0 -10
- package/index.js +0 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,4429 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import chalk10 from "chalk";
|
|
8
|
+
import { Command as Command3 } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/commands/add.ts
|
|
11
|
+
import fs5 from "fs";
|
|
12
|
+
import path4 from "path";
|
|
13
|
+
|
|
14
|
+
// ../shared/src/schemas.ts
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
var colorTokensSchema = z.object({
|
|
17
|
+
foreground: z.string().min(1),
|
|
18
|
+
background: z.string().min(1),
|
|
19
|
+
muted: z.string().min(1),
|
|
20
|
+
mutedForeground: z.string().min(1),
|
|
21
|
+
primary: z.string().min(1),
|
|
22
|
+
primaryForeground: z.string().min(1),
|
|
23
|
+
border: z.string().min(1),
|
|
24
|
+
accent: z.string().min(1),
|
|
25
|
+
destructive: z.string().min(1),
|
|
26
|
+
success: z.string().min(1),
|
|
27
|
+
warning: z.string().min(1),
|
|
28
|
+
info: z.string().min(1)
|
|
29
|
+
});
|
|
30
|
+
var headingFontSizeSchema = z.object({
|
|
31
|
+
h1: z.number().positive(),
|
|
32
|
+
h2: z.number().positive(),
|
|
33
|
+
h3: z.number().positive(),
|
|
34
|
+
h4: z.number().positive(),
|
|
35
|
+
h5: z.number().positive(),
|
|
36
|
+
h6: z.number().positive()
|
|
37
|
+
});
|
|
38
|
+
var typographyTokensSchema = z.object({
|
|
39
|
+
body: z.object({
|
|
40
|
+
fontFamily: z.string().min(1),
|
|
41
|
+
fontSize: z.number().positive(),
|
|
42
|
+
lineHeight: z.number().positive()
|
|
43
|
+
}),
|
|
44
|
+
heading: z.object({
|
|
45
|
+
fontFamily: z.string().min(1),
|
|
46
|
+
fontWeight: z.number().int().min(100).max(900),
|
|
47
|
+
lineHeight: z.number().positive(),
|
|
48
|
+
fontSize: headingFontSizeSchema
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
var spacingTokensSchema = z.object({
|
|
52
|
+
page: z.object({
|
|
53
|
+
marginTop: z.number().min(0),
|
|
54
|
+
marginRight: z.number().min(0),
|
|
55
|
+
marginBottom: z.number().min(0),
|
|
56
|
+
marginLeft: z.number().min(0)
|
|
57
|
+
}),
|
|
58
|
+
sectionGap: z.number().min(0),
|
|
59
|
+
paragraphGap: z.number().min(0),
|
|
60
|
+
componentGap: z.number().min(0)
|
|
61
|
+
});
|
|
62
|
+
var pageTokensSchema = z.object({
|
|
63
|
+
size: z.enum(["A4", "LETTER", "LEGAL"]),
|
|
64
|
+
orientation: z.enum(["portrait", "landscape"])
|
|
65
|
+
});
|
|
66
|
+
var primitiveTokensSchema = z.object({
|
|
67
|
+
typography: z.record(z.number()),
|
|
68
|
+
spacing: z.record(z.number()),
|
|
69
|
+
fontWeights: z.object({
|
|
70
|
+
regular: z.number(),
|
|
71
|
+
medium: z.number(),
|
|
72
|
+
semibold: z.number(),
|
|
73
|
+
bold: z.number()
|
|
74
|
+
}),
|
|
75
|
+
lineHeights: z.object({
|
|
76
|
+
tight: z.number(),
|
|
77
|
+
normal: z.number(),
|
|
78
|
+
relaxed: z.number()
|
|
79
|
+
}),
|
|
80
|
+
borderRadius: z.object({
|
|
81
|
+
none: z.number(),
|
|
82
|
+
sm: z.number(),
|
|
83
|
+
md: z.number(),
|
|
84
|
+
lg: z.number(),
|
|
85
|
+
full: z.number()
|
|
86
|
+
}),
|
|
87
|
+
letterSpacing: z.object({
|
|
88
|
+
tight: z.number(),
|
|
89
|
+
normal: z.number(),
|
|
90
|
+
wide: z.number(),
|
|
91
|
+
wider: z.number()
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
var themeSchema = z.object({
|
|
95
|
+
name: z.string().min(1),
|
|
96
|
+
primitives: primitiveTokensSchema,
|
|
97
|
+
colors: colorTokensSchema,
|
|
98
|
+
typography: typographyTokensSchema,
|
|
99
|
+
spacing: spacingTokensSchema,
|
|
100
|
+
page: pageTokensSchema
|
|
101
|
+
});
|
|
102
|
+
var configSchema = z.object({
|
|
103
|
+
$schema: z.string().optional(),
|
|
104
|
+
componentDir: z.string().min(1, "componentDir must not be empty"),
|
|
105
|
+
registry: z.string().url("registry must be a valid URL"),
|
|
106
|
+
theme: z.string().min(1).optional(),
|
|
107
|
+
blockDir: z.string().min(1).optional()
|
|
108
|
+
});
|
|
109
|
+
var registryFileTypes = [
|
|
110
|
+
"registry:component",
|
|
111
|
+
"registry:lib",
|
|
112
|
+
"registry:style",
|
|
113
|
+
"registry:template",
|
|
114
|
+
"registry:block",
|
|
115
|
+
"registry:file"
|
|
116
|
+
];
|
|
117
|
+
var registryFileSchema = z.object({
|
|
118
|
+
path: z.string().min(1),
|
|
119
|
+
content: z.string(),
|
|
120
|
+
type: z.enum(registryFileTypes)
|
|
121
|
+
});
|
|
122
|
+
var registryItemSchema = z.object({
|
|
123
|
+
name: z.string().min(1),
|
|
124
|
+
type: z.string().optional(),
|
|
125
|
+
title: z.string().optional(),
|
|
126
|
+
description: z.string().optional(),
|
|
127
|
+
files: z.array(registryFileSchema).min(1, "Component must have at least one file"),
|
|
128
|
+
dependencies: z.array(z.string()).optional(),
|
|
129
|
+
devDependencies: z.array(z.string()).optional(),
|
|
130
|
+
registryDependencies: z.array(z.string()).optional(),
|
|
131
|
+
peerComponents: z.array(z.string()).optional()
|
|
132
|
+
});
|
|
133
|
+
var registryIndexItemSchema = z.object({
|
|
134
|
+
name: z.string().min(1),
|
|
135
|
+
type: z.string(),
|
|
136
|
+
title: z.string(),
|
|
137
|
+
description: z.string(),
|
|
138
|
+
files: z.array(
|
|
139
|
+
z.object({
|
|
140
|
+
path: z.string().min(1),
|
|
141
|
+
type: z.string()
|
|
142
|
+
})
|
|
143
|
+
),
|
|
144
|
+
dependencies: z.array(z.string()).optional(),
|
|
145
|
+
devDependencies: z.array(z.string()).optional(),
|
|
146
|
+
registryDependencies: z.array(z.string()).optional(),
|
|
147
|
+
peerComponents: z.array(z.string()).optional()
|
|
148
|
+
});
|
|
149
|
+
var registrySchema = z.object({
|
|
150
|
+
$schema: z.string(),
|
|
151
|
+
name: z.string(),
|
|
152
|
+
homepage: z.string(),
|
|
153
|
+
items: z.array(registryIndexItemSchema)
|
|
154
|
+
});
|
|
155
|
+
var componentNameSchema = z.string().regex(
|
|
156
|
+
/^[a-z][a-z0-9-]*$/,
|
|
157
|
+
"Component name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens"
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// ../shared/src/themes/primitives.ts
|
|
161
|
+
var defaultPrimitives = {
|
|
162
|
+
typography: {
|
|
163
|
+
xs: 10,
|
|
164
|
+
sm: 12,
|
|
165
|
+
base: 15,
|
|
166
|
+
lg: 18,
|
|
167
|
+
xl: 22,
|
|
168
|
+
"2xl": 28,
|
|
169
|
+
"3xl": 36
|
|
170
|
+
},
|
|
171
|
+
spacing: {
|
|
172
|
+
0: 0,
|
|
173
|
+
0.5: 2,
|
|
174
|
+
1: 4,
|
|
175
|
+
2: 8,
|
|
176
|
+
3: 12,
|
|
177
|
+
4: 16,
|
|
178
|
+
5: 20,
|
|
179
|
+
6: 24,
|
|
180
|
+
8: 32,
|
|
181
|
+
10: 40,
|
|
182
|
+
12: 48,
|
|
183
|
+
16: 64
|
|
184
|
+
},
|
|
185
|
+
fontWeights: {
|
|
186
|
+
regular: 400,
|
|
187
|
+
medium: 500,
|
|
188
|
+
semibold: 600,
|
|
189
|
+
bold: 700
|
|
190
|
+
},
|
|
191
|
+
lineHeights: {
|
|
192
|
+
tight: 1.2,
|
|
193
|
+
normal: 1.4,
|
|
194
|
+
relaxed: 1.6
|
|
195
|
+
},
|
|
196
|
+
borderRadius: {
|
|
197
|
+
none: 0,
|
|
198
|
+
sm: 2,
|
|
199
|
+
md: 4,
|
|
200
|
+
lg: 8,
|
|
201
|
+
full: 9999
|
|
202
|
+
},
|
|
203
|
+
letterSpacing: {
|
|
204
|
+
tight: -0.025,
|
|
205
|
+
normal: 0,
|
|
206
|
+
wide: 0.025,
|
|
207
|
+
wider: 0.05
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ../shared/src/themes/professional.ts
|
|
212
|
+
var professionalTheme = {
|
|
213
|
+
name: "professional",
|
|
214
|
+
primitives: defaultPrimitives,
|
|
215
|
+
colors: {
|
|
216
|
+
foreground: "#18181b",
|
|
217
|
+
background: "#ffffff",
|
|
218
|
+
muted: "#f4f4f5",
|
|
219
|
+
mutedForeground: "#71717a",
|
|
220
|
+
primary: "#18181b",
|
|
221
|
+
primaryForeground: "#ffffff",
|
|
222
|
+
border: "#e4e4e7",
|
|
223
|
+
accent: "#3b82f6",
|
|
224
|
+
destructive: "#dc2626",
|
|
225
|
+
success: "#16a34a",
|
|
226
|
+
warning: "#d97706",
|
|
227
|
+
info: "#0ea5e9"
|
|
228
|
+
},
|
|
229
|
+
typography: {
|
|
230
|
+
body: {
|
|
231
|
+
fontFamily: "Helvetica",
|
|
232
|
+
fontSize: 11,
|
|
233
|
+
lineHeight: 1.6
|
|
234
|
+
},
|
|
235
|
+
heading: {
|
|
236
|
+
fontFamily: "Times-Roman",
|
|
237
|
+
fontWeight: 700,
|
|
238
|
+
lineHeight: 1.25,
|
|
239
|
+
fontSize: {
|
|
240
|
+
h1: 32,
|
|
241
|
+
h2: 24,
|
|
242
|
+
h3: 20,
|
|
243
|
+
h4: 16,
|
|
244
|
+
h5: 14,
|
|
245
|
+
h6: 12
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
spacing: {
|
|
250
|
+
page: {
|
|
251
|
+
marginTop: 56,
|
|
252
|
+
marginRight: 48,
|
|
253
|
+
marginBottom: 56,
|
|
254
|
+
marginLeft: 48
|
|
255
|
+
},
|
|
256
|
+
sectionGap: 28,
|
|
257
|
+
paragraphGap: 10,
|
|
258
|
+
componentGap: 14
|
|
259
|
+
},
|
|
260
|
+
page: {
|
|
261
|
+
size: "A4",
|
|
262
|
+
orientation: "portrait"
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// ../shared/src/themes/modern.ts
|
|
267
|
+
var modernTheme = {
|
|
268
|
+
name: "modern",
|
|
269
|
+
primitives: defaultPrimitives,
|
|
270
|
+
colors: {
|
|
271
|
+
foreground: "#0f172a",
|
|
272
|
+
background: "#ffffff",
|
|
273
|
+
muted: "#f1f5f9",
|
|
274
|
+
mutedForeground: "#64748b",
|
|
275
|
+
primary: "#334155",
|
|
276
|
+
primaryForeground: "#ffffff",
|
|
277
|
+
border: "#e2e8f0",
|
|
278
|
+
accent: "#6366f1",
|
|
279
|
+
destructive: "#ef4444",
|
|
280
|
+
success: "#22c55e",
|
|
281
|
+
warning: "#f59e0b",
|
|
282
|
+
info: "#3b82f6"
|
|
283
|
+
},
|
|
284
|
+
typography: {
|
|
285
|
+
body: {
|
|
286
|
+
fontFamily: "Helvetica",
|
|
287
|
+
fontSize: 11,
|
|
288
|
+
lineHeight: 1.6
|
|
289
|
+
},
|
|
290
|
+
heading: {
|
|
291
|
+
fontFamily: "Helvetica",
|
|
292
|
+
fontWeight: 600,
|
|
293
|
+
lineHeight: 1.25,
|
|
294
|
+
fontSize: {
|
|
295
|
+
h1: 28,
|
|
296
|
+
h2: 22,
|
|
297
|
+
h3: 18,
|
|
298
|
+
h4: 16,
|
|
299
|
+
h5: 14,
|
|
300
|
+
h6: 12
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
spacing: {
|
|
305
|
+
page: {
|
|
306
|
+
marginTop: 40,
|
|
307
|
+
marginRight: 40,
|
|
308
|
+
marginBottom: 40,
|
|
309
|
+
marginLeft: 40
|
|
310
|
+
},
|
|
311
|
+
sectionGap: 24,
|
|
312
|
+
paragraphGap: 10,
|
|
313
|
+
componentGap: 12
|
|
314
|
+
},
|
|
315
|
+
page: {
|
|
316
|
+
size: "A4",
|
|
317
|
+
orientation: "portrait"
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// ../shared/src/themes/minimal.ts
|
|
322
|
+
var minimalTheme = {
|
|
323
|
+
name: "minimal",
|
|
324
|
+
primitives: defaultPrimitives,
|
|
325
|
+
colors: {
|
|
326
|
+
foreground: "#18181b",
|
|
327
|
+
background: "#ffffff",
|
|
328
|
+
muted: "#fafafa",
|
|
329
|
+
mutedForeground: "#a1a1aa",
|
|
330
|
+
primary: "#18181b",
|
|
331
|
+
primaryForeground: "#ffffff",
|
|
332
|
+
border: "#e4e4e7",
|
|
333
|
+
accent: "#71717a",
|
|
334
|
+
destructive: "#b91c1c",
|
|
335
|
+
success: "#15803d",
|
|
336
|
+
warning: "#a16207",
|
|
337
|
+
info: "#0369a1"
|
|
338
|
+
},
|
|
339
|
+
typography: {
|
|
340
|
+
body: {
|
|
341
|
+
fontFamily: "Helvetica",
|
|
342
|
+
fontSize: 11,
|
|
343
|
+
lineHeight: 1.65
|
|
344
|
+
},
|
|
345
|
+
heading: {
|
|
346
|
+
fontFamily: "Courier",
|
|
347
|
+
fontWeight: 600,
|
|
348
|
+
lineHeight: 1.25,
|
|
349
|
+
fontSize: {
|
|
350
|
+
h1: 24,
|
|
351
|
+
h2: 20,
|
|
352
|
+
h3: 16,
|
|
353
|
+
h4: 14,
|
|
354
|
+
h5: 12,
|
|
355
|
+
h6: 10
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
spacing: {
|
|
360
|
+
page: {
|
|
361
|
+
marginTop: 72,
|
|
362
|
+
marginRight: 56,
|
|
363
|
+
marginBottom: 72,
|
|
364
|
+
marginLeft: 56
|
|
365
|
+
},
|
|
366
|
+
sectionGap: 36,
|
|
367
|
+
paragraphGap: 14,
|
|
368
|
+
componentGap: 18
|
|
369
|
+
},
|
|
370
|
+
page: {
|
|
371
|
+
size: "A4",
|
|
372
|
+
orientation: "portrait"
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// ../shared/src/themes/index.ts
|
|
377
|
+
var themePresets = {
|
|
378
|
+
professional: professionalTheme,
|
|
379
|
+
modern: modernTheme,
|
|
380
|
+
minimal: minimalTheme
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// ../shared/src/errors.ts
|
|
384
|
+
var PdfxError = class extends Error {
|
|
385
|
+
constructor(message, code, suggestion) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.code = code;
|
|
388
|
+
this.suggestion = suggestion;
|
|
389
|
+
this.name = "PdfxError";
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
var ConfigError = class extends PdfxError {
|
|
393
|
+
constructor(message, suggestion) {
|
|
394
|
+
super(message, "CONFIG_ERROR", suggestion);
|
|
395
|
+
this.name = "ConfigError";
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
var RegistryError = class extends PdfxError {
|
|
399
|
+
constructor(message, suggestion) {
|
|
400
|
+
super(message, "REGISTRY_ERROR", suggestion);
|
|
401
|
+
this.name = "RegistryError";
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
var NetworkError = class extends PdfxError {
|
|
405
|
+
constructor(message, suggestion) {
|
|
406
|
+
super(
|
|
407
|
+
message,
|
|
408
|
+
"NETWORK_ERROR",
|
|
409
|
+
suggestion ?? "Check your internet connection and registry URL"
|
|
410
|
+
);
|
|
411
|
+
this.name = "NetworkError";
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var ValidationError = class extends PdfxError {
|
|
415
|
+
constructor(message, suggestion) {
|
|
416
|
+
super(message, "VALIDATION_ERROR", suggestion);
|
|
417
|
+
this.name = "ValidationError";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/commands/add.ts
|
|
422
|
+
import chalk from "chalk";
|
|
423
|
+
import { execa } from "execa";
|
|
424
|
+
import ora from "ora";
|
|
425
|
+
import prompts from "prompts";
|
|
426
|
+
|
|
427
|
+
// src/constants.ts
|
|
428
|
+
var DEFAULTS = {
|
|
429
|
+
REGISTRY_URL: "https://pdfx.akashpise.dev/r",
|
|
430
|
+
SCHEMA_URL: "https://pdfx.akashpise.dev/schema.json",
|
|
431
|
+
COMPONENT_DIR: "./src/components/pdfx",
|
|
432
|
+
THEME_FILE: "./src/lib/pdfx-theme.ts",
|
|
433
|
+
BLOCK_DIR: "./src/blocks/pdfx"
|
|
434
|
+
};
|
|
435
|
+
var REGISTRY_SUBPATHS = {
|
|
436
|
+
BLOCKS: "blocks"
|
|
437
|
+
};
|
|
438
|
+
var REQUIRED_VERSIONS = {
|
|
439
|
+
"@react-pdf/renderer": ">=3.0.0",
|
|
440
|
+
react: ">=16.8.0",
|
|
441
|
+
node: ">=20.0.0"
|
|
442
|
+
};
|
|
443
|
+
var FETCH_TIMEOUT_MS = 1e4;
|
|
444
|
+
|
|
445
|
+
// src/utils/dependency-validator.ts
|
|
446
|
+
import fs2 from "fs";
|
|
447
|
+
import path from "path";
|
|
448
|
+
import semver from "semver";
|
|
449
|
+
|
|
450
|
+
// src/utils/read-json.ts
|
|
451
|
+
import fs from "fs";
|
|
452
|
+
function readJsonFile(filePath) {
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
457
|
+
throw new ConfigError(`Failed to read ${filePath}: ${details}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/utils/dependency-validator.ts
|
|
462
|
+
function getPackageJson(cwd = process.cwd()) {
|
|
463
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
464
|
+
if (!fs2.existsSync(pkgPath)) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return readJsonFile(pkgPath);
|
|
468
|
+
}
|
|
469
|
+
function getInstalledVersion(packageName, cwd = process.cwd(), pkg) {
|
|
470
|
+
const resolved = pkg !== void 0 ? pkg : getPackageJson(cwd);
|
|
471
|
+
if (!resolved) return null;
|
|
472
|
+
const deps = {
|
|
473
|
+
...resolved.dependencies,
|
|
474
|
+
...resolved.devDependencies
|
|
475
|
+
};
|
|
476
|
+
const version = deps[packageName];
|
|
477
|
+
if (!version) return null;
|
|
478
|
+
return semver.clean(version) || semver.coerce(version)?.version || null;
|
|
479
|
+
}
|
|
480
|
+
function validateReactPdfRenderer(cwd = process.cwd(), pkg) {
|
|
481
|
+
const version = getInstalledVersion("@react-pdf/renderer", cwd, pkg);
|
|
482
|
+
const required = REQUIRED_VERSIONS["@react-pdf/renderer"];
|
|
483
|
+
if (!version) {
|
|
484
|
+
return {
|
|
485
|
+
valid: false,
|
|
486
|
+
installed: false,
|
|
487
|
+
requiredVersion: required,
|
|
488
|
+
message: "@react-pdf/renderer is not installed"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const isCompatible = semver.satisfies(version, required);
|
|
492
|
+
return {
|
|
493
|
+
valid: isCompatible,
|
|
494
|
+
installed: true,
|
|
495
|
+
currentVersion: version,
|
|
496
|
+
requiredVersion: required,
|
|
497
|
+
message: isCompatible ? "@react-pdf/renderer version is compatible" : `@react-pdf/renderer version ${version} does not meet requirement ${required}`
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function validateReact(cwd = process.cwd(), pkg) {
|
|
501
|
+
const version = getInstalledVersion("react", cwd, pkg);
|
|
502
|
+
const required = REQUIRED_VERSIONS.react;
|
|
503
|
+
if (!version) {
|
|
504
|
+
return {
|
|
505
|
+
valid: false,
|
|
506
|
+
installed: false,
|
|
507
|
+
requiredVersion: required,
|
|
508
|
+
message: "React is not installed"
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const isCompatible = semver.satisfies(version, required);
|
|
512
|
+
return {
|
|
513
|
+
valid: isCompatible,
|
|
514
|
+
installed: true,
|
|
515
|
+
currentVersion: version,
|
|
516
|
+
requiredVersion: required,
|
|
517
|
+
message: isCompatible ? "React version is compatible" : `React version ${version} does not meet requirement ${required}`
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function validateNodeVersion() {
|
|
521
|
+
const version = process.version;
|
|
522
|
+
const required = REQUIRED_VERSIONS.node;
|
|
523
|
+
const cleanVersion = semver.clean(version);
|
|
524
|
+
if (!cleanVersion) {
|
|
525
|
+
return {
|
|
526
|
+
valid: false,
|
|
527
|
+
installed: true,
|
|
528
|
+
currentVersion: version,
|
|
529
|
+
requiredVersion: required,
|
|
530
|
+
message: `Unable to parse Node.js version: ${version}`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const isCompatible = semver.satisfies(cleanVersion, required);
|
|
534
|
+
return {
|
|
535
|
+
valid: isCompatible,
|
|
536
|
+
installed: true,
|
|
537
|
+
currentVersion: cleanVersion,
|
|
538
|
+
requiredVersion: required,
|
|
539
|
+
message: isCompatible ? "Node.js version is compatible" : `Node.js version ${cleanVersion} does not meet requirement ${required}`
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function validateDependencies(cwd = process.cwd()) {
|
|
543
|
+
const pkg = getPackageJson(cwd);
|
|
544
|
+
return {
|
|
545
|
+
reactPdfRenderer: validateReactPdfRenderer(cwd, pkg),
|
|
546
|
+
react: validateReact(cwd, pkg),
|
|
547
|
+
nodeJs: validateNodeVersion()
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/utils/file-system.ts
|
|
552
|
+
import fs3 from "fs";
|
|
553
|
+
import path2 from "path";
|
|
554
|
+
function ensureDir(dir) {
|
|
555
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
function writeFile(filePath, content) {
|
|
558
|
+
const dir = path2.dirname(filePath);
|
|
559
|
+
ensureDir(dir);
|
|
560
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
561
|
+
}
|
|
562
|
+
function checkFileExists(filePath) {
|
|
563
|
+
return fs3.existsSync(filePath);
|
|
564
|
+
}
|
|
565
|
+
function normalizePath(...segments) {
|
|
566
|
+
return path2.resolve(...segments);
|
|
567
|
+
}
|
|
568
|
+
function isPathWithinDirectory(resolvedPath, targetDir) {
|
|
569
|
+
const normalizedTarget = normalizePath(targetDir);
|
|
570
|
+
const normalizedResolved = normalizePath(resolvedPath);
|
|
571
|
+
if (normalizedResolved === normalizedTarget) return true;
|
|
572
|
+
const prefix = normalizedTarget.endsWith(path2.sep) ? normalizedTarget : normalizedTarget + path2.sep;
|
|
573
|
+
return normalizedResolved.startsWith(prefix);
|
|
574
|
+
}
|
|
575
|
+
function safePath(targetDir, fileName) {
|
|
576
|
+
const resolved = normalizePath(targetDir, fileName);
|
|
577
|
+
const normalizedTarget = normalizePath(targetDir);
|
|
578
|
+
if (!isPathWithinDirectory(resolved, normalizedTarget)) {
|
|
579
|
+
throw new Error(`Path "${fileName}" escapes target directory "${normalizedTarget}"`);
|
|
580
|
+
}
|
|
581
|
+
return resolved;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/utils/generate-theme.ts
|
|
585
|
+
function generateThemeFile(preset) {
|
|
586
|
+
return `// Generated by pdfx init.
|
|
587
|
+
// See: https://pdfx.akashpise.dev/installation#theming
|
|
588
|
+
//
|
|
589
|
+
// This module is the single source of truth for PDFX styling tokens.
|
|
590
|
+
// Update the exported \`theme\` object to customize component styles.
|
|
591
|
+
|
|
592
|
+
interface PdfxTheme {
|
|
593
|
+
name: string;
|
|
594
|
+
primitives: {
|
|
595
|
+
typography: Record<string, number>;
|
|
596
|
+
spacing: Record<string | number, number>;
|
|
597
|
+
fontWeights: { regular: number; medium: number; semibold: number; bold: number };
|
|
598
|
+
lineHeights: { tight: number; normal: number; relaxed: number };
|
|
599
|
+
borderRadius: { none: number; sm: number; md: number; lg: number; full: number };
|
|
600
|
+
letterSpacing: { tight: number; normal: number; wide: number; wider: number };
|
|
601
|
+
};
|
|
602
|
+
colors: {
|
|
603
|
+
foreground: string;
|
|
604
|
+
background: string;
|
|
605
|
+
muted: string;
|
|
606
|
+
mutedForeground: string;
|
|
607
|
+
primary: string;
|
|
608
|
+
primaryForeground: string;
|
|
609
|
+
border: string;
|
|
610
|
+
accent: string;
|
|
611
|
+
destructive: string;
|
|
612
|
+
success: string;
|
|
613
|
+
warning: string;
|
|
614
|
+
info: string;
|
|
615
|
+
};
|
|
616
|
+
typography: {
|
|
617
|
+
body: { fontFamily: string; fontSize: number; lineHeight: number };
|
|
618
|
+
heading: {
|
|
619
|
+
fontFamily: string;
|
|
620
|
+
fontWeight: number;
|
|
621
|
+
lineHeight: number;
|
|
622
|
+
fontSize: { h1: number; h2: number; h3: number; h4: number; h5: number; h6: number };
|
|
623
|
+
};
|
|
624
|
+
};
|
|
625
|
+
spacing: {
|
|
626
|
+
page: { marginTop: number; marginRight: number; marginBottom: number; marginLeft: number };
|
|
627
|
+
sectionGap: number;
|
|
628
|
+
paragraphGap: number;
|
|
629
|
+
componentGap: number;
|
|
630
|
+
};
|
|
631
|
+
page: {
|
|
632
|
+
size: 'A4' | 'LETTER' | 'LEGAL';
|
|
633
|
+
orientation: 'portrait' | 'landscape';
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export const theme: PdfxTheme = ${serializeTheme(preset)};
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
640
|
+
function serializeTheme(t) {
|
|
641
|
+
return `{
|
|
642
|
+
name: '${t.name}',
|
|
643
|
+
|
|
644
|
+
// Primitive scales.
|
|
645
|
+
primitives: {
|
|
646
|
+
typography: {
|
|
647
|
+
xs: ${t.primitives.typography.xs},
|
|
648
|
+
sm: ${t.primitives.typography.sm},
|
|
649
|
+
base: ${t.primitives.typography.base},
|
|
650
|
+
lg: ${t.primitives.typography.lg},
|
|
651
|
+
xl: ${t.primitives.typography.xl},
|
|
652
|
+
'2xl': ${t.primitives.typography["2xl"]},
|
|
653
|
+
'3xl': ${t.primitives.typography["3xl"]},
|
|
654
|
+
},
|
|
655
|
+
spacing: {
|
|
656
|
+
0: ${t.primitives.spacing[0]},
|
|
657
|
+
0.5: ${t.primitives.spacing[0.5]},
|
|
658
|
+
1: ${t.primitives.spacing[1]},
|
|
659
|
+
2: ${t.primitives.spacing[2]},
|
|
660
|
+
3: ${t.primitives.spacing[3]},
|
|
661
|
+
4: ${t.primitives.spacing[4]},
|
|
662
|
+
5: ${t.primitives.spacing[5]},
|
|
663
|
+
6: ${t.primitives.spacing[6]},
|
|
664
|
+
8: ${t.primitives.spacing[8]},
|
|
665
|
+
10: ${t.primitives.spacing[10]},
|
|
666
|
+
12: ${t.primitives.spacing[12]},
|
|
667
|
+
16: ${t.primitives.spacing[16]},
|
|
668
|
+
},
|
|
669
|
+
fontWeights: {
|
|
670
|
+
regular: ${t.primitives.fontWeights.regular},
|
|
671
|
+
medium: ${t.primitives.fontWeights.medium},
|
|
672
|
+
semibold: ${t.primitives.fontWeights.semibold},
|
|
673
|
+
bold: ${t.primitives.fontWeights.bold},
|
|
674
|
+
},
|
|
675
|
+
lineHeights: {
|
|
676
|
+
tight: ${t.primitives.lineHeights.tight},
|
|
677
|
+
normal: ${t.primitives.lineHeights.normal},
|
|
678
|
+
relaxed: ${t.primitives.lineHeights.relaxed},
|
|
679
|
+
},
|
|
680
|
+
borderRadius: {
|
|
681
|
+
none: ${t.primitives.borderRadius.none},
|
|
682
|
+
sm: ${t.primitives.borderRadius.sm},
|
|
683
|
+
md: ${t.primitives.borderRadius.md},
|
|
684
|
+
lg: ${t.primitives.borderRadius.lg},
|
|
685
|
+
full: ${t.primitives.borderRadius.full},
|
|
686
|
+
},
|
|
687
|
+
letterSpacing: {
|
|
688
|
+
tight: ${t.primitives.letterSpacing.tight},
|
|
689
|
+
normal: ${t.primitives.letterSpacing.normal},
|
|
690
|
+
wide: ${t.primitives.letterSpacing.wide},
|
|
691
|
+
wider: ${t.primitives.letterSpacing.wider},
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
// Semantic colors. Values must be valid for react-pdf.
|
|
696
|
+
colors: {
|
|
697
|
+
foreground: '${t.colors.foreground}',
|
|
698
|
+
background: '${t.colors.background}',
|
|
699
|
+
muted: '${t.colors.muted}',
|
|
700
|
+
mutedForeground: '${t.colors.mutedForeground}',
|
|
701
|
+
primary: '${t.colors.primary}',
|
|
702
|
+
primaryForeground: '${t.colors.primaryForeground}',
|
|
703
|
+
border: '${t.colors.border}',
|
|
704
|
+
accent: '${t.colors.accent}',
|
|
705
|
+
destructive: '${t.colors.destructive}',
|
|
706
|
+
success: '${t.colors.success}',
|
|
707
|
+
warning: '${t.colors.warning}',
|
|
708
|
+
info: '${t.colors.info}',
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
// Typography defaults.
|
|
712
|
+
typography: {
|
|
713
|
+
body: {
|
|
714
|
+
fontFamily: '${t.typography.body.fontFamily}',
|
|
715
|
+
fontSize: ${t.typography.body.fontSize},
|
|
716
|
+
lineHeight: ${t.typography.body.lineHeight},
|
|
717
|
+
},
|
|
718
|
+
heading: {
|
|
719
|
+
fontFamily: '${t.typography.heading.fontFamily}',
|
|
720
|
+
fontWeight: ${t.typography.heading.fontWeight},
|
|
721
|
+
lineHeight: ${t.typography.heading.lineHeight},
|
|
722
|
+
fontSize: {
|
|
723
|
+
h1: ${t.typography.heading.fontSize.h1},
|
|
724
|
+
h2: ${t.typography.heading.fontSize.h2},
|
|
725
|
+
h3: ${t.typography.heading.fontSize.h3},
|
|
726
|
+
h4: ${t.typography.heading.fontSize.h4},
|
|
727
|
+
h5: ${t.typography.heading.fontSize.h5},
|
|
728
|
+
h6: ${t.typography.heading.fontSize.h6},
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
// Layout spacing.
|
|
734
|
+
spacing: {
|
|
735
|
+
page: {
|
|
736
|
+
marginTop: ${t.spacing.page.marginTop},
|
|
737
|
+
marginRight: ${t.spacing.page.marginRight},
|
|
738
|
+
marginBottom: ${t.spacing.page.marginBottom},
|
|
739
|
+
marginLeft: ${t.spacing.page.marginLeft},
|
|
740
|
+
},
|
|
741
|
+
sectionGap: ${t.spacing.sectionGap},
|
|
742
|
+
paragraphGap: ${t.spacing.paragraphGap},
|
|
743
|
+
componentGap: ${t.spacing.componentGap},
|
|
744
|
+
},
|
|
745
|
+
|
|
746
|
+
// Page defaults.
|
|
747
|
+
page: {
|
|
748
|
+
size: '${t.page.size}',
|
|
749
|
+
orientation: '${t.page.orientation}',
|
|
750
|
+
},
|
|
751
|
+
}`;
|
|
752
|
+
}
|
|
753
|
+
function generateThemeContextFile() {
|
|
754
|
+
return `// Generated by pdfx init.
|
|
755
|
+
// See: https://pdfx.akashpise.dev/installation#theming
|
|
756
|
+
//
|
|
757
|
+
// Provides runtime theme overrides via React context.
|
|
758
|
+
// Wrap a subtree in <PdfxThemeProvider theme={myTheme}> to override defaults.
|
|
759
|
+
|
|
760
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
761
|
+
// Intentional: this module exports both a component and hooks/context.
|
|
762
|
+
// Keeping a single import surface preserves the generated public API.
|
|
763
|
+
|
|
764
|
+
import { type DependencyList, type ReactNode, createContext, useContext } from 'react';
|
|
765
|
+
import { theme as defaultTheme } from './pdfx-theme';
|
|
766
|
+
|
|
767
|
+
type PdfxTheme = typeof defaultTheme;
|
|
768
|
+
|
|
769
|
+
export const PdfxThemeContext = createContext<PdfxTheme>(defaultTheme);
|
|
770
|
+
|
|
771
|
+
export interface PdfxThemeProviderProps {
|
|
772
|
+
theme?: PdfxTheme;
|
|
773
|
+
children: ReactNode;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function PdfxThemeProvider({ theme, children }: PdfxThemeProviderProps) {
|
|
777
|
+
const resolvedTheme = theme ?? defaultTheme;
|
|
778
|
+
return <PdfxThemeContext.Provider value={resolvedTheme}>{children}</PdfxThemeContext.Provider>;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Returns the active theme from context.
|
|
783
|
+
*
|
|
784
|
+
* When called outside a React render tree (for example in plain-function tests),
|
|
785
|
+
* React may throw an invalid hook error. In that case we return defaultTheme.
|
|
786
|
+
* Unexpected errors are re-thrown.
|
|
787
|
+
*/
|
|
788
|
+
export function usePdfxTheme(): PdfxTheme {
|
|
789
|
+
try {
|
|
790
|
+
return useContext(PdfxThemeContext);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
// Suppress only known React dispatcher errors.
|
|
793
|
+
if (
|
|
794
|
+
error instanceof Error &&
|
|
795
|
+
/invalid hook call|useContext|cannot read properties of null/i.test(error.message)
|
|
796
|
+
) {
|
|
797
|
+
return defaultTheme;
|
|
798
|
+
}
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Calls factory() and returns the result.
|
|
805
|
+
* The deps parameter is accepted for API compatibility with existing callers.
|
|
806
|
+
*/
|
|
807
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
808
|
+
export function useSafeMemo<T>(factory: () => T, _deps: DependencyList): T {
|
|
809
|
+
return factory();
|
|
810
|
+
}
|
|
811
|
+
`;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/utils/package-manager.ts
|
|
815
|
+
import fs4 from "fs";
|
|
816
|
+
import path3 from "path";
|
|
817
|
+
var PACKAGE_MANAGERS = {
|
|
818
|
+
pnpm: {
|
|
819
|
+
name: "pnpm",
|
|
820
|
+
lockfile: "pnpm-lock.yaml",
|
|
821
|
+
installCommand: "pnpm add"
|
|
822
|
+
},
|
|
823
|
+
yarn: {
|
|
824
|
+
name: "yarn",
|
|
825
|
+
lockfile: "yarn.lock",
|
|
826
|
+
installCommand: "yarn add"
|
|
827
|
+
},
|
|
828
|
+
bun: {
|
|
829
|
+
name: "bun",
|
|
830
|
+
lockfile: "bun.lock",
|
|
831
|
+
installCommand: "bun add"
|
|
832
|
+
},
|
|
833
|
+
npm: {
|
|
834
|
+
name: "npm",
|
|
835
|
+
lockfile: "package-lock.json",
|
|
836
|
+
installCommand: "npm install"
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
function findPackageRoot(startDir) {
|
|
840
|
+
let dir = path3.resolve(startDir);
|
|
841
|
+
const fsRoot = path3.parse(dir).root;
|
|
842
|
+
while (dir !== fsRoot) {
|
|
843
|
+
const pkgPath = path3.join(dir, "package.json");
|
|
844
|
+
if (fs4.existsSync(pkgPath)) {
|
|
845
|
+
try {
|
|
846
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
|
|
847
|
+
const hasWorkspacesField = Array.isArray(pkg.workspaces) || typeof pkg.workspaces === "object" && pkg.workspaces !== null;
|
|
848
|
+
const hasPnpmWorkspaceFile = fs4.existsSync(path3.join(dir, "pnpm-workspace.yaml"));
|
|
849
|
+
if (!hasWorkspacesField && !hasPnpmWorkspaceFile) {
|
|
850
|
+
return dir;
|
|
851
|
+
}
|
|
852
|
+
} catch {
|
|
853
|
+
return dir;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
dir = path3.dirname(dir);
|
|
857
|
+
}
|
|
858
|
+
return startDir;
|
|
859
|
+
}
|
|
860
|
+
function detectPackageManager(startDir = process.cwd()) {
|
|
861
|
+
const managers = ["pnpm", "yarn", "bun", "npm"];
|
|
862
|
+
let dir = path3.resolve(startDir);
|
|
863
|
+
const fsRoot = path3.parse(dir).root;
|
|
864
|
+
while (dir !== fsRoot) {
|
|
865
|
+
for (const manager of managers) {
|
|
866
|
+
const info = PACKAGE_MANAGERS[manager];
|
|
867
|
+
if (fs4.existsSync(path3.join(dir, info.lockfile))) {
|
|
868
|
+
return info;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (fs4.existsSync(path3.join(dir, "bun.lockb"))) {
|
|
872
|
+
return PACKAGE_MANAGERS.bun;
|
|
873
|
+
}
|
|
874
|
+
dir = path3.dirname(dir);
|
|
875
|
+
}
|
|
876
|
+
return PACKAGE_MANAGERS.npm;
|
|
877
|
+
}
|
|
878
|
+
function getInstallCommand(packageManager, packages, devDependency = false) {
|
|
879
|
+
const pm = PACKAGE_MANAGERS[packageManager];
|
|
880
|
+
const devFlag = devDependency ? packageManager === "npm" ? "--save-dev" : "-D" : "";
|
|
881
|
+
return `${pm.installCommand} ${packages.join(" ")} ${devFlag}`.trim();
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/commands/add.ts
|
|
885
|
+
function readConfig(configPath) {
|
|
886
|
+
const raw = readJsonFile(configPath);
|
|
887
|
+
const result = configSchema.safeParse(raw);
|
|
888
|
+
if (!result.success) {
|
|
889
|
+
const issues = result.error.issues.map((i) => {
|
|
890
|
+
const fieldPath = i.path.length > 0 ? i.path.join(".") : "root";
|
|
891
|
+
return `"${fieldPath}": ${i.message}`;
|
|
892
|
+
}).join("; ");
|
|
893
|
+
throw new ConfigError(
|
|
894
|
+
`Invalid pdfx.json: ${issues}`,
|
|
895
|
+
`Fix the config or re-run ${chalk.cyan("npx pdfx-cli@latest init")}`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return result.data;
|
|
899
|
+
}
|
|
900
|
+
async function fetchComponent(name, registryUrl) {
|
|
901
|
+
const url = `${registryUrl}/${name}.json`;
|
|
902
|
+
let response;
|
|
903
|
+
try {
|
|
904
|
+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
905
|
+
} catch (err) {
|
|
906
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
907
|
+
throw new NetworkError(
|
|
908
|
+
isTimeout ? `Registry request timed out after 10 seconds.
|
|
909
|
+
${chalk.dim("Check your internet connection or try again later.")}` : `Could not reach registry at ${registryUrl}
|
|
910
|
+
${chalk.dim("Verify the URL is correct and you have internet access.")}`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
if (!response.ok) {
|
|
914
|
+
throw new RegistryError(
|
|
915
|
+
response.status === 404 ? `Component "${name}" not found in registry` : `Registry returned HTTP ${response.status}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
let data;
|
|
919
|
+
try {
|
|
920
|
+
data = await response.json();
|
|
921
|
+
} catch {
|
|
922
|
+
throw new RegistryError(`Invalid response for "${name}": not valid JSON`);
|
|
923
|
+
}
|
|
924
|
+
const result = registryItemSchema.safeParse(data);
|
|
925
|
+
if (!result.success) {
|
|
926
|
+
throw new RegistryError(
|
|
927
|
+
`Invalid registry entry for "${name}": ${result.error.issues[0]?.message}`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
return result.data;
|
|
931
|
+
}
|
|
932
|
+
function resolveThemeImport(componentDir, themePath, fileContent) {
|
|
933
|
+
const absComponentDir = path4.resolve(process.cwd(), componentDir);
|
|
934
|
+
const absThemePath = path4.resolve(process.cwd(), themePath);
|
|
935
|
+
const themeImportTarget = absThemePath.replace(/\.tsx?$/, "");
|
|
936
|
+
let relativePath = path4.relative(absComponentDir, themeImportTarget);
|
|
937
|
+
if (!relativePath.startsWith(".")) {
|
|
938
|
+
relativePath = `./${relativePath}`;
|
|
939
|
+
}
|
|
940
|
+
const absContextPath = path4.join(path4.dirname(absThemePath), "pdfx-theme-context");
|
|
941
|
+
let relativeContextPath = path4.relative(absComponentDir, absContextPath);
|
|
942
|
+
if (!relativeContextPath.startsWith(".")) {
|
|
943
|
+
relativeContextPath = `./${relativeContextPath}`;
|
|
944
|
+
}
|
|
945
|
+
let content = fileContent.replace(
|
|
946
|
+
/from\s+['"]\.\.\/lib\/pdfx-theme['"]/g,
|
|
947
|
+
`from '${relativePath}'`
|
|
948
|
+
);
|
|
949
|
+
content = content.replace(
|
|
950
|
+
/from\s+['"]\.\.\/lib\/pdfx-theme-context['"]/g,
|
|
951
|
+
`from '${relativeContextPath}'`
|
|
952
|
+
);
|
|
953
|
+
return content;
|
|
954
|
+
}
|
|
955
|
+
function collectComponentDependencies(items) {
|
|
956
|
+
const runtime = /* @__PURE__ */ new Set();
|
|
957
|
+
const dev = /* @__PURE__ */ new Set();
|
|
958
|
+
for (const item of items) {
|
|
959
|
+
for (const dep of item.dependencies ?? []) {
|
|
960
|
+
runtime.add(dep);
|
|
961
|
+
}
|
|
962
|
+
for (const dep of item.devDependencies ?? []) {
|
|
963
|
+
dev.add(dep);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
runtime: [...runtime],
|
|
968
|
+
dev: [...dev]
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function getDeclaredDependencies(pkg) {
|
|
972
|
+
const deps = pkg.dependencies ?? {};
|
|
973
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
974
|
+
return /* @__PURE__ */ new Set([...Object.keys(deps), ...Object.keys(devDeps)]);
|
|
975
|
+
}
|
|
976
|
+
function findMissingDependencies(requirements, pkg) {
|
|
977
|
+
const installed = getDeclaredDependencies(pkg);
|
|
978
|
+
return {
|
|
979
|
+
runtime: requirements.runtime.filter((dep) => !installed.has(dep)),
|
|
980
|
+
dev: requirements.dev.filter((dep) => !installed.has(dep))
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function resolveDependencyInstallMode(options) {
|
|
984
|
+
if (options.installDeps === true) return "always";
|
|
985
|
+
if (options.installDeps === false) return "never";
|
|
986
|
+
return "prompt";
|
|
987
|
+
}
|
|
988
|
+
async function installDependencySet(packageRoot, runtimeDeps, devDeps) {
|
|
989
|
+
const pm = detectPackageManager(packageRoot);
|
|
990
|
+
const installArgs = pm.installCommand.split(" ").slice(1);
|
|
991
|
+
if (runtimeDeps.length > 0) {
|
|
992
|
+
await execa(pm.name, [...installArgs, ...runtimeDeps], {
|
|
993
|
+
cwd: packageRoot,
|
|
994
|
+
stdio: "pipe"
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
if (devDeps.length > 0) {
|
|
998
|
+
const devFlag = pm.name === "npm" ? "--save-dev" : "-D";
|
|
999
|
+
await execa(pm.name, [...installArgs, ...devDeps, devFlag], {
|
|
1000
|
+
cwd: packageRoot,
|
|
1001
|
+
stdio: "pipe"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function formatDependencyInstallHint(packageRoot, runtimeDeps, devDeps) {
|
|
1006
|
+
const pm = detectPackageManager(packageRoot);
|
|
1007
|
+
const commands = [];
|
|
1008
|
+
if (runtimeDeps.length > 0) {
|
|
1009
|
+
commands.push(getInstallCommand(pm.name, runtimeDeps));
|
|
1010
|
+
}
|
|
1011
|
+
if (devDeps.length > 0) {
|
|
1012
|
+
commands.push(getInstallCommand(pm.name, devDeps, true));
|
|
1013
|
+
}
|
|
1014
|
+
return commands.map((cmd) => ` ${cmd}`).join("\n");
|
|
1015
|
+
}
|
|
1016
|
+
async function ensureComponentDependencies(requirements, options) {
|
|
1017
|
+
const packageRoot = findPackageRoot(process.cwd());
|
|
1018
|
+
const pkgPath = path4.join(packageRoot, "package.json");
|
|
1019
|
+
if (!fs5.existsSync(pkgPath)) {
|
|
1020
|
+
throw new ValidationError(
|
|
1021
|
+
`Missing package.json at ${packageRoot}`,
|
|
1022
|
+
"Run this command inside a Node.js project"
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const pkg = readJsonFile(pkgPath);
|
|
1026
|
+
const missing = findMissingDependencies(requirements, pkg);
|
|
1027
|
+
if (missing.runtime.length === 0 && missing.dev.length === 0) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const installMode = resolveDependencyInstallMode(options);
|
|
1031
|
+
const strictDeps = options.strictDeps ?? false;
|
|
1032
|
+
const installHint = formatDependencyInstallHint(packageRoot, missing.runtime, missing.dev);
|
|
1033
|
+
if (installMode === "never") {
|
|
1034
|
+
const hasBlocking = missing.runtime.length > 0 || strictDeps && missing.dev.length > 0;
|
|
1035
|
+
const missingMessage = [
|
|
1036
|
+
missing.runtime.length > 0 ? `runtime: ${missing.runtime.join(", ")}` : void 0,
|
|
1037
|
+
missing.dev.length > 0 ? `dev: ${missing.dev.join(", ")}` : void 0
|
|
1038
|
+
].filter(Boolean).join("; ");
|
|
1039
|
+
if (hasBlocking) {
|
|
1040
|
+
throw new ValidationError(
|
|
1041
|
+
`Missing component dependencies (${missingMessage})`,
|
|
1042
|
+
`Install manually:
|
|
1043
|
+
${installHint}`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
console.log(
|
|
1047
|
+
chalk.yellow(
|
|
1048
|
+
`
|
|
1049
|
+
\u26A0 Missing devDependencies (${missing.dev.join(", ")})
|
|
1050
|
+
${chalk.dim("\u2192")} Install manually:
|
|
1051
|
+
${chalk.cyan(installHint)}
|
|
1052
|
+
`
|
|
1053
|
+
)
|
|
1054
|
+
);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
let shouldInstall = installMode === "always";
|
|
1058
|
+
if (installMode === "prompt") {
|
|
1059
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1060
|
+
if (!interactive) {
|
|
1061
|
+
throw new ValidationError(
|
|
1062
|
+
"Missing component dependencies in non-interactive mode",
|
|
1063
|
+
`Use --install-deps or install manually:
|
|
1064
|
+
${installHint}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const depSummary = [
|
|
1068
|
+
missing.runtime.length > 0 ? `runtime: ${missing.runtime.join(", ")}` : void 0,
|
|
1069
|
+
missing.dev.length > 0 ? `dev: ${missing.dev.join(", ")}` : void 0
|
|
1070
|
+
].filter(Boolean).join("\n ");
|
|
1071
|
+
console.log(chalk.yellow("\n Missing component dependencies detected:\n"));
|
|
1072
|
+
console.log(chalk.dim(` ${depSummary}
|
|
1073
|
+
`));
|
|
1074
|
+
const response = await prompts({
|
|
1075
|
+
type: "confirm",
|
|
1076
|
+
name: "install",
|
|
1077
|
+
message: "Install missing dependencies now?",
|
|
1078
|
+
initial: true
|
|
1079
|
+
});
|
|
1080
|
+
shouldInstall = response.install === true;
|
|
1081
|
+
}
|
|
1082
|
+
if (!shouldInstall) {
|
|
1083
|
+
throw new ValidationError(
|
|
1084
|
+
"Dependency installation cancelled",
|
|
1085
|
+
`Install manually:
|
|
1086
|
+
${installHint}`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
const spinner = ora("Installing missing component dependencies...").start();
|
|
1090
|
+
try {
|
|
1091
|
+
await installDependencySet(packageRoot, missing.runtime, missing.dev);
|
|
1092
|
+
spinner.succeed("Installed component dependencies");
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
spinner.fail("Failed to install component dependencies");
|
|
1095
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1096
|
+
if (strictDeps || missing.runtime.length > 0) {
|
|
1097
|
+
throw new ValidationError(
|
|
1098
|
+
`Dependency installation failed: ${message}`,
|
|
1099
|
+
`Install manually:
|
|
1100
|
+
${installHint}`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
console.log(
|
|
1104
|
+
chalk.yellow(
|
|
1105
|
+
`
|
|
1106
|
+
\u26A0 Could not install devDependencies automatically
|
|
1107
|
+
${chalk.dim("\u2192")} Install manually:
|
|
1108
|
+
${chalk.cyan(installHint)}
|
|
1109
|
+
`
|
|
1110
|
+
)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async function installComponent(component, config, force) {
|
|
1115
|
+
const targetDir = path4.resolve(process.cwd(), config.componentDir);
|
|
1116
|
+
const componentDir = path4.join(targetDir, component.name);
|
|
1117
|
+
ensureDir(componentDir);
|
|
1118
|
+
const componentRelDir = path4.join(config.componentDir, component.name);
|
|
1119
|
+
const filesToWrite = [];
|
|
1120
|
+
for (const file of component.files) {
|
|
1121
|
+
const fileName = path4.basename(file.path);
|
|
1122
|
+
const filePath = safePath(componentDir, fileName);
|
|
1123
|
+
let content = file.content;
|
|
1124
|
+
if (config.theme && (content.includes("pdfx-theme") || content.includes("pdfx-theme-context"))) {
|
|
1125
|
+
content = resolveThemeImport(componentRelDir, config.theme, content);
|
|
1126
|
+
}
|
|
1127
|
+
filesToWrite.push({ filePath, content });
|
|
1128
|
+
}
|
|
1129
|
+
if (!force) {
|
|
1130
|
+
const existing = filesToWrite.filter((f) => checkFileExists(f.filePath));
|
|
1131
|
+
if (existing.length > 0) {
|
|
1132
|
+
const fileNames = existing.map((f) => path4.basename(f.filePath)).join(", ");
|
|
1133
|
+
throw new ValidationError(
|
|
1134
|
+
`${component.name}: already exists (${fileNames}), skipped install (use --force to overwrite)`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
for (const file of filesToWrite) {
|
|
1139
|
+
writeFile(file.filePath, file.content);
|
|
1140
|
+
}
|
|
1141
|
+
if (config.theme) {
|
|
1142
|
+
const absThemePath = path4.resolve(process.cwd(), config.theme);
|
|
1143
|
+
const contextPath = path4.join(path4.dirname(absThemePath), "pdfx-theme-context.tsx");
|
|
1144
|
+
if (!checkFileExists(contextPath)) {
|
|
1145
|
+
ensureDir(path4.dirname(contextPath));
|
|
1146
|
+
writeFile(contextPath, generateThemeContextFile());
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
async function add(components, options = {}) {
|
|
1151
|
+
if (!components || components.length === 0) {
|
|
1152
|
+
console.error(chalk.red("Error: Component name required"));
|
|
1153
|
+
console.log(chalk.dim("Usage: npx pdfx-cli@latest add <component...>"));
|
|
1154
|
+
console.log(chalk.dim("Example: npx pdfx-cli@latest add heading text table\n"));
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
const reactPdfCheck = validateReactPdfRenderer();
|
|
1158
|
+
if (!reactPdfCheck.installed) {
|
|
1159
|
+
console.error(chalk.red("\nError: @react-pdf/renderer is not installed\n"));
|
|
1160
|
+
console.log(chalk.yellow(" PDFx components require @react-pdf/renderer to work.\n"));
|
|
1161
|
+
console.log(chalk.cyan(" Run: npx pdfx-cli@latest init"));
|
|
1162
|
+
console.log(chalk.dim(" or install manually: npm install @react-pdf/renderer\n"));
|
|
1163
|
+
process.exit(1);
|
|
1164
|
+
}
|
|
1165
|
+
if (!reactPdfCheck.valid) {
|
|
1166
|
+
console.log(
|
|
1167
|
+
chalk.yellow(
|
|
1168
|
+
`
|
|
1169
|
+
\u26A0 Warning: ${reactPdfCheck.message}
|
|
1170
|
+
${chalk.dim("\u2192")} You may encounter compatibility issues
|
|
1171
|
+
`
|
|
1172
|
+
)
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
const configPath = path4.join(process.cwd(), "pdfx.json");
|
|
1176
|
+
if (!checkFileExists(configPath)) {
|
|
1177
|
+
console.error(chalk.red("Error: pdfx.json not found"));
|
|
1178
|
+
console.log(chalk.yellow("Run: npx pdfx-cli@latest init"));
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
let config;
|
|
1182
|
+
try {
|
|
1183
|
+
config = readConfig(configPath);
|
|
1184
|
+
if (options.registry) {
|
|
1185
|
+
config = { ...config, registry: options.registry };
|
|
1186
|
+
}
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
if (error instanceof ConfigError) {
|
|
1189
|
+
console.error(chalk.red(error.message));
|
|
1190
|
+
if (error.suggestion) console.log(chalk.yellow(` Hint: ${error.suggestion}`));
|
|
1191
|
+
} else {
|
|
1192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1193
|
+
console.error(chalk.red(message));
|
|
1194
|
+
}
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
const force = options.force ?? false;
|
|
1198
|
+
const failed = [];
|
|
1199
|
+
let installedCount = 0;
|
|
1200
|
+
const resolvedComponents = [];
|
|
1201
|
+
const validNames = [];
|
|
1202
|
+
for (const componentName of components) {
|
|
1203
|
+
const nameResult = componentNameSchema.safeParse(componentName);
|
|
1204
|
+
if (!nameResult.success) {
|
|
1205
|
+
console.error(chalk.red(`Invalid component name: "${componentName}"`));
|
|
1206
|
+
console.log(
|
|
1207
|
+
chalk.dim(' Names must be lowercase alphanumeric with hyphens (e.g., "data-table")')
|
|
1208
|
+
);
|
|
1209
|
+
failed.push(componentName);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
validNames.push(componentName);
|
|
1213
|
+
}
|
|
1214
|
+
for (const componentName of validNames) {
|
|
1215
|
+
const spinner = ora(`Resolving ${componentName}...`).start();
|
|
1216
|
+
try {
|
|
1217
|
+
const component = await fetchComponent(componentName, config.registry);
|
|
1218
|
+
resolvedComponents.push(component);
|
|
1219
|
+
spinner.succeed(`Resolved ${componentName}`);
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
spinner.fail(`Failed to resolve ${componentName}`);
|
|
1222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1223
|
+
console.error(chalk.dim(` ${message}`));
|
|
1224
|
+
failed.push(componentName);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (resolvedComponents.length > 0) {
|
|
1228
|
+
try {
|
|
1229
|
+
const requirements = collectComponentDependencies(resolvedComponents);
|
|
1230
|
+
await ensureComponentDependencies(requirements, options);
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1233
|
+
console.error(chalk.red(message));
|
|
1234
|
+
if (error instanceof ValidationError && error.suggestion) {
|
|
1235
|
+
console.log(chalk.yellow(` Hint: ${error.suggestion}`));
|
|
1236
|
+
}
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
for (const component of resolvedComponents) {
|
|
1241
|
+
const spinner = ora(`Adding ${component.name}...`).start();
|
|
1242
|
+
try {
|
|
1243
|
+
await installComponent(component, config, force);
|
|
1244
|
+
installedCount++;
|
|
1245
|
+
spinner.succeed(`Added ${component.name}`);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
let shouldMarkAsFailed = true;
|
|
1248
|
+
if (error instanceof ValidationError && error.message.includes("already exists")) {
|
|
1249
|
+
spinner.info(error.message);
|
|
1250
|
+
shouldMarkAsFailed = false;
|
|
1251
|
+
} else if (error instanceof NetworkError || error instanceof RegistryError || error instanceof ValidationError) {
|
|
1252
|
+
spinner.fail(error.message);
|
|
1253
|
+
if (error.suggestion) {
|
|
1254
|
+
console.log(chalk.dim(` Hint: ${error.suggestion}`));
|
|
1255
|
+
}
|
|
1256
|
+
} else {
|
|
1257
|
+
spinner.fail(`Failed to add ${component.name}`);
|
|
1258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1259
|
+
console.error(chalk.dim(` ${message}`));
|
|
1260
|
+
}
|
|
1261
|
+
if (shouldMarkAsFailed) {
|
|
1262
|
+
failed.push(component.name);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
console.log();
|
|
1267
|
+
if (failed.length > 0) {
|
|
1268
|
+
console.log(chalk.yellow(`Failed to add: ${failed.join(", ")}`));
|
|
1269
|
+
}
|
|
1270
|
+
if (installedCount > 0) {
|
|
1271
|
+
const resolvedDir = path4.resolve(process.cwd(), config.componentDir);
|
|
1272
|
+
console.log(chalk.green("Done!"));
|
|
1273
|
+
console.log(chalk.dim(`Components installed to: ${resolvedDir}
|
|
1274
|
+
`));
|
|
1275
|
+
}
|
|
1276
|
+
if (failed.length > 0) {
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/commands/block.ts
|
|
1282
|
+
import path5 from "path";
|
|
1283
|
+
import chalk2 from "chalk";
|
|
1284
|
+
import ora2 from "ora";
|
|
1285
|
+
import prompts2 from "prompts";
|
|
1286
|
+
async function fetchBlock(name, registryUrl) {
|
|
1287
|
+
const url = `${registryUrl}/${REGISTRY_SUBPATHS.BLOCKS}/${name}.json`;
|
|
1288
|
+
let response;
|
|
1289
|
+
try {
|
|
1290
|
+
response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
1291
|
+
} catch (err) {
|
|
1292
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
1293
|
+
throw new NetworkError(
|
|
1294
|
+
isTimeout ? "Registry request timed out" : `Could not reach ${registryUrl}`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (!response.ok) {
|
|
1298
|
+
throw new RegistryError(
|
|
1299
|
+
response.status === 404 ? `Block "${name}" not found in registry` : `Registry returned HTTP ${response.status}`,
|
|
1300
|
+
response.status === 404 ? 'Run "npx pdfx-cli@latest block list" to see all available blocks' : void 0
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
let data;
|
|
1304
|
+
try {
|
|
1305
|
+
data = await response.json();
|
|
1306
|
+
} catch {
|
|
1307
|
+
throw new RegistryError(`Invalid response for "${name}": not valid JSON`);
|
|
1308
|
+
}
|
|
1309
|
+
const result = registryItemSchema.safeParse(data);
|
|
1310
|
+
if (!result.success) {
|
|
1311
|
+
throw new RegistryError(
|
|
1312
|
+
`Invalid registry entry for "${name}": ${result.error.issues[0]?.message}`
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return result.data;
|
|
1316
|
+
}
|
|
1317
|
+
function resolveBlockImports(content, blockName, config) {
|
|
1318
|
+
const cwd = process.cwd();
|
|
1319
|
+
const blockSubdir = path5.resolve(cwd, config.blockDir ?? DEFAULTS.BLOCK_DIR, blockName);
|
|
1320
|
+
let result = content.replace(
|
|
1321
|
+
/from '\.\.\/\.\.\/components\/pdfx\/([a-z][a-z0-9-]*)\/pdfx-([a-z][a-z0-9-]*)'/g,
|
|
1322
|
+
(_match, componentName) => {
|
|
1323
|
+
const absCompFile = path5.resolve(
|
|
1324
|
+
cwd,
|
|
1325
|
+
config.componentDir,
|
|
1326
|
+
componentName,
|
|
1327
|
+
`pdfx-${componentName}`
|
|
1328
|
+
);
|
|
1329
|
+
let rel = path5.relative(blockSubdir, absCompFile);
|
|
1330
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
1331
|
+
return `from '${rel}'`;
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
if (config.theme) {
|
|
1335
|
+
const absThemePath = path5.resolve(cwd, config.theme).replace(/\.tsx?$/, "");
|
|
1336
|
+
let relTheme = path5.relative(blockSubdir, absThemePath);
|
|
1337
|
+
if (!relTheme.startsWith(".")) relTheme = `./${relTheme}`;
|
|
1338
|
+
const absContextPath = path5.join(
|
|
1339
|
+
path5.dirname(path5.resolve(cwd, config.theme)),
|
|
1340
|
+
"pdfx-theme-context"
|
|
1341
|
+
);
|
|
1342
|
+
let relContext = path5.relative(blockSubdir, absContextPath);
|
|
1343
|
+
if (!relContext.startsWith(".")) relContext = `./${relContext}`;
|
|
1344
|
+
result = result.replace(/from '\.\.\/\.\.\/lib\/pdfx-theme'/g, `from '${relTheme}'`);
|
|
1345
|
+
result = result.replace(/from '\.\.\/\.\.\/lib\/pdfx-theme-context'/g, `from '${relContext}'`);
|
|
1346
|
+
}
|
|
1347
|
+
return result;
|
|
1348
|
+
}
|
|
1349
|
+
async function resolveConflict(fileName, currentDecision) {
|
|
1350
|
+
if (currentDecision === "overwrite-all") return "overwrite-all";
|
|
1351
|
+
const { action } = await prompts2({
|
|
1352
|
+
type: "select",
|
|
1353
|
+
name: "action",
|
|
1354
|
+
message: `${chalk2.yellow(fileName)} already exists. What would you like to do?`,
|
|
1355
|
+
choices: [
|
|
1356
|
+
{ title: "Skip", value: "skip", description: "Keep the existing file unchanged" },
|
|
1357
|
+
{ title: "Overwrite", value: "overwrite", description: "Replace this file only" },
|
|
1358
|
+
{
|
|
1359
|
+
title: "Overwrite all",
|
|
1360
|
+
value: "overwrite-all",
|
|
1361
|
+
description: "Replace all conflicting files"
|
|
1362
|
+
}
|
|
1363
|
+
],
|
|
1364
|
+
initial: 0
|
|
1365
|
+
});
|
|
1366
|
+
if (!action) throw new ValidationError("Cancelled by user");
|
|
1367
|
+
return action;
|
|
1368
|
+
}
|
|
1369
|
+
async function ensurePeerComponents(block, config, force) {
|
|
1370
|
+
const installedPeers = [];
|
|
1371
|
+
const peerWarnings = [];
|
|
1372
|
+
if (!block.peerComponents || block.peerComponents.length === 0) {
|
|
1373
|
+
return { installedPeers, peerWarnings, allSkipped: false };
|
|
1374
|
+
}
|
|
1375
|
+
const componentBaseDir = path5.resolve(process.cwd(), config.componentDir);
|
|
1376
|
+
for (const componentName of block.peerComponents) {
|
|
1377
|
+
const componentDir = path5.join(componentBaseDir, componentName);
|
|
1378
|
+
const expectedMain = path5.join(componentDir, `pdfx-${componentName}.tsx`);
|
|
1379
|
+
if (checkFileExists(componentDir)) {
|
|
1380
|
+
if (!checkFileExists(expectedMain)) {
|
|
1381
|
+
peerWarnings.push(
|
|
1382
|
+
`${componentName}: directory exists but expected file missing (${path5.basename(expectedMain)})`
|
|
1383
|
+
);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
if (!force) {
|
|
1387
|
+
peerWarnings.push(
|
|
1388
|
+
`${componentName}: already exists, skipped install (use "npx pdfx-cli@latest diff ${componentName}" to verify freshness)`
|
|
1389
|
+
);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const component = await fetchComponent(componentName, config.registry);
|
|
1394
|
+
ensureDir(componentDir);
|
|
1395
|
+
const componentRelDir = path5.join(config.componentDir, component.name);
|
|
1396
|
+
for (const file of component.files) {
|
|
1397
|
+
const fileName = path5.basename(file.path);
|
|
1398
|
+
const filePath = safePath(componentDir, fileName);
|
|
1399
|
+
let content = file.content;
|
|
1400
|
+
if (config.theme && (content.includes("pdfx-theme") || content.includes("pdfx-theme-context"))) {
|
|
1401
|
+
content = resolveThemeImport(componentRelDir, config.theme, content);
|
|
1402
|
+
}
|
|
1403
|
+
writeFile(filePath, content);
|
|
1404
|
+
}
|
|
1405
|
+
installedPeers.push(componentName);
|
|
1406
|
+
}
|
|
1407
|
+
return { installedPeers, peerWarnings, allSkipped: false };
|
|
1408
|
+
}
|
|
1409
|
+
async function installBlock(name, config, force, spinner) {
|
|
1410
|
+
const block = await fetchBlock(name, config.registry);
|
|
1411
|
+
const peerResult = await ensurePeerComponents(block, config, force);
|
|
1412
|
+
const blockBaseDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
|
|
1413
|
+
const blockDir = path5.join(blockBaseDir, block.name);
|
|
1414
|
+
ensureDir(blockDir);
|
|
1415
|
+
const filesToWrite = [];
|
|
1416
|
+
let allSkipped = false;
|
|
1417
|
+
let globalDecision = null;
|
|
1418
|
+
const resolved = [];
|
|
1419
|
+
for (const file of block.files) {
|
|
1420
|
+
const fileName = path5.basename(file.path);
|
|
1421
|
+
const filePath = safePath(blockDir, fileName);
|
|
1422
|
+
let content = file.content;
|
|
1423
|
+
if (/\.(tsx?|jsx?)$/.test(fileName) && content.includes("../../")) {
|
|
1424
|
+
content = resolveBlockImports(content, block.name, config);
|
|
1425
|
+
}
|
|
1426
|
+
filesToWrite.push({ filePath, content });
|
|
1427
|
+
}
|
|
1428
|
+
if (!force) {
|
|
1429
|
+
const hasConflicts = filesToWrite.some((f) => checkFileExists(f.filePath));
|
|
1430
|
+
if (hasConflicts) {
|
|
1431
|
+
spinner.stop();
|
|
1432
|
+
}
|
|
1433
|
+
for (const file of filesToWrite) {
|
|
1434
|
+
if (checkFileExists(file.filePath)) {
|
|
1435
|
+
const decision = await resolveConflict(path5.basename(file.filePath), globalDecision);
|
|
1436
|
+
if (decision === "overwrite-all") {
|
|
1437
|
+
globalDecision = "overwrite-all";
|
|
1438
|
+
}
|
|
1439
|
+
resolved.push({ ...file, skip: decision === "skip" });
|
|
1440
|
+
} else {
|
|
1441
|
+
resolved.push({ ...file, skip: false });
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
allSkipped = resolved.every((f) => f.skip);
|
|
1445
|
+
for (const file of resolved) {
|
|
1446
|
+
if (!file.skip) {
|
|
1447
|
+
writeFile(file.filePath, file.content);
|
|
1448
|
+
} else {
|
|
1449
|
+
console.log(chalk2.dim(` skipped ${path5.basename(file.filePath)}`));
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
} else {
|
|
1453
|
+
for (const file of filesToWrite) {
|
|
1454
|
+
writeFile(file.filePath, file.content);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (config.theme) {
|
|
1458
|
+
const absThemePath = path5.resolve(process.cwd(), config.theme);
|
|
1459
|
+
const contextPath = path5.join(path5.dirname(absThemePath), "pdfx-theme-context.tsx");
|
|
1460
|
+
if (!checkFileExists(contextPath)) {
|
|
1461
|
+
ensureDir(path5.dirname(contextPath));
|
|
1462
|
+
writeFile(contextPath, generateThemeContextFile());
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
return { ...peerResult, allSkipped };
|
|
1466
|
+
}
|
|
1467
|
+
async function blockAdd(names, options = {}) {
|
|
1468
|
+
const configPath = path5.join(process.cwd(), "pdfx.json");
|
|
1469
|
+
let config;
|
|
1470
|
+
const force = options.force ?? false;
|
|
1471
|
+
const failed = [];
|
|
1472
|
+
let installedCount = 0;
|
|
1473
|
+
if (!checkFileExists(configPath)) {
|
|
1474
|
+
console.error(chalk2.red("Error: pdfx.json not found"));
|
|
1475
|
+
console.log(chalk2.yellow("Run: npx pdfx-cli@latest init"));
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
config = readConfig(configPath);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
if (error instanceof ConfigError) {
|
|
1482
|
+
console.error(chalk2.red(error.message));
|
|
1483
|
+
if (error.suggestion) console.log(chalk2.yellow(` Hint: ${error.suggestion}`));
|
|
1484
|
+
} else {
|
|
1485
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1486
|
+
console.error(chalk2.red(message));
|
|
1487
|
+
}
|
|
1488
|
+
process.exit(1);
|
|
1489
|
+
}
|
|
1490
|
+
for (const blockName of names) {
|
|
1491
|
+
const nameResult = componentNameSchema.safeParse(blockName);
|
|
1492
|
+
if (!nameResult.success) {
|
|
1493
|
+
console.error(chalk2.red(`Invalid block name: "${blockName}"`));
|
|
1494
|
+
console.log(
|
|
1495
|
+
chalk2.dim(' Names must be lowercase alphanumeric with hyphens (e.g., "invoice-classic")')
|
|
1496
|
+
);
|
|
1497
|
+
failed.push(blockName);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const spinner = ora2(`Adding block ${blockName}...`).start();
|
|
1501
|
+
try {
|
|
1502
|
+
const result = await installBlock(blockName, config, force, spinner);
|
|
1503
|
+
if (result.allSkipped) {
|
|
1504
|
+
spinner.info(
|
|
1505
|
+
`Skipped ${chalk2.cyan(blockName)} \u2014 all files already exist (use ${chalk2.cyan("--force")} to overwrite)`
|
|
1506
|
+
);
|
|
1507
|
+
} else {
|
|
1508
|
+
installedCount++;
|
|
1509
|
+
spinner.succeed(`Added block ${chalk2.cyan(blockName)}`);
|
|
1510
|
+
if (result.installedPeers.length > 0) {
|
|
1511
|
+
console.log(
|
|
1512
|
+
chalk2.green(` Installed required components: ${result.installedPeers.join(", ")}`)
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
for (const warning of result.peerWarnings) {
|
|
1516
|
+
console.log(chalk2.yellow(` Warning: ${warning}`));
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (error instanceof ValidationError && error.message.includes("Cancelled")) {
|
|
1521
|
+
spinner.info("Cancelled");
|
|
1522
|
+
process.exit(0);
|
|
1523
|
+
} else if (error instanceof NetworkError || error instanceof RegistryError || error instanceof ValidationError) {
|
|
1524
|
+
spinner.fail(error.message);
|
|
1525
|
+
if (error.suggestion) {
|
|
1526
|
+
console.log(chalk2.dim(` Hint: ${error.suggestion}`));
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
spinner.fail(`Failed to add block ${blockName}`);
|
|
1530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1531
|
+
console.error(chalk2.dim(` ${message}`));
|
|
1532
|
+
}
|
|
1533
|
+
failed.push(blockName);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
console.log();
|
|
1537
|
+
if (failed.length > 0) {
|
|
1538
|
+
console.log(chalk2.yellow(`Failed: ${failed.join(", ")}`));
|
|
1539
|
+
}
|
|
1540
|
+
if (installedCount > 0) {
|
|
1541
|
+
const resolvedDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
|
|
1542
|
+
console.log(chalk2.green("Done!"));
|
|
1543
|
+
console.log(chalk2.dim(`Blocks installed to: ${resolvedDir}
|
|
1544
|
+
`));
|
|
1545
|
+
}
|
|
1546
|
+
if (failed.length > 0) {
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
async function blockList() {
|
|
1551
|
+
const configPath = path5.join(process.cwd(), "pdfx.json");
|
|
1552
|
+
if (!checkFileExists(configPath)) {
|
|
1553
|
+
console.error(chalk2.red("Error: pdfx.json not found"));
|
|
1554
|
+
console.log(chalk2.yellow("Run: npx pdfx-cli@latest init"));
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
let config;
|
|
1558
|
+
try {
|
|
1559
|
+
config = readConfig(configPath);
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
if (error instanceof ConfigError) {
|
|
1562
|
+
console.error(chalk2.red(error.message));
|
|
1563
|
+
if (error.suggestion) console.log(chalk2.yellow(` Hint: ${error.suggestion}`));
|
|
1564
|
+
} else {
|
|
1565
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1566
|
+
console.error(chalk2.red(message));
|
|
1567
|
+
}
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
const spinner = ora2("Fetching block list...").start();
|
|
1571
|
+
try {
|
|
1572
|
+
let response;
|
|
1573
|
+
try {
|
|
1574
|
+
response = await fetch(`${config.registry}/index.json`, {
|
|
1575
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1576
|
+
});
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
1579
|
+
throw new NetworkError(
|
|
1580
|
+
isTimeout ? "Registry request timed out" : `Could not reach ${config.registry}`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
if (!response.ok) {
|
|
1584
|
+
throw new RegistryError(`Registry returned HTTP ${response.status}`);
|
|
1585
|
+
}
|
|
1586
|
+
let data;
|
|
1587
|
+
try {
|
|
1588
|
+
data = await response.json();
|
|
1589
|
+
} catch {
|
|
1590
|
+
throw new RegistryError("Invalid response from registry: not valid JSON");
|
|
1591
|
+
}
|
|
1592
|
+
const result = registrySchema.safeParse(data);
|
|
1593
|
+
if (!result.success) {
|
|
1594
|
+
throw new RegistryError("Invalid registry format");
|
|
1595
|
+
}
|
|
1596
|
+
spinner.stop();
|
|
1597
|
+
const blocks = result.data.items.filter((item) => item.type === "registry:block");
|
|
1598
|
+
if (blocks.length === 0) {
|
|
1599
|
+
console.log(chalk2.dim("\n No blocks available in the registry.\n"));
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const blockBaseDir = path5.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
|
|
1603
|
+
console.log(chalk2.bold(`
|
|
1604
|
+
Available Blocks (${blocks.length})
|
|
1605
|
+
`));
|
|
1606
|
+
for (const item of blocks) {
|
|
1607
|
+
const blockDir = path5.join(blockBaseDir, item.name);
|
|
1608
|
+
const installed = checkFileExists(blockDir);
|
|
1609
|
+
const status = installed ? chalk2.green("[installed]") : chalk2.dim("[not installed]");
|
|
1610
|
+
console.log(` ${chalk2.cyan(item.name.padEnd(22))} ${item.description ?? ""}`);
|
|
1611
|
+
console.log(` ${"".padEnd(22)} ${status}`);
|
|
1612
|
+
if (item.peerComponents && item.peerComponents.length > 0) {
|
|
1613
|
+
console.log(chalk2.dim(` ${"".padEnd(22)} requires: ${item.peerComponents.join(", ")}`));
|
|
1614
|
+
}
|
|
1615
|
+
console.log();
|
|
1616
|
+
}
|
|
1617
|
+
console.log(
|
|
1618
|
+
chalk2.dim(` Install with: ${chalk2.cyan("npx pdfx-cli@latest block add <block-name>")}
|
|
1619
|
+
`)
|
|
1620
|
+
);
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1623
|
+
spinner.fail(message);
|
|
1624
|
+
process.exit(1);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/commands/diff.ts
|
|
1629
|
+
import fs6 from "fs";
|
|
1630
|
+
import path6 from "path";
|
|
1631
|
+
import chalk3 from "chalk";
|
|
1632
|
+
import ora3 from "ora";
|
|
1633
|
+
async function diff(components) {
|
|
1634
|
+
const configPath = path6.join(process.cwd(), "pdfx.json");
|
|
1635
|
+
let hasFailures = false;
|
|
1636
|
+
if (!checkFileExists(configPath)) {
|
|
1637
|
+
console.error(chalk3.red("Error: pdfx.json not found"));
|
|
1638
|
+
console.log(chalk3.yellow("Run: npx pdfx-cli@latest init"));
|
|
1639
|
+
process.exit(1);
|
|
1640
|
+
}
|
|
1641
|
+
let config;
|
|
1642
|
+
try {
|
|
1643
|
+
config = readConfig(configPath);
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1646
|
+
console.error(chalk3.red(message));
|
|
1647
|
+
process.exit(1);
|
|
1648
|
+
}
|
|
1649
|
+
const targetDir = path6.resolve(process.cwd(), config.componentDir);
|
|
1650
|
+
for (const componentName of components) {
|
|
1651
|
+
const nameResult = componentNameSchema.safeParse(componentName);
|
|
1652
|
+
if (!nameResult.success) {
|
|
1653
|
+
console.error(chalk3.red(`Invalid component name: "${componentName}"`));
|
|
1654
|
+
hasFailures = true;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
const spinner = ora3(`Comparing ${componentName}...`).start();
|
|
1658
|
+
try {
|
|
1659
|
+
const component = await fetchComponent(componentName, config.registry);
|
|
1660
|
+
spinner.stop();
|
|
1661
|
+
const componentSubDir = path6.join(targetDir, component.name);
|
|
1662
|
+
for (const file of component.files) {
|
|
1663
|
+
const fileName = path6.basename(file.path);
|
|
1664
|
+
const localPath = safePath(componentSubDir, fileName);
|
|
1665
|
+
if (!checkFileExists(localPath)) {
|
|
1666
|
+
console.log(chalk3.yellow(` ${fileName}: not installed locally`));
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
const localContent = fs6.readFileSync(localPath, "utf-8");
|
|
1670
|
+
const registryContent = config.theme && (file.content.includes("pdfx-theme") || file.content.includes("pdfx-theme-context")) ? resolveThemeImport(
|
|
1671
|
+
path6.join(config.componentDir, component.name),
|
|
1672
|
+
config.theme,
|
|
1673
|
+
file.content
|
|
1674
|
+
) : file.content;
|
|
1675
|
+
if (localContent === registryContent) {
|
|
1676
|
+
console.log(chalk3.green(` ${fileName}: up to date`));
|
|
1677
|
+
} else {
|
|
1678
|
+
console.log(chalk3.yellow(` ${fileName}: differs from registry`));
|
|
1679
|
+
const localLines = localContent.split("\n");
|
|
1680
|
+
const registryLines = registryContent.split("\n");
|
|
1681
|
+
const lineDiff = localLines.length - registryLines.length;
|
|
1682
|
+
console.log(chalk3.dim(` Local: ${localLines.length} lines`));
|
|
1683
|
+
console.log(chalk3.dim(` Registry: ${registryLines.length} lines`));
|
|
1684
|
+
if (lineDiff !== 0) {
|
|
1685
|
+
const diffText = lineDiff > 0 ? `${Math.abs(lineDiff)} line${Math.abs(lineDiff) > 1 ? "s" : ""} added locally` : `${Math.abs(lineDiff)} line${Math.abs(lineDiff) > 1 ? "s" : ""} removed locally`;
|
|
1686
|
+
console.log(chalk3.dim(` \u2192 ${diffText}`));
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
console.log();
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1693
|
+
spinner.fail(message);
|
|
1694
|
+
hasFailures = true;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (hasFailures) {
|
|
1698
|
+
process.exit(1);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/commands/init.ts
|
|
1703
|
+
import fs7 from "fs";
|
|
1704
|
+
import path9 from "path";
|
|
1705
|
+
import chalk6 from "chalk";
|
|
1706
|
+
import ora5 from "ora";
|
|
1707
|
+
import prompts4 from "prompts";
|
|
1708
|
+
|
|
1709
|
+
// src/utils/install-dependencies.ts
|
|
1710
|
+
import chalk4 from "chalk";
|
|
1711
|
+
import { execa as execa2 } from "execa";
|
|
1712
|
+
import ora4 from "ora";
|
|
1713
|
+
import prompts3 from "prompts";
|
|
1714
|
+
async function promptAndInstallReactPdf(validation, cwd = process.cwd()) {
|
|
1715
|
+
if (validation.installed && validation.valid) {
|
|
1716
|
+
return {
|
|
1717
|
+
success: true,
|
|
1718
|
+
message: "@react-pdf/renderer is already installed and compatible"
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
const packageRoot = findPackageRoot(cwd);
|
|
1722
|
+
const pm = detectPackageManager(cwd);
|
|
1723
|
+
const packageName = "@react-pdf/renderer";
|
|
1724
|
+
const installCmd2 = getInstallCommand(pm.name, [packageName]);
|
|
1725
|
+
console.log(chalk4.yellow("\n \u26A0 @react-pdf/renderer is required but not installed\n"));
|
|
1726
|
+
console.log(chalk4.dim(` Package root: ${packageRoot}`));
|
|
1727
|
+
console.log(chalk4.dim(` This command will run: ${installCmd2}
|
|
1728
|
+
`));
|
|
1729
|
+
const { shouldInstall } = await prompts3({
|
|
1730
|
+
type: "confirm",
|
|
1731
|
+
name: "shouldInstall",
|
|
1732
|
+
message: "Install @react-pdf/renderer now?",
|
|
1733
|
+
initial: true
|
|
1734
|
+
});
|
|
1735
|
+
if (!shouldInstall) {
|
|
1736
|
+
return {
|
|
1737
|
+
success: false,
|
|
1738
|
+
message: `Installation cancelled. Please install manually:
|
|
1739
|
+
${chalk4.cyan(installCmd2)}`
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
const spinner = ora4("Installing @react-pdf/renderer...").start();
|
|
1743
|
+
try {
|
|
1744
|
+
const installArgs = pm.installCommand.split(" ").slice(1);
|
|
1745
|
+
await execa2(pm.name, [...installArgs, packageName], {
|
|
1746
|
+
cwd: packageRoot,
|
|
1747
|
+
stdio: "pipe"
|
|
1748
|
+
});
|
|
1749
|
+
spinner.succeed("Installed @react-pdf/renderer");
|
|
1750
|
+
return {
|
|
1751
|
+
success: true,
|
|
1752
|
+
message: "@react-pdf/renderer installed successfully"
|
|
1753
|
+
};
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
spinner.fail("Failed to install @react-pdf/renderer");
|
|
1756
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1757
|
+
return {
|
|
1758
|
+
success: false,
|
|
1759
|
+
message: `Installation failed: ${message}
|
|
1760
|
+
Try manually: ${chalk4.cyan(installCmd2)}`
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async function ensureReactPdfRenderer(validation, cwd = process.cwd()) {
|
|
1765
|
+
if (!validation.installed) {
|
|
1766
|
+
const result = await promptAndInstallReactPdf(validation, cwd);
|
|
1767
|
+
if (!result.success) {
|
|
1768
|
+
console.error(chalk4.red(`
|
|
1769
|
+
${result.message}
|
|
1770
|
+
`));
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
if (!validation.valid) {
|
|
1776
|
+
console.log(
|
|
1777
|
+
chalk4.yellow(
|
|
1778
|
+
`
|
|
1779
|
+
\u26A0 ${validation.message}
|
|
1780
|
+
${chalk4.dim("\u2192")} You may encounter compatibility issues
|
|
1781
|
+
`
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// src/utils/pre-flight.ts
|
|
1789
|
+
import chalk5 from "chalk";
|
|
1790
|
+
|
|
1791
|
+
// src/utils/environment-validator.ts
|
|
1792
|
+
import path7 from "path";
|
|
1793
|
+
function validatePackageJson(cwd = process.cwd()) {
|
|
1794
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
1795
|
+
const exists = checkFileExists(pkgPath);
|
|
1796
|
+
return {
|
|
1797
|
+
valid: exists,
|
|
1798
|
+
message: exists ? "package.json found" : "No package.json found in current directory",
|
|
1799
|
+
fixCommand: exists ? void 0 : 'Run "npm init" or "pnpm init" to create a package.json'
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function validateReactProject(cwd = process.cwd()) {
|
|
1803
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
1804
|
+
if (!checkFileExists(pkgPath)) {
|
|
1805
|
+
return {
|
|
1806
|
+
valid: false,
|
|
1807
|
+
message: "Cannot validate React project without package.json"
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
try {
|
|
1811
|
+
const pkg = readJsonFile(pkgPath);
|
|
1812
|
+
const deps = {
|
|
1813
|
+
...pkg.dependencies,
|
|
1814
|
+
...pkg.devDependencies
|
|
1815
|
+
};
|
|
1816
|
+
const hasReact = "react" in deps;
|
|
1817
|
+
return {
|
|
1818
|
+
valid: hasReact,
|
|
1819
|
+
message: hasReact ? "React is installed" : "This does not appear to be a React project",
|
|
1820
|
+
fixCommand: hasReact ? void 0 : "Install React: npx create-vite@latest or npx create-react-app"
|
|
1821
|
+
};
|
|
1822
|
+
} catch {
|
|
1823
|
+
return {
|
|
1824
|
+
valid: false,
|
|
1825
|
+
message: "Failed to read package.json"
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function validateEnvironment(cwd = process.cwd()) {
|
|
1830
|
+
return {
|
|
1831
|
+
hasPackageJson: validatePackageJson(cwd),
|
|
1832
|
+
isReactProject: validateReactProject(cwd)
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/utils/pre-flight.ts
|
|
1837
|
+
function runPreFlightChecks(cwd = process.cwd()) {
|
|
1838
|
+
const environment = validateEnvironment(cwd);
|
|
1839
|
+
const dependencies = validateDependencies(cwd);
|
|
1840
|
+
const blockingErrors = [];
|
|
1841
|
+
const warnings = [];
|
|
1842
|
+
if (!environment.hasPackageJson.valid) {
|
|
1843
|
+
blockingErrors.push(
|
|
1844
|
+
`${environment.hasPackageJson.message}
|
|
1845
|
+
${chalk5.dim("\u2192")} ${environment.hasPackageJson.fixCommand}`
|
|
1846
|
+
);
|
|
1847
|
+
} else if (!environment.isReactProject.valid) {
|
|
1848
|
+
blockingErrors.push(
|
|
1849
|
+
`${environment.isReactProject.message}
|
|
1850
|
+
${chalk5.dim("\u2192")} ${environment.isReactProject.fixCommand}`
|
|
1851
|
+
);
|
|
1852
|
+
} else {
|
|
1853
|
+
if (!dependencies.react.valid && dependencies.react.installed) {
|
|
1854
|
+
warnings.push(
|
|
1855
|
+
`${dependencies.react.message}
|
|
1856
|
+
${chalk5.dim("\u2192")} Current: ${dependencies.react.currentVersion}, Required: ${dependencies.react.requiredVersion}`
|
|
1857
|
+
);
|
|
1858
|
+
} else if (!dependencies.react.installed) {
|
|
1859
|
+
blockingErrors.push(
|
|
1860
|
+
`${dependencies.react.message}
|
|
1861
|
+
${chalk5.dim("\u2192")} Install React: npm install react react-dom`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (!dependencies.nodeJs.valid) {
|
|
1866
|
+
blockingErrors.push(
|
|
1867
|
+
`${dependencies.nodeJs.message}
|
|
1868
|
+
${chalk5.dim("\u2192")} Current: ${dependencies.nodeJs.currentVersion}, Required: ${dependencies.nodeJs.requiredVersion}
|
|
1869
|
+
${chalk5.dim("\u2192")} Visit https://nodejs.org to upgrade`
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
if (dependencies.reactPdfRenderer.installed && !dependencies.reactPdfRenderer.valid) {
|
|
1873
|
+
warnings.push(
|
|
1874
|
+
`${dependencies.reactPdfRenderer.message}
|
|
1875
|
+
${chalk5.dim("\u2192")} Consider upgrading: npm install @react-pdf/renderer@latest`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
environment,
|
|
1880
|
+
dependencies,
|
|
1881
|
+
blockingErrors,
|
|
1882
|
+
warnings,
|
|
1883
|
+
canProceed: blockingErrors.length === 0
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function displayPreFlightResults(result) {
|
|
1887
|
+
console.log(chalk5.bold("\n Pre-flight Checks:\n"));
|
|
1888
|
+
if (result.blockingErrors.length > 0) {
|
|
1889
|
+
console.log(chalk5.red(" \u2717 Blocking Issues:\n"));
|
|
1890
|
+
for (const error of result.blockingErrors) {
|
|
1891
|
+
console.log(chalk5.red(` \u2022 ${error}
|
|
1892
|
+
`));
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
if (result.warnings.length > 0) {
|
|
1896
|
+
console.log(chalk5.yellow(" \u26A0 Warnings:\n"));
|
|
1897
|
+
for (const warning of result.warnings) {
|
|
1898
|
+
console.log(chalk5.yellow(` \u2022 ${warning}
|
|
1899
|
+
`));
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (result.blockingErrors.length === 0 && result.warnings.length === 0) {
|
|
1903
|
+
console.log(chalk5.green(" \u2713 All checks passed!\n"));
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/utils/theme-path.ts
|
|
1908
|
+
import path8 from "path";
|
|
1909
|
+
function normalizeThemePath(value) {
|
|
1910
|
+
const trimmed = value.trim();
|
|
1911
|
+
if (trimmed && !path8.isAbsolute(trimmed) && !trimmed.startsWith(".")) {
|
|
1912
|
+
return `./${trimmed}`;
|
|
1913
|
+
}
|
|
1914
|
+
return trimmed;
|
|
1915
|
+
}
|
|
1916
|
+
function validateThemePath(value) {
|
|
1917
|
+
const trimmed = value.trim();
|
|
1918
|
+
if (!trimmed) return "Theme path is required";
|
|
1919
|
+
if (path8.isAbsolute(trimmed)) {
|
|
1920
|
+
return "Please use a relative path (e.g., ./src/lib/pdfx-theme.ts)";
|
|
1921
|
+
}
|
|
1922
|
+
if (trimmed.startsWith(".") && !trimmed.startsWith("./") && !trimmed.startsWith("../")) {
|
|
1923
|
+
return "Path must start with ./ or ../ (e.g., ./src/lib/pdfx-theme.ts)";
|
|
1924
|
+
}
|
|
1925
|
+
if (!trimmed.endsWith(".ts") && !trimmed.endsWith(".tsx")) {
|
|
1926
|
+
return "Theme file must have a .ts or .tsx extension";
|
|
1927
|
+
}
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/commands/init.ts
|
|
1932
|
+
async function init(options = {}) {
|
|
1933
|
+
console.log(chalk6.bold.cyan("\n Welcome to the pdfx cli\n"));
|
|
1934
|
+
const preFlightResult = runPreFlightChecks();
|
|
1935
|
+
displayPreFlightResults(preFlightResult);
|
|
1936
|
+
if (!preFlightResult.canProceed) {
|
|
1937
|
+
console.error(
|
|
1938
|
+
chalk6.red("\n Cannot proceed due to blocking issues. Please fix them and try again.\n")
|
|
1939
|
+
);
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
}
|
|
1942
|
+
const hasReactPdf = await ensureReactPdfRenderer(preFlightResult.dependencies.reactPdfRenderer);
|
|
1943
|
+
if (!hasReactPdf) {
|
|
1944
|
+
console.error(
|
|
1945
|
+
chalk6.red("\n @react-pdf/renderer is required. Please install it and try again.\n")
|
|
1946
|
+
);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
const existingConfig = path9.join(process.cwd(), "pdfx.json");
|
|
1950
|
+
if (fs7.existsSync(existingConfig)) {
|
|
1951
|
+
if (options.yes) {
|
|
1952
|
+
console.log(chalk6.dim(" pdfx.json already exists \u2014 overwriting (--yes)."));
|
|
1953
|
+
} else {
|
|
1954
|
+
const { overwrite } = await prompts4({
|
|
1955
|
+
type: "confirm",
|
|
1956
|
+
name: "overwrite",
|
|
1957
|
+
message: "pdfx.json already exists. Overwrite?",
|
|
1958
|
+
initial: false
|
|
1959
|
+
});
|
|
1960
|
+
if (!overwrite) {
|
|
1961
|
+
console.log(chalk6.yellow("Init cancelled \u2014 existing config preserved."));
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
const answers = options.yes ? {
|
|
1967
|
+
componentDir: DEFAULTS.COMPONENT_DIR,
|
|
1968
|
+
blockDir: DEFAULTS.BLOCK_DIR,
|
|
1969
|
+
registry: DEFAULTS.REGISTRY_URL,
|
|
1970
|
+
themePreset: "professional",
|
|
1971
|
+
themePath: normalizeThemePath(DEFAULTS.THEME_FILE)
|
|
1972
|
+
} : await prompts4(
|
|
1973
|
+
[
|
|
1974
|
+
{
|
|
1975
|
+
type: "text",
|
|
1976
|
+
name: "componentDir",
|
|
1977
|
+
message: "Where should we install components?",
|
|
1978
|
+
initial: DEFAULTS.COMPONENT_DIR,
|
|
1979
|
+
validate: (value) => {
|
|
1980
|
+
if (!value || value.trim().length === 0) {
|
|
1981
|
+
return "Component directory is required";
|
|
1982
|
+
}
|
|
1983
|
+
if (path9.isAbsolute(value)) {
|
|
1984
|
+
return "Please use a relative path (e.g., ./src/components/pdfx)";
|
|
1985
|
+
}
|
|
1986
|
+
if (!value.startsWith(".")) {
|
|
1987
|
+
return "Path should start with ./ or ../ (e.g., ./src/components/pdfx)";
|
|
1988
|
+
}
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
type: "text",
|
|
1994
|
+
name: "blockDir",
|
|
1995
|
+
message: "Where should we install blocks?",
|
|
1996
|
+
initial: DEFAULTS.BLOCK_DIR,
|
|
1997
|
+
validate: (value) => {
|
|
1998
|
+
if (!value || value.trim().length === 0) {
|
|
1999
|
+
return "Block directory is required";
|
|
2000
|
+
}
|
|
2001
|
+
if (path9.isAbsolute(value)) {
|
|
2002
|
+
return "Please use a relative path (e.g., ./src/blocks/pdfx)";
|
|
2003
|
+
}
|
|
2004
|
+
if (!value.startsWith(".")) {
|
|
2005
|
+
return "Path should start with ./ or ../ (e.g., ./src/blocks/pdfx)";
|
|
2006
|
+
}
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
type: "text",
|
|
2012
|
+
name: "registry",
|
|
2013
|
+
message: "Registry URL:",
|
|
2014
|
+
initial: DEFAULTS.REGISTRY_URL,
|
|
2015
|
+
validate: (value) => {
|
|
2016
|
+
if (!value || !value.startsWith("http")) {
|
|
2017
|
+
return "Please enter a valid HTTP(S) URL";
|
|
2018
|
+
}
|
|
2019
|
+
return true;
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
type: "select",
|
|
2024
|
+
name: "themePreset",
|
|
2025
|
+
message: "Choose a theme:",
|
|
2026
|
+
choices: [
|
|
2027
|
+
{
|
|
2028
|
+
title: "Professional",
|
|
2029
|
+
description: "Serif headings, navy colors, generous margins",
|
|
2030
|
+
value: "professional"
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
title: "Modern",
|
|
2034
|
+
description: "Sans-serif, vibrant purple, tight spacing",
|
|
2035
|
+
value: "modern"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
title: "Minimal",
|
|
2039
|
+
description: "Monospace headings, stark black, maximum whitespace",
|
|
2040
|
+
value: "minimal"
|
|
2041
|
+
}
|
|
2042
|
+
],
|
|
2043
|
+
initial: 0
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
type: "text",
|
|
2047
|
+
name: "themePath",
|
|
2048
|
+
message: "Where should we create the theme file?",
|
|
2049
|
+
initial: DEFAULTS.THEME_FILE,
|
|
2050
|
+
format: normalizeThemePath,
|
|
2051
|
+
validate: validateThemePath
|
|
2052
|
+
}
|
|
2053
|
+
],
|
|
2054
|
+
{
|
|
2055
|
+
onCancel: () => {
|
|
2056
|
+
console.log(chalk6.yellow("\nSetup cancelled."));
|
|
2057
|
+
process.exit(0);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
);
|
|
2061
|
+
if (!answers.componentDir || !answers.registry) {
|
|
2062
|
+
console.error(chalk6.red("Missing required fields. Run npx pdfx-cli@latest init again."));
|
|
2063
|
+
process.exit(1);
|
|
2064
|
+
}
|
|
2065
|
+
const config = {
|
|
2066
|
+
$schema: DEFAULTS.SCHEMA_URL,
|
|
2067
|
+
componentDir: answers.componentDir,
|
|
2068
|
+
registry: answers.registry,
|
|
2069
|
+
theme: answers.themePath || DEFAULTS.THEME_FILE,
|
|
2070
|
+
blockDir: answers.blockDir || DEFAULTS.BLOCK_DIR
|
|
2071
|
+
};
|
|
2072
|
+
const validation = configSchema.safeParse(config);
|
|
2073
|
+
if (!validation.success) {
|
|
2074
|
+
const issues = validation.error.issues.map((i) => {
|
|
2075
|
+
const fieldPath = i.path.length > 0 ? i.path.join(".") : "root";
|
|
2076
|
+
return `"${fieldPath}": ${i.message}`;
|
|
2077
|
+
}).join("; ");
|
|
2078
|
+
console.error(chalk6.red(`Invalid configuration: ${issues}`));
|
|
2079
|
+
process.exit(1);
|
|
2080
|
+
}
|
|
2081
|
+
const spinner = ora5("Creating config and theme files...").start();
|
|
2082
|
+
try {
|
|
2083
|
+
const componentDirPath = path9.resolve(process.cwd(), answers.componentDir);
|
|
2084
|
+
ensureDir(componentDirPath);
|
|
2085
|
+
fs7.writeFileSync(path9.join(process.cwd(), "pdfx.json"), JSON.stringify(config, null, 2));
|
|
2086
|
+
const presetName = answers.themePreset || "professional";
|
|
2087
|
+
const preset = themePresets[presetName];
|
|
2088
|
+
const themePath = path9.resolve(process.cwd(), config.theme);
|
|
2089
|
+
ensureDir(path9.dirname(themePath));
|
|
2090
|
+
fs7.writeFileSync(themePath, generateThemeFile(preset), "utf-8");
|
|
2091
|
+
const contextPath = path9.join(path9.dirname(themePath), "pdfx-theme-context.tsx");
|
|
2092
|
+
fs7.writeFileSync(contextPath, generateThemeContextFile(), "utf-8");
|
|
2093
|
+
spinner.succeed(`Created pdfx.json + ${config.theme} (${presetName} theme)`);
|
|
2094
|
+
console.log(chalk6.green("\nSuccess! You can now run:"));
|
|
2095
|
+
console.log(chalk6.cyan(" npx pdfx-cli@latest add heading"));
|
|
2096
|
+
console.log(chalk6.cyan(" npx pdfx-cli@latest block add invoice-classic"));
|
|
2097
|
+
console.log(chalk6.dim(`
|
|
2098
|
+
Components: ${path9.resolve(process.cwd(), answers.componentDir)}`));
|
|
2099
|
+
console.log(chalk6.dim(` Blocks: ${path9.resolve(process.cwd(), config.blockDir)}`));
|
|
2100
|
+
console.log(chalk6.dim(` Theme: ${path9.resolve(process.cwd(), config.theme)}
|
|
2101
|
+
`));
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
spinner.fail("Failed to create config");
|
|
2104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2105
|
+
console.error(chalk6.dim(` ${message}`));
|
|
2106
|
+
process.exit(1);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// src/commands/list.ts
|
|
2111
|
+
import path10 from "path";
|
|
2112
|
+
import chalk7 from "chalk";
|
|
2113
|
+
import ora6 from "ora";
|
|
2114
|
+
async function list() {
|
|
2115
|
+
const configPath = path10.join(process.cwd(), "pdfx.json");
|
|
2116
|
+
let config;
|
|
2117
|
+
let hasLocalProject = false;
|
|
2118
|
+
if (checkFileExists(configPath)) {
|
|
2119
|
+
try {
|
|
2120
|
+
config = readConfig(configPath);
|
|
2121
|
+
hasLocalProject = true;
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
if (error instanceof ConfigError) {
|
|
2124
|
+
console.error(chalk7.red(error.message));
|
|
2125
|
+
if (error.suggestion) console.log(chalk7.yellow(` Hint: ${error.suggestion}`));
|
|
2126
|
+
} else {
|
|
2127
|
+
console.error(chalk7.red("Invalid pdfx.json"));
|
|
2128
|
+
}
|
|
2129
|
+
process.exit(1);
|
|
2130
|
+
}
|
|
2131
|
+
} else {
|
|
2132
|
+
config = {
|
|
2133
|
+
registry: DEFAULTS.REGISTRY_URL,
|
|
2134
|
+
componentDir: DEFAULTS.COMPONENT_DIR,
|
|
2135
|
+
theme: DEFAULTS.THEME_FILE,
|
|
2136
|
+
blockDir: DEFAULTS.BLOCK_DIR
|
|
2137
|
+
};
|
|
2138
|
+
console.log(chalk7.dim("No pdfx.json found. Listing from default registry.\n"));
|
|
2139
|
+
}
|
|
2140
|
+
const spinner = ora6("Fetching registry...").start();
|
|
2141
|
+
try {
|
|
2142
|
+
let response;
|
|
2143
|
+
try {
|
|
2144
|
+
response = await fetch(`${config.registry}/index.json`, {
|
|
2145
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2146
|
+
});
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
2149
|
+
throw new NetworkError(
|
|
2150
|
+
isTimeout ? `Registry request timed out after 10 seconds.
|
|
2151
|
+
${chalk7.dim("Check your internet connection or try again later.")}` : `Could not reach registry at ${config.registry}
|
|
2152
|
+
${chalk7.dim("Verify the URL is correct and you have internet access.")}`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
if (!response.ok) {
|
|
2156
|
+
throw new RegistryError(`Registry returned HTTP ${response.status}`);
|
|
2157
|
+
}
|
|
2158
|
+
const data = await response.json();
|
|
2159
|
+
const result = registrySchema.safeParse(data);
|
|
2160
|
+
if (!result.success) {
|
|
2161
|
+
throw new RegistryError("Invalid registry format");
|
|
2162
|
+
}
|
|
2163
|
+
spinner.stop();
|
|
2164
|
+
const components = result.data.items.filter((item) => item.type === "registry:ui");
|
|
2165
|
+
const blocks = result.data.items.filter((item) => item.type === "registry:block");
|
|
2166
|
+
const componentBaseDir = path10.resolve(process.cwd(), config.componentDir);
|
|
2167
|
+
const blockBaseDir = path10.resolve(process.cwd(), config.blockDir ?? DEFAULTS.BLOCK_DIR);
|
|
2168
|
+
console.log(chalk7.bold(`
|
|
2169
|
+
Components (${components.length})`));
|
|
2170
|
+
console.log(chalk7.dim(" Install with: npx pdfx-cli@latest add <component>\n"));
|
|
2171
|
+
for (const item of components) {
|
|
2172
|
+
const componentSubDir = path10.join(componentBaseDir, item.name);
|
|
2173
|
+
const localPath = safePath(componentSubDir, `pdfx-${item.name}.tsx`);
|
|
2174
|
+
const installed = hasLocalProject && checkFileExists(localPath);
|
|
2175
|
+
const status = installed ? chalk7.green("[installed]") : chalk7.dim("[not installed]");
|
|
2176
|
+
console.log(` ${chalk7.cyan(item.name.padEnd(20))} ${item.description}`);
|
|
2177
|
+
if (hasLocalProject) {
|
|
2178
|
+
console.log(` ${"".padEnd(20)} ${status}`);
|
|
2179
|
+
}
|
|
2180
|
+
console.log();
|
|
2181
|
+
}
|
|
2182
|
+
console.log(chalk7.bold(` Blocks (${blocks.length})`));
|
|
2183
|
+
console.log(
|
|
2184
|
+
chalk7.dim(" Copy-paste designs. Install with: npx pdfx-cli@latest block add <block>\n")
|
|
2185
|
+
);
|
|
2186
|
+
for (const item of blocks) {
|
|
2187
|
+
const blockDir = path10.join(blockBaseDir, item.name);
|
|
2188
|
+
const installed = hasLocalProject && checkFileExists(blockDir);
|
|
2189
|
+
const status = installed ? chalk7.green("[installed]") : chalk7.dim("[not installed]");
|
|
2190
|
+
console.log(` ${chalk7.cyan(item.name.padEnd(22))} ${item.description ?? ""}`);
|
|
2191
|
+
if (hasLocalProject) {
|
|
2192
|
+
console.log(` ${"".padEnd(22)} ${status}`);
|
|
2193
|
+
}
|
|
2194
|
+
if (item.peerComponents && item.peerComponents.length > 0) {
|
|
2195
|
+
console.log(chalk7.dim(` ${"".padEnd(22)} requires: ${item.peerComponents.join(", ")}`));
|
|
2196
|
+
}
|
|
2197
|
+
console.log();
|
|
2198
|
+
}
|
|
2199
|
+
console.log(chalk7.dim(" Quick Start:"));
|
|
2200
|
+
console.log(
|
|
2201
|
+
chalk7.dim(
|
|
2202
|
+
` npx pdfx-cli@latest add heading table ${chalk7.dim("# Add components")}`
|
|
2203
|
+
)
|
|
2204
|
+
);
|
|
2205
|
+
console.log(
|
|
2206
|
+
chalk7.dim(` npx pdfx-cli@latest block add invoice-classic ${chalk7.dim("# Add a block")}`)
|
|
2207
|
+
);
|
|
2208
|
+
console.log();
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2211
|
+
spinner.fail(message);
|
|
2212
|
+
process.exit(1);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/commands/mcp.ts
|
|
2217
|
+
import { existsSync } from "fs";
|
|
2218
|
+
import { mkdir, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
2219
|
+
import path11 from "path";
|
|
2220
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2221
|
+
import { Command } from "commander";
|
|
2222
|
+
import prompts5 from "prompts";
|
|
2223
|
+
|
|
2224
|
+
// src/mcp/index.ts
|
|
2225
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2226
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2227
|
+
import { z as z8 } from "zod";
|
|
2228
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2229
|
+
|
|
2230
|
+
// src/mcp/tools/add-command.ts
|
|
2231
|
+
import dedent from "dedent";
|
|
2232
|
+
import { z as z2 } from "zod";
|
|
2233
|
+
|
|
2234
|
+
// src/mcp/utils.ts
|
|
2235
|
+
var REGISTRY_BASE = DEFAULTS.REGISTRY_URL;
|
|
2236
|
+
var BLOCKS_BASE = `${DEFAULTS.REGISTRY_URL}/${REGISTRY_SUBPATHS.BLOCKS}`;
|
|
2237
|
+
async function fetchRegistryIndex() {
|
|
2238
|
+
let response;
|
|
2239
|
+
try {
|
|
2240
|
+
response = await fetch(`${REGISTRY_BASE}/index.json`, {
|
|
2241
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2242
|
+
});
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
2245
|
+
throw new NetworkError(
|
|
2246
|
+
isTimeout ? "Registry request timed out. Check your internet connection." : "Could not reach the PDFx registry. Check your internet connection."
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
if (!response.ok) {
|
|
2250
|
+
throw new RegistryError(`Registry returned HTTP ${response.status}`);
|
|
2251
|
+
}
|
|
2252
|
+
let data;
|
|
2253
|
+
try {
|
|
2254
|
+
data = await response.json();
|
|
2255
|
+
} catch {
|
|
2256
|
+
throw new RegistryError("Registry returned invalid JSON");
|
|
2257
|
+
}
|
|
2258
|
+
const result = registrySchema.safeParse(data);
|
|
2259
|
+
if (!result.success) {
|
|
2260
|
+
throw new RegistryError("Registry index has an unexpected format");
|
|
2261
|
+
}
|
|
2262
|
+
return result.data.items;
|
|
2263
|
+
}
|
|
2264
|
+
async function fetchRegistryItem(name, base = REGISTRY_BASE) {
|
|
2265
|
+
const url = `${base}/${name}.json`;
|
|
2266
|
+
let response;
|
|
2267
|
+
try {
|
|
2268
|
+
response = await fetch(url, {
|
|
2269
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
2270
|
+
});
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
const isTimeout = err instanceof Error && err.name === "TimeoutError";
|
|
2273
|
+
throw new NetworkError(
|
|
2274
|
+
isTimeout ? `Registry request timed out for "${name}".` : "Could not reach the PDFx registry. Check your internet connection."
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
if (!response.ok) {
|
|
2278
|
+
if (response.status === 404) {
|
|
2279
|
+
throw new RegistryError(
|
|
2280
|
+
`"${name}" not found in the registry. Use list_components or list_blocks to see available items.`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
throw new RegistryError(`Registry returned HTTP ${response.status} for "${name}"`);
|
|
2284
|
+
}
|
|
2285
|
+
let data;
|
|
2286
|
+
try {
|
|
2287
|
+
data = await response.json();
|
|
2288
|
+
} catch {
|
|
2289
|
+
throw new RegistryError(`Registry returned invalid JSON for "${name}"`);
|
|
2290
|
+
}
|
|
2291
|
+
const result = registryItemSchema.safeParse(data);
|
|
2292
|
+
if (!result.success) {
|
|
2293
|
+
throw new RegistryError(`Unexpected registry format for "${name}"`);
|
|
2294
|
+
}
|
|
2295
|
+
return result.data;
|
|
2296
|
+
}
|
|
2297
|
+
function textResponse(text) {
|
|
2298
|
+
return { content: [{ type: "text", text }] };
|
|
2299
|
+
}
|
|
2300
|
+
function errorResponse(error) {
|
|
2301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2302
|
+
return {
|
|
2303
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2304
|
+
isError: true
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/mcp/tools/add-command.ts
|
|
2309
|
+
var getAddCommandSchema = z2.object({
|
|
2310
|
+
items: z2.array(z2.string().min(1)).min(1).describe(
|
|
2311
|
+
"Item names to add, e.g. ['table', 'heading'] for components or ['invoice-modern'] for blocks"
|
|
2312
|
+
),
|
|
2313
|
+
type: z2.enum(["component", "block"]).describe("Whether the items are components or blocks")
|
|
2314
|
+
});
|
|
2315
|
+
async function getAddCommand(args) {
|
|
2316
|
+
const isBlock = args.type === "block";
|
|
2317
|
+
const cmd = isBlock ? `npx pdfx-cli block add ${args.items.join(" ")}` : `npx pdfx-cli add ${args.items.join(" ")}`;
|
|
2318
|
+
const installDir = isBlock ? "src/blocks/pdfx/" : "src/components/pdfx/";
|
|
2319
|
+
const inspectTool = isBlock ? "get_block" : "get_component";
|
|
2320
|
+
const itemList = args.items.map((i) => `- \`${i}\``).join("\n");
|
|
2321
|
+
return textResponse(dedent`
|
|
2322
|
+
# Add Command
|
|
2323
|
+
|
|
2324
|
+
\`\`\`bash
|
|
2325
|
+
${cmd}
|
|
2326
|
+
\`\`\`
|
|
2327
|
+
|
|
2328
|
+
**Items:**
|
|
2329
|
+
${itemList}
|
|
2330
|
+
|
|
2331
|
+
**What this does:**
|
|
2332
|
+
- Copies source files into \`${installDir}\`
|
|
2333
|
+
- You own the code — no runtime package is added
|
|
2334
|
+
${isBlock ? "- The block includes a complete document layout ready to customize" : "- Each component gets its own subdirectory inside componentDir"}
|
|
2335
|
+
|
|
2336
|
+
**Before running:** make sure \`pdfx.json\` exists. Run \`npx pdfx-cli init\` if not.
|
|
2337
|
+
|
|
2338
|
+
**See source first:** call \`${inspectTool}\` with the item name to review the code before adding.
|
|
2339
|
+
|
|
2340
|
+
**After adding:** call \`get_audit_checklist\` to verify your setup is correct.
|
|
2341
|
+
`);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// src/mcp/tools/audit.ts
|
|
2345
|
+
import dedent2 from "dedent";
|
|
2346
|
+
async function getAuditChecklist() {
|
|
2347
|
+
return textResponse(dedent2`
|
|
2348
|
+
# PDFx Setup Audit Checklist
|
|
2349
|
+
|
|
2350
|
+
Work through this after adding components or generating PDF document code.
|
|
2351
|
+
|
|
2352
|
+
## Configuration
|
|
2353
|
+
- [ ] \`pdfx.json\` exists in the project root
|
|
2354
|
+
- [ ] \`componentDir\` path in \`pdfx.json\` is correct (default: \`./src/components/pdfx\`)
|
|
2355
|
+
- [ ] Theme file exists at the path set in \`pdfx.json\` (default: \`./src/lib/pdfx-theme.ts\`)
|
|
2356
|
+
|
|
2357
|
+
## Dependencies
|
|
2358
|
+
- [ ] \`@react-pdf/renderer\` is installed — run \`npm ls @react-pdf/renderer\` to confirm
|
|
2359
|
+
- [ ] Version is ≥ 3.0.0 (PDFx requires react-pdf v3+)
|
|
2360
|
+
|
|
2361
|
+
## Imports
|
|
2362
|
+
- [ ] PDFx components use **named exports**: \`import { Table, Heading } from '@/components/pdfx/...'\`
|
|
2363
|
+
- [ ] \`Document\` and \`Page\` are imported from \`@react-pdf/renderer\`, not from PDFx
|
|
2364
|
+
- [ ] No Tailwind classes, CSS variables, or DOM APIs are used inside PDF components
|
|
2365
|
+
- [ ] All styles use \`StyleSheet.create({})\` from \`@react-pdf/renderer\`
|
|
2366
|
+
|
|
2367
|
+
## Rendering
|
|
2368
|
+
- [ ] The root PDF component is **not** inside a React Server Component
|
|
2369
|
+
- [ ] Using \`renderToBuffer\`, \`PDFViewer\`, or \`PDFDownloadLink\` to render the document
|
|
2370
|
+
- [ ] Root component returns \`<Document><Page>...</Page></Document>\`
|
|
2371
|
+
- [ ] No console errors about missing fonts
|
|
2372
|
+
|
|
2373
|
+
## TypeScript
|
|
2374
|
+
- [ ] No TypeScript errors in component files
|
|
2375
|
+
- [ ] Theme is typed as \`PdfxTheme\` (imported as a type from \`@pdfx/shared\`)
|
|
2376
|
+
|
|
2377
|
+
---
|
|
2378
|
+
|
|
2379
|
+
## Common Issues & Fixes
|
|
2380
|
+
|
|
2381
|
+
### "Cannot find module @/components/pdfx/..."
|
|
2382
|
+
The component hasn't been added yet. Run:
|
|
2383
|
+
\`\`\`bash
|
|
2384
|
+
npx pdfx-cli add <component-name>
|
|
2385
|
+
\`\`\`
|
|
2386
|
+
|
|
2387
|
+
### "Invalid hook call"
|
|
2388
|
+
PDFx components render to PDF, not to the DOM — React hooks are not supported inside them.
|
|
2389
|
+
Move hook calls to the parent component and pass data down as props.
|
|
2390
|
+
|
|
2391
|
+
### "Text strings must be rendered inside \`<Text>\` component"
|
|
2392
|
+
Wrap all string literals in \`<Text>\` from \`@react-pdf/renderer\`:
|
|
2393
|
+
\`\`\`tsx
|
|
2394
|
+
import { Text } from '@react-pdf/renderer';
|
|
2395
|
+
// ✗ Wrong: <View>Hello</View>
|
|
2396
|
+
// ✓ Correct: <View><Text>Hello</Text></View>
|
|
2397
|
+
\`\`\`
|
|
2398
|
+
|
|
2399
|
+
### Fonts not loading / rendering as a fallback
|
|
2400
|
+
Register custom fonts in your theme file:
|
|
2401
|
+
\`\`\`tsx
|
|
2402
|
+
import { Font } from '@react-pdf/renderer';
|
|
2403
|
+
|
|
2404
|
+
Font.register({
|
|
2405
|
+
family: 'Inter',
|
|
2406
|
+
src: 'https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2',
|
|
2407
|
+
});
|
|
2408
|
+
\`\`\`
|
|
2409
|
+
|
|
2410
|
+
### PDF renders blank or empty
|
|
2411
|
+
Ensure your root component returns a \`<Document>\` with at least one \`<Page>\` inside:
|
|
2412
|
+
\`\`\`tsx
|
|
2413
|
+
import { Document, Page } from '@react-pdf/renderer';
|
|
2414
|
+
|
|
2415
|
+
export function MyDocument() {
|
|
2416
|
+
return (
|
|
2417
|
+
<Document>
|
|
2418
|
+
<Page size="A4">
|
|
2419
|
+
{/* content here */}
|
|
2420
|
+
</Page>
|
|
2421
|
+
</Document>
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
\`\`\`
|
|
2425
|
+
|
|
2426
|
+
### @react-pdf/renderer TypeScript errors
|
|
2427
|
+
Install the types package:
|
|
2428
|
+
\`\`\`bash
|
|
2429
|
+
npm install --save-dev @react-pdf/types
|
|
2430
|
+
\`\`\`
|
|
2431
|
+
|
|
2432
|
+
---
|
|
2433
|
+
|
|
2434
|
+
## @react-pdf/renderer Layout Constraints
|
|
2435
|
+
|
|
2436
|
+
These are **fundamental PDF rendering limitations** — they cannot be fixed with CSS-like
|
|
2437
|
+
style tweaks. Understanding them will save hours of debugging.
|
|
2438
|
+
|
|
2439
|
+
### ⚠️ CRITICAL: Do NOT mix \`<View>\` and \`<Text>\` in the same flex row
|
|
2440
|
+
|
|
2441
|
+
In HTML, inline elements (spans, badges) can sit next to block text freely.
|
|
2442
|
+
In \`@react-pdf/renderer\`, \`View\` and \`Text\` are fundamentally different node types.
|
|
2443
|
+
Placing a \`View\`-based component (e.g. \`<Badge>\`, \`<PdfAlert>\`) **inline** alongside
|
|
2444
|
+
a \`<Text>\` node in the same flex row causes irrecoverable misalignment, overlap, and
|
|
2445
|
+
overflow that no amount of padding, margin, or \`alignItems\` can fix.
|
|
2446
|
+
|
|
2447
|
+
**Wrong — will cause layout corruption:**
|
|
2448
|
+
\`\`\`tsx
|
|
2449
|
+
{/* Badge is a View; Text is a Text node — they CANNOT share a flex row */}
|
|
2450
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
2451
|
+
<Text>INV-2026-001</Text>
|
|
2452
|
+
<Badge label="PAID" variant="success" />
|
|
2453
|
+
</View>
|
|
2454
|
+
\`\`\`
|
|
2455
|
+
|
|
2456
|
+
**Correct — place View-based components on their own line:**
|
|
2457
|
+
\`\`\`tsx
|
|
2458
|
+
<View style={{ flexDirection: 'column', gap: 2 }}>
|
|
2459
|
+
<Text>INV-2026-001</Text>
|
|
2460
|
+
<Badge label="PAID" variant="success" />
|
|
2461
|
+
</View>
|
|
2462
|
+
\`\`\`
|
|
2463
|
+
|
|
2464
|
+
PDFx components that are \`View\`-based (cannot be mixed inline with \`<Text>\`):
|
|
2465
|
+
\`Badge\`, \`PdfAlert\`, \`Card\`, \`Divider\`, \`KeyValue\`, \`Section\`, \`Table\`, \`DataTable\`,
|
|
2466
|
+
\`PdfGraph\`, \`PdfImage\`, \`PdfSignatureBlock\`, \`PdfList\`
|
|
2467
|
+
|
|
2468
|
+
### No \`position: absolute\` stacking inside \`<Text>\` nodes
|
|
2469
|
+
Absolute positioning works on \`View\` elements but not inside \`Text\` runs.
|
|
2470
|
+
|
|
2471
|
+
### \`gap\` only works between \`View\` siblings
|
|
2472
|
+
Use \`gap\` on a \`View\` container whose children are all \`View\` elements.
|
|
2473
|
+
If any child is a raw \`Text\` node, use \`marginBottom\` on siblings instead.
|
|
2474
|
+
|
|
2475
|
+
### No percentage-based font sizes
|
|
2476
|
+
\`@react-pdf/renderer\` requires numeric pt/px values for \`fontSize\`. Do not use strings like \`"1rem"\` or \`"120%"\`.
|
|
2477
|
+
`);
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// src/mcp/tools/blocks.ts
|
|
2481
|
+
import dedent3 from "dedent";
|
|
2482
|
+
import { z as z3 } from "zod";
|
|
2483
|
+
var listBlocksSchema = z3.object({});
|
|
2484
|
+
async function listBlocks() {
|
|
2485
|
+
const items = await fetchRegistryIndex();
|
|
2486
|
+
const blocks = items.filter((i) => i.type === "registry:block");
|
|
2487
|
+
const invoices = blocks.filter((b) => b.name.startsWith("invoice-"));
|
|
2488
|
+
const reports = blocks.filter((b) => b.name.startsWith("report-"));
|
|
2489
|
+
const others = blocks.filter(
|
|
2490
|
+
(b) => !b.name.startsWith("invoice-") && !b.name.startsWith("report-")
|
|
2491
|
+
);
|
|
2492
|
+
const formatBlock = (b) => {
|
|
2493
|
+
const peers = b.peerComponents?.length ? ` _(requires: ${b.peerComponents.join(", ")})_` : "";
|
|
2494
|
+
return `- **${b.name}** \u2014 ${b.description ?? "No description"}${peers}`;
|
|
2495
|
+
};
|
|
2496
|
+
const sections = [];
|
|
2497
|
+
if (invoices.length > 0) {
|
|
2498
|
+
sections.push(
|
|
2499
|
+
`### Invoice Blocks (${invoices.length})
|
|
2500
|
+
${invoices.map(formatBlock).join("\n")}`
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
if (reports.length > 0) {
|
|
2504
|
+
sections.push(`### Report Blocks (${reports.length})
|
|
2505
|
+
${reports.map(formatBlock).join("\n")}`);
|
|
2506
|
+
}
|
|
2507
|
+
if (others.length > 0) {
|
|
2508
|
+
sections.push(`### Other Blocks (${others.length})
|
|
2509
|
+
${others.map(formatBlock).join("\n")}`);
|
|
2510
|
+
}
|
|
2511
|
+
return textResponse(dedent3`
|
|
2512
|
+
# PDFx Blocks (${blocks.length})
|
|
2513
|
+
|
|
2514
|
+
Blocks are complete, copy-paste ready document layouts. Unlike components, they are full documents ready to customize.
|
|
2515
|
+
|
|
2516
|
+
${sections.join("\n\n")}
|
|
2517
|
+
|
|
2518
|
+
---
|
|
2519
|
+
Add a block: \`npx pdfx-cli block add <name>\`
|
|
2520
|
+
See full source: call \`get_block\` with the block name
|
|
2521
|
+
`);
|
|
2522
|
+
}
|
|
2523
|
+
var getBlockSchema = z3.object({
|
|
2524
|
+
block: z3.string().min(1).describe("Block name, e.g. 'invoice-modern', 'report-financial'")
|
|
2525
|
+
});
|
|
2526
|
+
async function getBlock(args) {
|
|
2527
|
+
const item = await fetchRegistryItem(args.block, BLOCKS_BASE);
|
|
2528
|
+
const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
|
|
2529
|
+
const peers = item.peerComponents?.length ? item.peerComponents.join(", ") : "none";
|
|
2530
|
+
const fileSources = item.files.map(
|
|
2531
|
+
(f) => dedent3`
|
|
2532
|
+
### \`${f.path}\`
|
|
2533
|
+
\`\`\`tsx
|
|
2534
|
+
${f.content}
|
|
2535
|
+
\`\`\`
|
|
2536
|
+
`
|
|
2537
|
+
).join("\n\n");
|
|
2538
|
+
return textResponse(dedent3`
|
|
2539
|
+
# ${item.title ?? item.name}
|
|
2540
|
+
|
|
2541
|
+
${item.description ?? ""}
|
|
2542
|
+
|
|
2543
|
+
## Files
|
|
2544
|
+
${fileList}
|
|
2545
|
+
|
|
2546
|
+
## Required PDFx Components
|
|
2547
|
+
${peers}
|
|
2548
|
+
|
|
2549
|
+
## Add Command
|
|
2550
|
+
\`\`\`bash
|
|
2551
|
+
npx pdfx-cli block add ${args.block}
|
|
2552
|
+
\`\`\`
|
|
2553
|
+
|
|
2554
|
+
## Source Code
|
|
2555
|
+
${fileSources}
|
|
2556
|
+
`);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// src/mcp/tools/components.ts
|
|
2560
|
+
import dedent4 from "dedent";
|
|
2561
|
+
import { z as z4 } from "zod";
|
|
2562
|
+
var listComponentsSchema = z4.object({});
|
|
2563
|
+
async function listComponents() {
|
|
2564
|
+
const items = await fetchRegistryIndex();
|
|
2565
|
+
const components = items.filter((i) => i.type === "registry:ui");
|
|
2566
|
+
const rows = components.map((c) => `- **${c.name}** \u2014 ${c.description ?? "No description"}`).join("\n");
|
|
2567
|
+
return textResponse(dedent4`
|
|
2568
|
+
# PDFx Components (${components.length})
|
|
2569
|
+
|
|
2570
|
+
${rows}
|
|
2571
|
+
|
|
2572
|
+
---
|
|
2573
|
+
Add a component: \`npx pdfx-cli add <name>\`
|
|
2574
|
+
See full source, props, and exact export name: call \`get_component\` with the component name
|
|
2575
|
+
`);
|
|
2576
|
+
}
|
|
2577
|
+
var getComponentSchema = z4.object({
|
|
2578
|
+
component: z4.string().min(1).describe("Component name, e.g. 'table', 'heading', 'data-table'")
|
|
2579
|
+
});
|
|
2580
|
+
async function getComponent(args) {
|
|
2581
|
+
const item = await fetchRegistryItem(args.component);
|
|
2582
|
+
const fileList = item.files.map((f) => `- \`${f.path}\``).join("\n");
|
|
2583
|
+
const deps = item.dependencies?.length ? item.dependencies.join(", ") : "none";
|
|
2584
|
+
const devDeps = item.devDependencies?.length ? item.devDependencies.join(", ") : "none";
|
|
2585
|
+
const registryDeps = item.registryDependencies?.length ? item.registryDependencies.join(", ") : "none";
|
|
2586
|
+
const primaryContent = item.files[0]?.content ?? "";
|
|
2587
|
+
const primaryPath = item.files[0]?.path ?? "";
|
|
2588
|
+
const exportNames = extractAllExportNames(primaryContent);
|
|
2589
|
+
const mainExport = extractExportName(primaryContent);
|
|
2590
|
+
const exportSection = exportNames.length > 0 ? dedent4`
|
|
2591
|
+
## Exports
|
|
2592
|
+
**Main component export:** \`${mainExport ?? exportNames[0]}\`
|
|
2593
|
+
|
|
2594
|
+
All named exports from \`${primaryPath}\`:
|
|
2595
|
+
${exportNames.map((n) => `- \`${n}\``).join("\n")}
|
|
2596
|
+
|
|
2597
|
+
**Import after \`npx pdfx-cli@latest add ${args.component}\`:**
|
|
2598
|
+
\`\`\`tsx
|
|
2599
|
+
import { ${mainExport ?? exportNames[0]} } from './components/pdfx/${args.component}/pdfx-${args.component}';
|
|
2600
|
+
\`\`\`
|
|
2601
|
+
` : "";
|
|
2602
|
+
const fileSources = item.files.map(
|
|
2603
|
+
(f) => dedent4`
|
|
2604
|
+
### \`${f.path}\`
|
|
2605
|
+
\`\`\`tsx
|
|
2606
|
+
${f.content}
|
|
2607
|
+
\`\`\`
|
|
2608
|
+
`
|
|
2609
|
+
).join("\n\n");
|
|
2610
|
+
return textResponse(dedent4`
|
|
2611
|
+
# ${item.title ?? item.name}
|
|
2612
|
+
|
|
2613
|
+
${item.description ?? ""}
|
|
2614
|
+
|
|
2615
|
+
## Files
|
|
2616
|
+
${fileList}
|
|
2617
|
+
|
|
2618
|
+
## Dependencies
|
|
2619
|
+
- Runtime: ${deps}
|
|
2620
|
+
- Dev: ${devDeps}
|
|
2621
|
+
- Other PDFx components required: ${registryDeps}
|
|
2622
|
+
|
|
2623
|
+
${exportSection}
|
|
2624
|
+
|
|
2625
|
+
## Add Command
|
|
2626
|
+
\`\`\`bash
|
|
2627
|
+
npx pdfx-cli add ${args.component}
|
|
2628
|
+
\`\`\`
|
|
2629
|
+
|
|
2630
|
+
## Source Code
|
|
2631
|
+
${fileSources}
|
|
2632
|
+
`);
|
|
2633
|
+
}
|
|
2634
|
+
function extractExportName(source) {
|
|
2635
|
+
if (!source) return null;
|
|
2636
|
+
const matches = [...source.matchAll(/export\s+(?:function|const)\s+([A-Z][A-Za-z0-9]*)/g)];
|
|
2637
|
+
if (matches.length === 0) return null;
|
|
2638
|
+
return matches[0][1] ?? null;
|
|
2639
|
+
}
|
|
2640
|
+
function extractAllExportNames(source) {
|
|
2641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2642
|
+
const results = [];
|
|
2643
|
+
for (const m of source.matchAll(/export\s+(?:function|const|class)\s+([A-Za-z][A-Za-z0-9]*)/g)) {
|
|
2644
|
+
const name = m[1];
|
|
2645
|
+
if (name && !seen.has(name)) {
|
|
2646
|
+
seen.add(name);
|
|
2647
|
+
results.push(name);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
for (const m of source.matchAll(/export\s+\{([^}]+)\}/g)) {
|
|
2651
|
+
for (const part of m[1].split(",")) {
|
|
2652
|
+
const name = part.trim().split(/\s+as\s+/).pop()?.trim();
|
|
2653
|
+
if (name && /^[A-Za-z]/.test(name) && !seen.has(name)) {
|
|
2654
|
+
seen.add(name);
|
|
2655
|
+
results.push(name);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
return results;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// src/mcp/tools/installation.ts
|
|
2663
|
+
import dedent5 from "dedent";
|
|
2664
|
+
import { z as z5 } from "zod";
|
|
2665
|
+
var getInstallationSchema = z5.object({
|
|
2666
|
+
framework: z5.enum(["nextjs", "react", "vite", "remix", "other"]).describe("Target framework"),
|
|
2667
|
+
package_manager: z5.enum(["npm", "pnpm", "yarn", "bun"]).describe("Package manager to use")
|
|
2668
|
+
});
|
|
2669
|
+
function installCmd(pm, pkg, dev = false) {
|
|
2670
|
+
const base = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" }[pm];
|
|
2671
|
+
const devFlag = { npm: "--save-dev", pnpm: "-D", yarn: "--dev", bun: "-d" }[pm];
|
|
2672
|
+
return dev ? `${base} ${devFlag} ${pkg}` : `${base} ${pkg}`;
|
|
2673
|
+
}
|
|
2674
|
+
var FRAMEWORK_NOTES = {
|
|
2675
|
+
nextjs: dedent5`
|
|
2676
|
+
## Next.js Notes
|
|
2677
|
+
|
|
2678
|
+
**App Router** — Use a Route Handler to serve PDFs:
|
|
2679
|
+
\`\`\`tsx
|
|
2680
|
+
// app/api/pdf/route.ts
|
|
2681
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
2682
|
+
import { MyDocument } from '@/components/pdfx/my-document';
|
|
2683
|
+
|
|
2684
|
+
export async function GET() {
|
|
2685
|
+
const buffer = await renderToBuffer(<MyDocument />);
|
|
2686
|
+
return new Response(buffer, {
|
|
2687
|
+
headers: { 'Content-Type': 'application/pdf' },
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
\`\`\`
|
|
2691
|
+
|
|
2692
|
+
**Important:** Do NOT render PDFx components inside React Server Components.
|
|
2693
|
+
Always use a \`'use client'\` boundary or a Route Handler.
|
|
2694
|
+
|
|
2695
|
+
**Pages Router** — Use an API route:
|
|
2696
|
+
\`\`\`tsx
|
|
2697
|
+
// pages/api/pdf.ts
|
|
2698
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
2699
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
2700
|
+
import { MyDocument } from '@/components/pdfx/my-document';
|
|
2701
|
+
|
|
2702
|
+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
2703
|
+
const buffer = await renderToBuffer(<MyDocument />);
|
|
2704
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
2705
|
+
res.send(buffer);
|
|
2706
|
+
}
|
|
2707
|
+
\`\`\`
|
|
2708
|
+
`,
|
|
2709
|
+
react: dedent5`
|
|
2710
|
+
## React Notes
|
|
2711
|
+
|
|
2712
|
+
Display a PDF inline with \`PDFViewer\`:
|
|
2713
|
+
\`\`\`tsx
|
|
2714
|
+
import { PDFViewer } from '@react-pdf/renderer';
|
|
2715
|
+
import { MyDocument } from './components/pdfx/my-document';
|
|
2716
|
+
|
|
2717
|
+
export function App() {
|
|
2718
|
+
return (
|
|
2719
|
+
<PDFViewer width="100%" height="600px">
|
|
2720
|
+
<MyDocument />
|
|
2721
|
+
</PDFViewer>
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
\`\`\`
|
|
2725
|
+
|
|
2726
|
+
Trigger a download with \`PDFDownloadLink\`:
|
|
2727
|
+
\`\`\`tsx
|
|
2728
|
+
import { PDFDownloadLink } from '@react-pdf/renderer';
|
|
2729
|
+
import { MyDocument } from './components/pdfx/my-document';
|
|
2730
|
+
|
|
2731
|
+
export function DownloadButton() {
|
|
2732
|
+
return (
|
|
2733
|
+
<PDFDownloadLink document={<MyDocument />} fileName="document.pdf">
|
|
2734
|
+
{({ loading }) => (loading ? 'Generating PDF...' : 'Download PDF')}
|
|
2735
|
+
</PDFDownloadLink>
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
\`\`\`
|
|
2739
|
+
`,
|
|
2740
|
+
vite: dedent5`
|
|
2741
|
+
## Vite Notes
|
|
2742
|
+
|
|
2743
|
+
Works with both \`vite + react\` and \`vite + react-swc\` templates.
|
|
2744
|
+
|
|
2745
|
+
For client-side rendering, use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`.
|
|
2746
|
+
|
|
2747
|
+
For server-side generation, use a separate Node.js server or Vite's server-side features.
|
|
2748
|
+
`,
|
|
2749
|
+
remix: dedent5`
|
|
2750
|
+
## Remix Notes
|
|
2751
|
+
|
|
2752
|
+
Use a resource route to serve PDFs:
|
|
2753
|
+
\`\`\`tsx
|
|
2754
|
+
// app/routes/pdf.tsx
|
|
2755
|
+
import { renderToStream } from '@react-pdf/renderer';
|
|
2756
|
+
import { MyDocument } from '~/components/pdfx/my-document';
|
|
2757
|
+
|
|
2758
|
+
export async function loader() {
|
|
2759
|
+
const stream = await renderToStream(<MyDocument />);
|
|
2760
|
+
return new Response(stream as unknown as ReadableStream, {
|
|
2761
|
+
headers: { 'Content-Type': 'application/pdf' },
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
\`\`\`
|
|
2765
|
+
`,
|
|
2766
|
+
other: dedent5`
|
|
2767
|
+
## General Notes
|
|
2768
|
+
|
|
2769
|
+
\`@react-pdf/renderer\` works in any Node.js ≥ 18 environment.
|
|
2770
|
+
|
|
2771
|
+
- **Buffer output**: \`await renderToBuffer(<MyDocument />)\`
|
|
2772
|
+
- **Stream output**: \`await renderToStream(<MyDocument />)\`
|
|
2773
|
+
- **Client-side**: Use \`PDFViewer\` or \`PDFDownloadLink\` from \`@react-pdf/renderer\`
|
|
2774
|
+
`
|
|
2775
|
+
};
|
|
2776
|
+
async function getInstallation(args) {
|
|
2777
|
+
const pm = args.package_manager;
|
|
2778
|
+
const fw = args.framework;
|
|
2779
|
+
return textResponse(dedent5`
|
|
2780
|
+
# PDFx Setup Guide: ${fw} + ${pm}
|
|
2781
|
+
|
|
2782
|
+
## Step 1 — Install the peer dependency
|
|
2783
|
+
|
|
2784
|
+
\`\`\`bash
|
|
2785
|
+
${installCmd(pm, "@react-pdf/renderer")}
|
|
2786
|
+
\`\`\`
|
|
2787
|
+
|
|
2788
|
+
## Step 2 — Initialize PDFx in your project
|
|
2789
|
+
|
|
2790
|
+
\`\`\`bash
|
|
2791
|
+
npx pdfx-cli init
|
|
2792
|
+
\`\`\`
|
|
2793
|
+
|
|
2794
|
+
This creates \`pdfx.json\` in your project root and generates a theme file at \`src/lib/pdfx-theme.ts\`.
|
|
2795
|
+
|
|
2796
|
+
## Step 3 — Add your first component
|
|
2797
|
+
|
|
2798
|
+
\`\`\`bash
|
|
2799
|
+
npx pdfx-cli add heading text table
|
|
2800
|
+
\`\`\`
|
|
2801
|
+
|
|
2802
|
+
Components are copied into \`src/components/pdfx/\`. You own the source — there is no runtime package dependency.
|
|
2803
|
+
|
|
2804
|
+
## Step 4 — Or start with a complete document block
|
|
2805
|
+
|
|
2806
|
+
\`\`\`bash
|
|
2807
|
+
npx pdfx-cli block add invoice-modern
|
|
2808
|
+
\`\`\`
|
|
2809
|
+
|
|
2810
|
+
${FRAMEWORK_NOTES[fw]}
|
|
2811
|
+
|
|
2812
|
+
## Generated pdfx.json
|
|
2813
|
+
|
|
2814
|
+
\`\`\`json
|
|
2815
|
+
{
|
|
2816
|
+
"$schema": "https://pdfx.akashpise.dev/schema.json",
|
|
2817
|
+
"componentDir": "./src/components/pdfx",
|
|
2818
|
+
"blockDir": "./src/blocks/pdfx",
|
|
2819
|
+
"registry": "https://pdfx.akashpise.dev/r",
|
|
2820
|
+
"theme": "./src/lib/pdfx-theme.ts"
|
|
2821
|
+
}
|
|
2822
|
+
\`\`\`
|
|
2823
|
+
|
|
2824
|
+
## pdfx.json Field Reference
|
|
2825
|
+
|
|
2826
|
+
All four fields are **required**. Relative paths must start with \`./\` or \`../\`.
|
|
2827
|
+
|
|
2828
|
+
| Field | Type | Description | Default |
|
|
2829
|
+
|-------|------|-------------|---------|
|
|
2830
|
+
| \`componentDir\` | string | Where individual components are installed | \`./src/components/pdfx\` |
|
|
2831
|
+
| \`blockDir\` | string | Where full document blocks are installed | \`./src/blocks/pdfx\` |
|
|
2832
|
+
| \`registry\` | string (URL) | Registry base URL (must start with http) | \`https://pdfx.akashpise.dev/r\` |
|
|
2833
|
+
| \`theme\` | string | Path to your generated theme file | \`./src/lib/pdfx-theme.ts\` |
|
|
2834
|
+
|
|
2835
|
+
> **Non-interactive init (CI / AI agents):** pass \`--yes\` to accept all defaults:
|
|
2836
|
+
> \`\`\`bash
|
|
2837
|
+
> npx pdfx-cli init --yes
|
|
2838
|
+
> \`\`\`
|
|
2839
|
+
|
|
2840
|
+
## Troubleshooting
|
|
2841
|
+
|
|
2842
|
+
| Problem | Fix |
|
|
2843
|
+
|---------|-----|
|
|
2844
|
+
| TypeScript errors on \`@react-pdf/renderer\` | \`${installCmd(pm, "@react-pdf/types", true)}\` |
|
|
2845
|
+
| "Cannot find module @/components/pdfx/..." | Run \`npx pdfx-cli@latest add <component>\` to install it |
|
|
2846
|
+
| PDF renders blank | Ensure root returns \`<Document><Page>...</Page></Document>\` |
|
|
2847
|
+
| "Invalid hook call" | PDFx components cannot use React hooks — pass data as props |
|
|
2848
|
+
|
|
2849
|
+
---
|
|
2850
|
+
Next: call \`get_audit_checklist\` to verify your setup is correct.
|
|
2851
|
+
`);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// src/mcp/tools/search.ts
|
|
2855
|
+
import dedent6 from "dedent";
|
|
2856
|
+
import { z as z6 } from "zod";
|
|
2857
|
+
var searchRegistrySchema = z6.object({
|
|
2858
|
+
query: z6.string().min(1).describe("Search query \u2014 matched against component name, title, and description"),
|
|
2859
|
+
type: z6.enum(["all", "component", "block"]).optional().default("all").describe("Filter by item type (default: all)"),
|
|
2860
|
+
limit: z6.number().int().positive().max(50).optional().default(20).describe("Maximum number of results to return (default: 20, max: 50)")
|
|
2861
|
+
});
|
|
2862
|
+
async function searchRegistry(args) {
|
|
2863
|
+
const items = await fetchRegistryIndex();
|
|
2864
|
+
const q = args.query.toLowerCase();
|
|
2865
|
+
let pool = items;
|
|
2866
|
+
if (args.type === "component") {
|
|
2867
|
+
pool = items.filter((i) => i.type === "registry:ui");
|
|
2868
|
+
} else if (args.type === "block") {
|
|
2869
|
+
pool = items.filter((i) => i.type === "registry:block");
|
|
2870
|
+
}
|
|
2871
|
+
const scored = pool.map((item) => {
|
|
2872
|
+
const name = item.name.toLowerCase();
|
|
2873
|
+
const title = (item.title ?? "").toLowerCase();
|
|
2874
|
+
const desc = (item.description ?? "").toLowerCase();
|
|
2875
|
+
let score = 0;
|
|
2876
|
+
if (name === q) score = 100;
|
|
2877
|
+
else if (name.startsWith(q)) score = 80;
|
|
2878
|
+
else if (name.includes(q)) score = 60;
|
|
2879
|
+
else if (title.includes(q)) score = 40;
|
|
2880
|
+
else if (desc.includes(q)) score = 20;
|
|
2881
|
+
return { item, score };
|
|
2882
|
+
}).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, args.limit);
|
|
2883
|
+
if (scored.length === 0) {
|
|
2884
|
+
return textResponse(dedent6`
|
|
2885
|
+
# Search: "${args.query}"
|
|
2886
|
+
|
|
2887
|
+
No results found. Try a broader query or browse all items:
|
|
2888
|
+
- \`list_components\` — see all 24 components
|
|
2889
|
+
- \`list_blocks\` — see all 10 blocks
|
|
2890
|
+
`);
|
|
2891
|
+
}
|
|
2892
|
+
const rows = scored.map(({ item }) => {
|
|
2893
|
+
const typeLabel2 = item.type === "registry:ui" ? "component" : "block";
|
|
2894
|
+
const addCmd = item.type === "registry:ui" ? `npx pdfx-cli add ${item.name}` : `npx pdfx-cli block add ${item.name}`;
|
|
2895
|
+
return dedent6`
|
|
2896
|
+
- **${item.name}** _(${typeLabel2})_ — ${item.description ?? "No description"}
|
|
2897
|
+
\`${addCmd}\`
|
|
2898
|
+
`;
|
|
2899
|
+
});
|
|
2900
|
+
const typeLabel = args.type === "all" ? "components + blocks" : args.type === "component" ? "components" : "blocks";
|
|
2901
|
+
return textResponse(dedent6`
|
|
2902
|
+
# Search: "${args.query}" — ${scored.length} result${scored.length === 1 ? "" : "s"} (${typeLabel})
|
|
2903
|
+
|
|
2904
|
+
${rows.join("\n")}
|
|
2905
|
+
`);
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/mcp/tools/theme.ts
|
|
2909
|
+
import dedent7 from "dedent";
|
|
2910
|
+
import { z as z7 } from "zod";
|
|
2911
|
+
var getThemeSchema = z7.object({
|
|
2912
|
+
theme: z7.enum(["professional", "modern", "minimal"]).describe("Theme preset name")
|
|
2913
|
+
});
|
|
2914
|
+
async function getTheme(args) {
|
|
2915
|
+
const preset = themePresets[args.theme];
|
|
2916
|
+
const { colors, typography, spacing, page } = preset;
|
|
2917
|
+
return textResponse(dedent7`
|
|
2918
|
+
# PDFx Theme: ${args.theme}
|
|
2919
|
+
|
|
2920
|
+
## Colors
|
|
2921
|
+
| Token | Value |
|
|
2922
|
+
|-------|-------|
|
|
2923
|
+
| foreground | \`${colors.foreground}\` |
|
|
2924
|
+
| background | \`${colors.background}\` |
|
|
2925
|
+
| primary | \`${colors.primary}\` |
|
|
2926
|
+
| primaryForeground | \`${colors.primaryForeground}\` |
|
|
2927
|
+
| accent | \`${colors.accent}\` |
|
|
2928
|
+
| muted | \`${colors.muted}\` |
|
|
2929
|
+
| mutedForeground | \`${colors.mutedForeground}\` |
|
|
2930
|
+
| border | \`${colors.border}\` |
|
|
2931
|
+
| destructive | \`${colors.destructive}\` |
|
|
2932
|
+
| success | \`${colors.success}\` |
|
|
2933
|
+
| warning | \`${colors.warning}\` |
|
|
2934
|
+
| info | \`${colors.info}\` |
|
|
2935
|
+
|
|
2936
|
+
## Typography
|
|
2937
|
+
|
|
2938
|
+
### Body
|
|
2939
|
+
- Font family: \`${typography.body.fontFamily}\`
|
|
2940
|
+
- Font size: ${typography.body.fontSize}pt
|
|
2941
|
+
- Line height: ${typography.body.lineHeight}
|
|
2942
|
+
|
|
2943
|
+
### Headings
|
|
2944
|
+
- Font family: \`${typography.heading.fontFamily}\`
|
|
2945
|
+
- Font weight: ${typography.heading.fontWeight}
|
|
2946
|
+
- Line height: ${typography.heading.lineHeight}
|
|
2947
|
+
- h1: ${typography.heading.fontSize.h1}pt
|
|
2948
|
+
- h2: ${typography.heading.fontSize.h2}pt
|
|
2949
|
+
- h3: ${typography.heading.fontSize.h3}pt
|
|
2950
|
+
- h4: ${typography.heading.fontSize.h4}pt
|
|
2951
|
+
- h5: ${typography.heading.fontSize.h5}pt
|
|
2952
|
+
- h6: ${typography.heading.fontSize.h6}pt
|
|
2953
|
+
|
|
2954
|
+
## Spacing
|
|
2955
|
+
- Page margins: top=${spacing.page.marginTop}pt · right=${spacing.page.marginRight}pt · bottom=${spacing.page.marginBottom}pt · left=${spacing.page.marginLeft}pt
|
|
2956
|
+
- Section gap: ${spacing.sectionGap}pt
|
|
2957
|
+
- Paragraph gap: ${spacing.paragraphGap}pt
|
|
2958
|
+
- Component gap: ${spacing.componentGap}pt
|
|
2959
|
+
|
|
2960
|
+
## Page
|
|
2961
|
+
- Size: ${page.size}
|
|
2962
|
+
- Orientation: ${page.orientation}
|
|
2963
|
+
|
|
2964
|
+
## Apply This Theme
|
|
2965
|
+
\`\`\`bash
|
|
2966
|
+
npx pdfx-cli theme switch ${args.theme}
|
|
2967
|
+
\`\`\`
|
|
2968
|
+
|
|
2969
|
+
## Usage in Components
|
|
2970
|
+
\`\`\`tsx
|
|
2971
|
+
// Access theme values in a PDFx component
|
|
2972
|
+
import type { PdfxTheme } from '@pdfx/shared';
|
|
2973
|
+
|
|
2974
|
+
interface Props {
|
|
2975
|
+
theme: PdfxTheme;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
export function MyComponent({ theme }: Props) {
|
|
2979
|
+
return (
|
|
2980
|
+
<View style={{ backgroundColor: theme.colors.background }}>
|
|
2981
|
+
<Text style={{ color: theme.colors.foreground, fontSize: theme.typography.body.fontSize }}>
|
|
2982
|
+
Content
|
|
2983
|
+
</Text>
|
|
2984
|
+
</View>
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
\`\`\`
|
|
2988
|
+
`);
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// src/mcp/index.ts
|
|
2992
|
+
var server = new Server(
|
|
2993
|
+
{ name: "pdfx", version: "1.0.0" },
|
|
2994
|
+
{ capabilities: { tools: {} } }
|
|
2995
|
+
);
|
|
2996
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2997
|
+
tools: [
|
|
2998
|
+
{
|
|
2999
|
+
name: "list_components",
|
|
3000
|
+
description: "List all available PDFx PDF components with names and descriptions. Call this first to discover what components exist before adding any.",
|
|
3001
|
+
inputSchema: zodToJsonSchema(listComponentsSchema)
|
|
3002
|
+
},
|
|
3003
|
+
{
|
|
3004
|
+
name: "get_component",
|
|
3005
|
+
description: "Get the full source code, files, and dependencies for a specific PDFx component. Use this to understand the API and props before using it in generated code.",
|
|
3006
|
+
inputSchema: zodToJsonSchema(getComponentSchema)
|
|
3007
|
+
},
|
|
3008
|
+
{
|
|
3009
|
+
name: "list_blocks",
|
|
3010
|
+
description: "List all PDFx pre-built document blocks (complete invoice and report layouts ready to customize).",
|
|
3011
|
+
inputSchema: zodToJsonSchema(listBlocksSchema)
|
|
3012
|
+
},
|
|
3013
|
+
{
|
|
3014
|
+
name: "get_block",
|
|
3015
|
+
description: "Get the full source code for a PDFx document block. Returns the complete layout code ready to customize for your use case.",
|
|
3016
|
+
inputSchema: zodToJsonSchema(getBlockSchema)
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
name: "search_registry",
|
|
3020
|
+
description: "Search PDFx components and blocks by name or description. Use this when you know what you need but not the exact item name.",
|
|
3021
|
+
inputSchema: zodToJsonSchema(searchRegistrySchema)
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
name: "get_theme",
|
|
3025
|
+
description: "Get the full design token values for a PDFx theme preset (professional, modern, or minimal). Use this to understand colors, typography, and spacing before customizing documents.",
|
|
3026
|
+
inputSchema: zodToJsonSchema(getThemeSchema)
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
name: "get_installation",
|
|
3030
|
+
description: "Get step-by-step PDFx setup instructions for a specific framework and package manager. Use this when setting up PDFx in a new project.",
|
|
3031
|
+
inputSchema: zodToJsonSchema(getInstallationSchema)
|
|
3032
|
+
},
|
|
3033
|
+
{
|
|
3034
|
+
name: "get_add_command",
|
|
3035
|
+
description: "Get the exact CLI command string to add specific PDFx components or blocks to a project.",
|
|
3036
|
+
inputSchema: zodToJsonSchema(getAddCommandSchema)
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
name: "get_audit_checklist",
|
|
3040
|
+
description: "Get a post-generation checklist to verify PDFx is set up correctly. Call this after adding components or generating PDF document code.",
|
|
3041
|
+
inputSchema: zodToJsonSchema(z8.object({}))
|
|
3042
|
+
}
|
|
3043
|
+
]
|
|
3044
|
+
}));
|
|
3045
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3046
|
+
const args = request.params.arguments ?? {};
|
|
3047
|
+
try {
|
|
3048
|
+
switch (request.params.name) {
|
|
3049
|
+
case "list_components":
|
|
3050
|
+
return await listComponents();
|
|
3051
|
+
case "get_component":
|
|
3052
|
+
return await getComponent(getComponentSchema.parse(args));
|
|
3053
|
+
case "list_blocks":
|
|
3054
|
+
return await listBlocks();
|
|
3055
|
+
case "get_block":
|
|
3056
|
+
return await getBlock(getBlockSchema.parse(args));
|
|
3057
|
+
case "search_registry":
|
|
3058
|
+
return await searchRegistry(searchRegistrySchema.parse(args));
|
|
3059
|
+
case "get_theme":
|
|
3060
|
+
return await getTheme(getThemeSchema.parse(args));
|
|
3061
|
+
case "get_installation":
|
|
3062
|
+
return await getInstallation(getInstallationSchema.parse(args));
|
|
3063
|
+
case "get_add_command":
|
|
3064
|
+
return await getAddCommand(getAddCommandSchema.parse(args));
|
|
3065
|
+
case "get_audit_checklist":
|
|
3066
|
+
return await getAuditChecklist();
|
|
3067
|
+
default:
|
|
3068
|
+
return errorResponse(new Error(`Unknown tool: ${request.params.name}`));
|
|
3069
|
+
}
|
|
3070
|
+
} catch (error) {
|
|
3071
|
+
return errorResponse(error);
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
// src/commands/mcp.ts
|
|
3076
|
+
var PDFX_CLI_PACKAGE = "pdfx-cli";
|
|
3077
|
+
var PDFX_MCP_VERSION = "latest";
|
|
3078
|
+
var CLIENTS = [
|
|
3079
|
+
{
|
|
3080
|
+
name: "claude",
|
|
3081
|
+
label: "Claude Code",
|
|
3082
|
+
configPath: ".mcp.json",
|
|
3083
|
+
config: {
|
|
3084
|
+
mcpServers: {
|
|
3085
|
+
pdfx: {
|
|
3086
|
+
command: "npx",
|
|
3087
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
},
|
|
3092
|
+
{
|
|
3093
|
+
name: "cursor",
|
|
3094
|
+
label: "Cursor",
|
|
3095
|
+
configPath: ".cursor/mcp.json",
|
|
3096
|
+
config: {
|
|
3097
|
+
mcpServers: {
|
|
3098
|
+
pdfx: {
|
|
3099
|
+
command: "npx",
|
|
3100
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
},
|
|
3105
|
+
{
|
|
3106
|
+
name: "vscode",
|
|
3107
|
+
label: "VS Code",
|
|
3108
|
+
configPath: ".vscode/mcp.json",
|
|
3109
|
+
config: {
|
|
3110
|
+
servers: {
|
|
3111
|
+
pdfx: {
|
|
3112
|
+
type: "stdio",
|
|
3113
|
+
command: "npx",
|
|
3114
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
},
|
|
3119
|
+
{
|
|
3120
|
+
name: "windsurf",
|
|
3121
|
+
label: "Windsurf",
|
|
3122
|
+
configPath: "mcp_config.json",
|
|
3123
|
+
config: {
|
|
3124
|
+
mcpServers: {
|
|
3125
|
+
pdfx: {
|
|
3126
|
+
command: "npx",
|
|
3127
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
},
|
|
3132
|
+
{
|
|
3133
|
+
name: "qoder",
|
|
3134
|
+
label: "Qoder",
|
|
3135
|
+
configPath: ".qoder/mcp.json",
|
|
3136
|
+
config: {
|
|
3137
|
+
mcpServers: {
|
|
3138
|
+
pdfx: {
|
|
3139
|
+
command: "npx",
|
|
3140
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
name: "opencode",
|
|
3147
|
+
label: "opencode",
|
|
3148
|
+
configPath: "opencode.json",
|
|
3149
|
+
config: {
|
|
3150
|
+
$schema: "https://opencode.ai/config.json",
|
|
3151
|
+
mcp: {
|
|
3152
|
+
pdfx: {
|
|
3153
|
+
type: "local",
|
|
3154
|
+
command: ["npx", "-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
name: "antigravity",
|
|
3161
|
+
label: "Antigravity",
|
|
3162
|
+
configPath: ".antigravity/mcp.json",
|
|
3163
|
+
config: {
|
|
3164
|
+
mcpServers: {
|
|
3165
|
+
pdfx: {
|
|
3166
|
+
command: "npx",
|
|
3167
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
name: "other",
|
|
3174
|
+
label: "Generic (any MCP client)",
|
|
3175
|
+
configPath: "pdfx-mcp.json",
|
|
3176
|
+
config: {
|
|
3177
|
+
mcpServers: {
|
|
3178
|
+
pdfx: {
|
|
3179
|
+
command: "npx",
|
|
3180
|
+
args: ["-y", `${PDFX_CLI_PACKAGE}@${PDFX_MCP_VERSION}`, "mcp"]
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
];
|
|
3186
|
+
function mergeDeep(target, source) {
|
|
3187
|
+
const result = { ...target };
|
|
3188
|
+
for (const [key, value] of Object.entries(source)) {
|
|
3189
|
+
const existing = result[key];
|
|
3190
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
3191
|
+
result[key] = mergeDeep(
|
|
3192
|
+
existing,
|
|
3193
|
+
value
|
|
3194
|
+
);
|
|
3195
|
+
} else {
|
|
3196
|
+
result[key] = value;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
return result;
|
|
3200
|
+
}
|
|
3201
|
+
async function readJsonConfig(filePath) {
|
|
3202
|
+
try {
|
|
3203
|
+
const content = await readFile(filePath, "utf-8");
|
|
3204
|
+
return JSON.parse(content);
|
|
3205
|
+
} catch {
|
|
3206
|
+
return {};
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
function isPdfxAlreadyConfigured(config) {
|
|
3210
|
+
const withMcpServers = config;
|
|
3211
|
+
const withServers = config;
|
|
3212
|
+
const withMcp = config;
|
|
3213
|
+
return withMcpServers.mcpServers?.pdfx !== void 0 || withServers.servers?.pdfx !== void 0 || withMcp.mcp?.pdfx !== void 0;
|
|
3214
|
+
}
|
|
3215
|
+
async function initMcpConfig(opts) {
|
|
3216
|
+
let clientName = opts.client;
|
|
3217
|
+
if (!clientName) {
|
|
3218
|
+
const response = await prompts5({
|
|
3219
|
+
type: "select",
|
|
3220
|
+
name: "client",
|
|
3221
|
+
message: "Which AI editor are you configuring?",
|
|
3222
|
+
choices: CLIENTS.map((c) => ({ title: c.label, value: c.name }))
|
|
3223
|
+
});
|
|
3224
|
+
if (!response.client) {
|
|
3225
|
+
process.exit(0);
|
|
3226
|
+
}
|
|
3227
|
+
clientName = response.client;
|
|
3228
|
+
}
|
|
3229
|
+
const client = CLIENTS.find((c) => c.name === clientName);
|
|
3230
|
+
if (!client) {
|
|
3231
|
+
process.stderr.write(`Unknown client: "${clientName}"
|
|
3232
|
+
`);
|
|
3233
|
+
process.stderr.write(`Valid options: ${CLIENTS.map((c) => c.name).join(", ")}
|
|
3234
|
+
`);
|
|
3235
|
+
process.exit(1);
|
|
3236
|
+
}
|
|
3237
|
+
const configPath = path11.resolve(process.cwd(), client.configPath);
|
|
3238
|
+
const configDir = path11.dirname(configPath);
|
|
3239
|
+
const existing = await readJsonConfig(configPath);
|
|
3240
|
+
if (isPdfxAlreadyConfigured(existing)) {
|
|
3241
|
+
const { overwrite } = await prompts5({
|
|
3242
|
+
type: "confirm",
|
|
3243
|
+
name: "overwrite",
|
|
3244
|
+
message: `PDFx MCP is already configured in ${client.configPath}. Overwrite?`,
|
|
3245
|
+
initial: false
|
|
3246
|
+
});
|
|
3247
|
+
if (!overwrite) {
|
|
3248
|
+
process.stdout.write("\nSkipped \u2014 existing configuration kept.\n");
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (!existsSync(configDir)) {
|
|
3253
|
+
await mkdir(configDir, { recursive: true });
|
|
3254
|
+
}
|
|
3255
|
+
const merged = mergeDeep(existing, client.config);
|
|
3256
|
+
await writeFile2(configPath, `${JSON.stringify(merged, null, 2)}
|
|
3257
|
+
`, "utf-8");
|
|
3258
|
+
process.stdout.write(`
|
|
3259
|
+
\u2713 Wrote MCP configuration to ${client.configPath}
|
|
3260
|
+
`);
|
|
3261
|
+
process.stdout.write(`
|
|
3262
|
+
Restart ${client.label} to activate the PDFx MCP server.
|
|
3263
|
+
`);
|
|
3264
|
+
if (client.name === "claude") {
|
|
3265
|
+
process.stdout.write("\nAlternatively, run:\n claude mcp add pdfx -- npx -y pdfx-cli mcp\n");
|
|
3266
|
+
}
|
|
3267
|
+
if (client.name === "opencode") {
|
|
3268
|
+
process.stdout.write("\nVerify: run `opencode mcp list` \u2014 pdfx should appear.\n");
|
|
3269
|
+
process.stdout.write(
|
|
3270
|
+
"Tip: move opencode.json to ~/.config/opencode/opencode.json for global access.\n"
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
if (client.name === "antigravity") {
|
|
3274
|
+
process.stdout.write("\nVerify: open Antigravity \u2192 MCP panel \u2192 pdfx should appear.\n");
|
|
3275
|
+
}
|
|
3276
|
+
if (client.name === "other") {
|
|
3277
|
+
process.stdout.write("\nThis file contains the PDFx MCP server configuration.\n");
|
|
3278
|
+
process.stdout.write(
|
|
3279
|
+
"Copy the pdfx entry into your editor's native MCP config file, or reference this file directly.\n"
|
|
3280
|
+
);
|
|
3281
|
+
}
|
|
3282
|
+
process.stdout.write("\n");
|
|
3283
|
+
}
|
|
3284
|
+
var mcpCommand = new Command().name("mcp").description("Start the PDFx MCP server for AI editor integration").action(async () => {
|
|
3285
|
+
const transport = new StdioServerTransport();
|
|
3286
|
+
await server.connect(transport);
|
|
3287
|
+
});
|
|
3288
|
+
mcpCommand.command("init").description("Add PDFx MCP server config to your AI editor").option(
|
|
3289
|
+
"--client <name>",
|
|
3290
|
+
`AI editor to configure. One of: ${CLIENTS.map((c) => c.name).join(", ")}`
|
|
3291
|
+
).action(initMcpConfig);
|
|
3292
|
+
|
|
3293
|
+
// src/commands/skills.ts
|
|
3294
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
3295
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
3296
|
+
import path12 from "path";
|
|
3297
|
+
import chalk8 from "chalk";
|
|
3298
|
+
import { Command as Command2 } from "commander";
|
|
3299
|
+
import prompts6 from "prompts";
|
|
3300
|
+
|
|
3301
|
+
// src/skills-content.ts
|
|
3302
|
+
var PDFX_SKILLS_CONTENT = `# PDFx \u2014 AI Context Guide
|
|
3303
|
+
# Version: 1.0 | Updated: 2026 | License: MIT
|
|
3304
|
+
# Or run: npx pdfx-cli@latest skills init (handles editor-specific paths & frontmatter)
|
|
3305
|
+
|
|
3306
|
+
## What is PDFx?
|
|
3307
|
+
|
|
3308
|
+
PDFx is an open-source, shadcn/ui-style PDF component library for React. It is built on
|
|
3309
|
+
@react-pdf/renderer and provides 24 type-safe components, 10 pre-built document blocks,
|
|
3310
|
+
3 themes, and a CLI. Components are copied into your project (not installed as npm imports
|
|
3311
|
+
that expose a public API).
|
|
3312
|
+
|
|
3313
|
+
Key facts:
|
|
3314
|
+
- Package: pdfx-cli (the CLI that installs components)
|
|
3315
|
+
- Registry: https://pdfx.akashpise.dev/r/
|
|
3316
|
+
- Runtime: Works in browser AND Node.js (Next.js App Router, Express, etc.)
|
|
3317
|
+
- React version: 16.8+ (hooks required)
|
|
3318
|
+
- Peer dep: @react-pdf/renderer ^3.x
|
|
3319
|
+
|
|
3320
|
+
---
|
|
3321
|
+
|
|
3322
|
+
## Installation (one-time project setup)
|
|
3323
|
+
|
|
3324
|
+
\`\`\`bash
|
|
3325
|
+
# 1. Initialize PDFx \u2014 creates src/lib/pdfx-theme.ts and installs @pdfx/shared
|
|
3326
|
+
npx pdfx-cli@latest init
|
|
3327
|
+
|
|
3328
|
+
# 2. Add components you need
|
|
3329
|
+
npx pdfx-cli@latest add heading text table
|
|
3330
|
+
|
|
3331
|
+
# 3. Add a pre-built block
|
|
3332
|
+
npx pdfx-cli@latest block add invoice-modern
|
|
3333
|
+
\`\`\`
|
|
3334
|
+
|
|
3335
|
+
The init command adds a theme file at src/lib/pdfx-theme.ts. All components read from this file.
|
|
3336
|
+
|
|
3337
|
+
---
|
|
3338
|
+
|
|
3339
|
+
## How components work
|
|
3340
|
+
|
|
3341
|
+
PDFx components are React components that render @react-pdf/renderer primitives (View, Text,
|
|
3342
|
+
Page, Document, etc.). They CANNOT render HTML or DOM elements \u2014 they only work inside a
|
|
3343
|
+
<Document> from @react-pdf/renderer.
|
|
3344
|
+
|
|
3345
|
+
Usage pattern:
|
|
3346
|
+
\`\`\`tsx
|
|
3347
|
+
import { Document, Page } from '@react-pdf/renderer';
|
|
3348
|
+
import { Heading } from '@/components/pdfx/heading/pdfx-heading';
|
|
3349
|
+
import { Text } from '@/components/pdfx/text/pdfx-text';
|
|
3350
|
+
import { Table } from '@/components/pdfx/table/pdfx-table';
|
|
3351
|
+
|
|
3352
|
+
export function MyDocument() {
|
|
3353
|
+
return (
|
|
3354
|
+
<Document>
|
|
3355
|
+
<Page size="A4" style={{ padding: 40 }}>
|
|
3356
|
+
<Heading level={1}>Invoice #001</Heading>
|
|
3357
|
+
<Text>Thank you for your business.</Text>
|
|
3358
|
+
<Table
|
|
3359
|
+
headers={['Item', 'Qty', 'Price']}
|
|
3360
|
+
rows={[['Design work', '1', '$4,800']]}
|
|
3361
|
+
/>
|
|
3362
|
+
</Page>
|
|
3363
|
+
</Document>
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
\`\`\`
|
|
3367
|
+
|
|
3368
|
+
Rendering to PDF:
|
|
3369
|
+
\`\`\`tsx
|
|
3370
|
+
// Browser: live preview
|
|
3371
|
+
import { PDFViewer } from '@react-pdf/renderer';
|
|
3372
|
+
<PDFViewer><MyDocument /></PDFViewer>
|
|
3373
|
+
|
|
3374
|
+
// Browser: download button
|
|
3375
|
+
import { PDFDownloadLink } from '@react-pdf/renderer';
|
|
3376
|
+
<PDFDownloadLink document={<MyDocument />} fileName="output.pdf">Download</PDFDownloadLink>
|
|
3377
|
+
|
|
3378
|
+
// Server (Next.js App Router):
|
|
3379
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
3380
|
+
export async function GET() {
|
|
3381
|
+
const buf = await renderToBuffer(<MyDocument />);
|
|
3382
|
+
return new Response(buf, { headers: { 'Content-Type': 'application/pdf' } });
|
|
3383
|
+
}
|
|
3384
|
+
\`\`\`
|
|
3385
|
+
|
|
3386
|
+
---
|
|
3387
|
+
|
|
3388
|
+
## All 24 Components \u2014 Props Reference
|
|
3389
|
+
|
|
3390
|
+
CRITICAL: These are the EXACT props. Do not invent additional props.
|
|
3391
|
+
|
|
3392
|
+
### Heading
|
|
3393
|
+
\`\`\`tsx
|
|
3394
|
+
import { Heading } from '@/components/pdfx/heading/pdfx-heading';
|
|
3395
|
+
<Heading
|
|
3396
|
+
level={1} // 1 | 2 | 3 | 4 | 5 | 6 \u2014 default: 1
|
|
3397
|
+
align="left" // 'left' | 'center' | 'right' \u2014 default: 'left'
|
|
3398
|
+
weight="bold" // 'normal' | 'bold' \u2014 default: 'bold'
|
|
3399
|
+
tracking="normal" // 'tight' | 'normal' | 'wide' \u2014 default: 'normal'
|
|
3400
|
+
color="#000" // string \u2014 default: theme.colors.foreground
|
|
3401
|
+
gutterBottom // boolean \u2014 adds bottom margin
|
|
3402
|
+
>
|
|
3403
|
+
My Heading
|
|
3404
|
+
</Heading>
|
|
3405
|
+
\`\`\`
|
|
3406
|
+
|
|
3407
|
+
### Text
|
|
3408
|
+
\`\`\`tsx
|
|
3409
|
+
import { Text } from '@/components/pdfx/text/pdfx-text';
|
|
3410
|
+
<Text
|
|
3411
|
+
size="md" // 'xs' | 'sm' | 'md' | 'lg' | 'xl' \u2014 default: 'md'
|
|
3412
|
+
weight="normal" // 'normal' | 'medium' | 'semibold' | 'bold' \u2014 default: 'normal'
|
|
3413
|
+
color="#000" // string
|
|
3414
|
+
align="left" // 'left' | 'center' | 'right' | 'justify'
|
|
3415
|
+
italic // boolean
|
|
3416
|
+
muted // boolean \u2014 applies theme.colors.mutedForeground
|
|
3417
|
+
gutterBottom // boolean
|
|
3418
|
+
>
|
|
3419
|
+
Paragraph text here.
|
|
3420
|
+
</Text>
|
|
3421
|
+
\`\`\`
|
|
3422
|
+
|
|
3423
|
+
### Link
|
|
3424
|
+
\`\`\`tsx
|
|
3425
|
+
import { Link } from '@/components/pdfx/link/pdfx-link';
|
|
3426
|
+
<Link
|
|
3427
|
+
href="https://example.com" // string \u2014 required
|
|
3428
|
+
size="md" // same as Text size
|
|
3429
|
+
color="#0000ff" // default: theme.colors.accent
|
|
3430
|
+
>
|
|
3431
|
+
Click here
|
|
3432
|
+
</Link>
|
|
3433
|
+
\`\`\`
|
|
3434
|
+
|
|
3435
|
+
### Divider
|
|
3436
|
+
\`\`\`tsx
|
|
3437
|
+
import { Divider } from '@/components/pdfx/divider/pdfx-divider';
|
|
3438
|
+
<Divider
|
|
3439
|
+
thickness={1} // number in pt \u2014 default: 1
|
|
3440
|
+
color="#e4e4e7" // string \u2014 default: theme.colors.border
|
|
3441
|
+
spacing="md" // 'sm' | 'md' | 'lg' \u2014 vertical margin
|
|
3442
|
+
style="solid" // 'solid' | 'dashed' | 'dotted'
|
|
3443
|
+
/>
|
|
3444
|
+
\`\`\`
|
|
3445
|
+
|
|
3446
|
+
### PageBreak
|
|
3447
|
+
\`\`\`tsx
|
|
3448
|
+
import { PageBreak } from '@/components/pdfx/page-break/pdfx-page-break';
|
|
3449
|
+
<PageBreak /> // No props. Forces a new page.
|
|
3450
|
+
\`\`\`
|
|
3451
|
+
|
|
3452
|
+
### Stack
|
|
3453
|
+
\`\`\`tsx
|
|
3454
|
+
import { Stack } from '@/components/pdfx/stack/pdfx-stack';
|
|
3455
|
+
<Stack
|
|
3456
|
+
direction="column" // 'row' | 'column' \u2014 default: 'column'
|
|
3457
|
+
gap={8} // number in pt \u2014 default: 0
|
|
3458
|
+
align="flex-start" // flexbox align-items
|
|
3459
|
+
justify="flex-start"// flexbox justify-content
|
|
3460
|
+
wrap // boolean \u2014 flex-wrap
|
|
3461
|
+
>
|
|
3462
|
+
{children}
|
|
3463
|
+
</Stack>
|
|
3464
|
+
\`\`\`
|
|
3465
|
+
|
|
3466
|
+
### Section
|
|
3467
|
+
\`\`\`tsx
|
|
3468
|
+
import { Section } from '@/components/pdfx/section/pdfx-section';
|
|
3469
|
+
<Section
|
|
3470
|
+
title="Section Title" // string \u2014 optional
|
|
3471
|
+
titleLevel={2} // 1\u20136 \u2014 default: 2
|
|
3472
|
+
padding={16} // number | {top,right,bottom,left} \u2014 default: 0
|
|
3473
|
+
bordered // boolean \u2014 adds border around section
|
|
3474
|
+
background="#f9f9f9" // string \u2014 background color
|
|
3475
|
+
>
|
|
3476
|
+
{children}
|
|
3477
|
+
</Section>
|
|
3478
|
+
\`\`\`
|
|
3479
|
+
|
|
3480
|
+
### Table
|
|
3481
|
+
\`\`\`tsx
|
|
3482
|
+
import { Table } from '@/components/pdfx/table/pdfx-table';
|
|
3483
|
+
<Table
|
|
3484
|
+
headers={['Column A', 'Column B', 'Column C']} // string[] \u2014 required
|
|
3485
|
+
rows={[['R1C1', 'R1C2', 'R1C3']]} // string[][] \u2014 required
|
|
3486
|
+
striped // boolean \u2014 alternating row colors
|
|
3487
|
+
bordered // boolean \u2014 cell borders
|
|
3488
|
+
compact // boolean \u2014 smaller padding
|
|
3489
|
+
headerBg="#18181b" // string \u2014 header background
|
|
3490
|
+
headerColor="#fff" // string \u2014 header text color
|
|
3491
|
+
columnWidths={[2, 1, 1]} // number[] \u2014 flex ratios
|
|
3492
|
+
caption="Table 1" // string \u2014 caption below table
|
|
3493
|
+
/>
|
|
3494
|
+
\`\`\`
|
|
3495
|
+
|
|
3496
|
+
### DataTable
|
|
3497
|
+
\`\`\`tsx
|
|
3498
|
+
import { DataTable } from '@/components/pdfx/data-table/pdfx-data-table';
|
|
3499
|
+
<DataTable
|
|
3500
|
+
columns={[
|
|
3501
|
+
{ key: 'name', header: 'Name', width: 2 },
|
|
3502
|
+
{ key: 'amount', header: 'Amount', width: 1, align: 'right' },
|
|
3503
|
+
]}
|
|
3504
|
+
data={[{ name: 'Item A', amount: '$100' }]}
|
|
3505
|
+
striped
|
|
3506
|
+
bordered
|
|
3507
|
+
compact
|
|
3508
|
+
/>
|
|
3509
|
+
\`\`\`
|
|
3510
|
+
|
|
3511
|
+
### List
|
|
3512
|
+
\`\`\`tsx
|
|
3513
|
+
import { List } from '@/components/pdfx/list/pdfx-list';
|
|
3514
|
+
<List
|
|
3515
|
+
items={['First item', 'Second item', 'Third item']} // string[] \u2014 required
|
|
3516
|
+
ordered // boolean \u2014 numbered list (default: bulleted)
|
|
3517
|
+
bullet="\u2022" // string \u2014 custom bullet character
|
|
3518
|
+
indent={16} // number \u2014 left indent in pt
|
|
3519
|
+
spacing="sm" // 'sm' | 'md' | 'lg' \u2014 gap between items
|
|
3520
|
+
/>
|
|
3521
|
+
\`\`\`
|
|
3522
|
+
|
|
3523
|
+
### Card
|
|
3524
|
+
\`\`\`tsx
|
|
3525
|
+
import { Card } from '@/components/pdfx/card/pdfx-card';
|
|
3526
|
+
<Card
|
|
3527
|
+
padding={16} // number \u2014 default: 16
|
|
3528
|
+
bordered // boolean \u2014 default: true
|
|
3529
|
+
shadow // boolean
|
|
3530
|
+
background="#fff" // string
|
|
3531
|
+
borderColor="#e4e4e7" // string
|
|
3532
|
+
borderRadius={4} // number in pt
|
|
3533
|
+
>
|
|
3534
|
+
{children}
|
|
3535
|
+
</Card>
|
|
3536
|
+
\`\`\`
|
|
3537
|
+
|
|
3538
|
+
### Form (read-only form fields for PDFs)
|
|
3539
|
+
\`\`\`tsx
|
|
3540
|
+
import { Form } from '@/components/pdfx/form/pdfx-form';
|
|
3541
|
+
<Form
|
|
3542
|
+
fields={[
|
|
3543
|
+
{ label: 'Full Name', value: 'John Doe' },
|
|
3544
|
+
{ label: 'Email', value: 'john@example.com' },
|
|
3545
|
+
{ label: 'Notes', value: 'Some notes here', multiline: true },
|
|
3546
|
+
]}
|
|
3547
|
+
columns={2} // 1 | 2 \u2014 default: 1
|
|
3548
|
+
bordered // boolean
|
|
3549
|
+
/>
|
|
3550
|
+
\`\`\`
|
|
3551
|
+
|
|
3552
|
+
### Signature
|
|
3553
|
+
\`\`\`tsx
|
|
3554
|
+
import { PdfSignatureBlock } from '@/components/pdfx/signature/pdfx-signature';
|
|
3555
|
+
<PdfSignatureBlock
|
|
3556
|
+
name="Sarah Chen" // string \u2014 printed name below line
|
|
3557
|
+
title="Engineering Lead" // string \u2014 title/role below name
|
|
3558
|
+
date="2024-12-12" // string \u2014 formatted date
|
|
3559
|
+
lineWidth={120} // number \u2014 signature line width in pt
|
|
3560
|
+
showDate // boolean \u2014 default: true
|
|
3561
|
+
/>
|
|
3562
|
+
\`\`\`
|
|
3563
|
+
|
|
3564
|
+
### PageHeader
|
|
3565
|
+
\`\`\`tsx
|
|
3566
|
+
import { PageHeader } from '@/components/pdfx/page-header/pdfx-page-header';
|
|
3567
|
+
<PageHeader
|
|
3568
|
+
title="Document Title"
|
|
3569
|
+
subtitle="Subtitle or tagline"
|
|
3570
|
+
logo={{ src: 'https://...', width: 60, height: 30 }}
|
|
3571
|
+
bordered // boolean \u2014 adds bottom border
|
|
3572
|
+
/>
|
|
3573
|
+
\`\`\`
|
|
3574
|
+
|
|
3575
|
+
### PageFooter
|
|
3576
|
+
\`\`\`tsx
|
|
3577
|
+
import { PageFooter } from '@/components/pdfx/page-footer/pdfx-page-footer';
|
|
3578
|
+
<PageFooter
|
|
3579
|
+
left="\xA9 2024 Acme Corp"
|
|
3580
|
+
center="Confidential"
|
|
3581
|
+
right="Page 1 of 1"
|
|
3582
|
+
bordered // boolean \u2014 top border
|
|
3583
|
+
/>
|
|
3584
|
+
\`\`\`
|
|
3585
|
+
|
|
3586
|
+
### Badge
|
|
3587
|
+
\`\`\`tsx
|
|
3588
|
+
import { Badge } from '@/components/pdfx/badge/pdfx-badge';
|
|
3589
|
+
// Use label prop OR children (string only \u2014 not a React node)
|
|
3590
|
+
<Badge
|
|
3591
|
+
label="PAID" // string \u2014 preferred API
|
|
3592
|
+
variant="success" // 'default' | 'success' | 'warning' | 'error' | 'info' | 'outline'
|
|
3593
|
+
size="md" // 'sm' | 'md' | 'lg'
|
|
3594
|
+
/>
|
|
3595
|
+
// OR
|
|
3596
|
+
<Badge variant="success">PAID</Badge>
|
|
3597
|
+
\`\`\`
|
|
3598
|
+
|
|
3599
|
+
### KeyValue
|
|
3600
|
+
\`\`\`tsx
|
|
3601
|
+
import { KeyValue } from '@/components/pdfx/key-value/pdfx-key-value';
|
|
3602
|
+
<KeyValue
|
|
3603
|
+
items={[
|
|
3604
|
+
{ label: 'Invoice #', value: 'INV-001' },
|
|
3605
|
+
{ label: 'Due Date', value: 'Jan 31, 2025' },
|
|
3606
|
+
]}
|
|
3607
|
+
columns={2} // 1 | 2 | 3 \u2014 default: 1
|
|
3608
|
+
labelWidth={80} // number in pt
|
|
3609
|
+
colon // boolean \u2014 adds colon after label
|
|
3610
|
+
/>
|
|
3611
|
+
\`\`\`
|
|
3612
|
+
|
|
3613
|
+
### KeepTogether
|
|
3614
|
+
\`\`\`tsx
|
|
3615
|
+
import { KeepTogether } from '@/components/pdfx/keep-together/pdfx-keep-together';
|
|
3616
|
+
// Prevents page breaks inside its children
|
|
3617
|
+
<KeepTogether>
|
|
3618
|
+
<Heading level={3}>Section that must not split</Heading>
|
|
3619
|
+
<Table headers={[...]} rows={[...]} />
|
|
3620
|
+
</KeepTogether>
|
|
3621
|
+
\`\`\`
|
|
3622
|
+
|
|
3623
|
+
### PdfImage
|
|
3624
|
+
\`\`\`tsx
|
|
3625
|
+
import { PdfImage } from '@/components/pdfx/pdf-image/pdfx-pdf-image';
|
|
3626
|
+
<PdfImage
|
|
3627
|
+
src="https://example.com/image.png" // string | base64 \u2014 required
|
|
3628
|
+
width={200} // number in pt
|
|
3629
|
+
height={150} // optional \u2014 maintains aspect ratio if omitted
|
|
3630
|
+
alt="Description" // string \u2014 for accessibility
|
|
3631
|
+
objectFit="cover" // 'cover' | 'contain' | 'fill'
|
|
3632
|
+
borderRadius={4} // number
|
|
3633
|
+
/>
|
|
3634
|
+
\`\`\`
|
|
3635
|
+
|
|
3636
|
+
### Graph
|
|
3637
|
+
\`\`\`tsx
|
|
3638
|
+
import { Graph } from '@/components/pdfx/graph/pdfx-graph';
|
|
3639
|
+
<Graph
|
|
3640
|
+
type="bar" // 'bar' | 'line' | 'pie' | 'donut'
|
|
3641
|
+
data={[
|
|
3642
|
+
{ label: 'Q1', value: 4200 },
|
|
3643
|
+
{ label: 'Q2', value: 6100 },
|
|
3644
|
+
]}
|
|
3645
|
+
width={400} // number in pt
|
|
3646
|
+
height={200} // number in pt
|
|
3647
|
+
title="Revenue" // string \u2014 optional
|
|
3648
|
+
showValues // boolean \u2014 show value labels
|
|
3649
|
+
showLegend // boolean
|
|
3650
|
+
colors={['#18181b', '#71717a']} // string[] \u2014 bar/slice colors
|
|
3651
|
+
/>
|
|
3652
|
+
\`\`\`
|
|
3653
|
+
|
|
3654
|
+
### PageNumber
|
|
3655
|
+
\`\`\`tsx
|
|
3656
|
+
import { PageNumber } from '@/components/pdfx/page-number/pdfx-page-number';
|
|
3657
|
+
<PageNumber
|
|
3658
|
+
format="Page {current} of {total}" // string template
|
|
3659
|
+
size="sm"
|
|
3660
|
+
align="right"
|
|
3661
|
+
/>
|
|
3662
|
+
\`\`\`
|
|
3663
|
+
|
|
3664
|
+
### Watermark
|
|
3665
|
+
\`\`\`tsx
|
|
3666
|
+
import { PdfWatermark } from '@/components/pdfx/watermark/pdfx-watermark';
|
|
3667
|
+
<PdfWatermark
|
|
3668
|
+
text="CONFIDENTIAL" // string \u2014 required
|
|
3669
|
+
opacity={0.08} // number 0\u20131 \u2014 default: 0.08
|
|
3670
|
+
angle={-35} // number in degrees \u2014 default: -35
|
|
3671
|
+
fontSize={72} // number \u2014 default: 72
|
|
3672
|
+
color="#000000" // string
|
|
3673
|
+
/>
|
|
3674
|
+
\`\`\`
|
|
3675
|
+
|
|
3676
|
+
### QRCode
|
|
3677
|
+
\`\`\`tsx
|
|
3678
|
+
import { PdfQRCode } from '@/components/pdfx/qrcode/pdfx-qrcode';
|
|
3679
|
+
<PdfQRCode
|
|
3680
|
+
value="https://example.com" // string \u2014 required
|
|
3681
|
+
size={80} // number in pt \u2014 default: 80
|
|
3682
|
+
errorCorrectionLevel="M" // 'L' | 'M' | 'Q' | 'H'
|
|
3683
|
+
/>
|
|
3684
|
+
\`\`\`
|
|
3685
|
+
|
|
3686
|
+
### Alert
|
|
3687
|
+
\`\`\`tsx
|
|
3688
|
+
import { Alert } from '@/components/pdfx/alert/pdfx-alert';
|
|
3689
|
+
<Alert
|
|
3690
|
+
variant="info" // 'info' | 'success' | 'warning' | 'error'
|
|
3691
|
+
title="Note" // string \u2014 optional bold title
|
|
3692
|
+
>
|
|
3693
|
+
This is an informational note.
|
|
3694
|
+
</Alert>
|
|
3695
|
+
\`\`\`
|
|
3696
|
+
|
|
3697
|
+
---
|
|
3698
|
+
|
|
3699
|
+
## Pre-built Blocks
|
|
3700
|
+
|
|
3701
|
+
Blocks are complete document templates. Add them with:
|
|
3702
|
+
\`\`\`bash
|
|
3703
|
+
npx pdfx-cli@latest block add <block-name>
|
|
3704
|
+
\`\`\`
|
|
3705
|
+
|
|
3706
|
+
### Invoice Blocks
|
|
3707
|
+
- invoice-modern \u2014 Clean two-column layout with totals table
|
|
3708
|
+
- invoice-minimal \u2014 Stripped-down, typography-focused
|
|
3709
|
+
- invoice-corporate \u2014 Header with logo area, full itemization
|
|
3710
|
+
- invoice-creative \u2014 Accent colors, bold layout
|
|
3711
|
+
|
|
3712
|
+
### Report Blocks
|
|
3713
|
+
- report-executive \u2014 KPI cards + summary table, 2-page
|
|
3714
|
+
- report-annual \u2014 Multi-section with charts and appendix
|
|
3715
|
+
- report-financial \u2014 P&L / balance sheet focus
|
|
3716
|
+
- report-marketing \u2014 Campaign metrics with graphs
|
|
3717
|
+
- report-technical \u2014 Code-friendly, monospace sections
|
|
3718
|
+
|
|
3719
|
+
### Contract Block
|
|
3720
|
+
- contract-standard \u2014 Signature page, numbered clauses, party info
|
|
3721
|
+
|
|
3722
|
+
Blocks are added as full React components in your project. Customize all content props.
|
|
3723
|
+
|
|
3724
|
+
---
|
|
3725
|
+
|
|
3726
|
+
## Theming
|
|
3727
|
+
|
|
3728
|
+
### The theme file
|
|
3729
|
+
|
|
3730
|
+
After \`npx pdfx-cli@latest init\`, a file is created at src/lib/pdfx-theme.ts.
|
|
3731
|
+
Every PDFx component reads from this file \u2014 change a token once, all components update.
|
|
3732
|
+
|
|
3733
|
+
\`\`\`typescript
|
|
3734
|
+
export const theme: PdfxTheme = {
|
|
3735
|
+
name: 'my-brand',
|
|
3736
|
+
colors: {
|
|
3737
|
+
primary: '#2563eb',
|
|
3738
|
+
accent: '#7c3aed',
|
|
3739
|
+
foreground: '#1a1a1a',
|
|
3740
|
+
background: '#ffffff',
|
|
3741
|
+
muted: '#f4f4f5',
|
|
3742
|
+
mutedForeground: '#71717a',
|
|
3743
|
+
primaryForeground: '#ffffff',
|
|
3744
|
+
border: '#e4e4e7',
|
|
3745
|
+
destructive: '#dc2626',
|
|
3746
|
+
success: '#16a34a',
|
|
3747
|
+
warning: '#d97706',
|
|
3748
|
+
info: '#0ea5e9',
|
|
3749
|
+
},
|
|
3750
|
+
typography: {
|
|
3751
|
+
heading: { fontFamily: 'Helvetica-Bold', fontWeight: 700, lineHeight: 1.2,
|
|
3752
|
+
fontSize: { h1: 36, h2: 28, h3: 22, h4: 18, h5: 15, h6: 12 } },
|
|
3753
|
+
body: { fontFamily: 'Helvetica', fontSize: 11, lineHeight: 1.5 },
|
|
3754
|
+
},
|
|
3755
|
+
// primitives, spacing, page \u2014 all required (scaffolded by init)
|
|
3756
|
+
};
|
|
3757
|
+
\`\`\`
|
|
3758
|
+
|
|
3759
|
+
### Theme presets
|
|
3760
|
+
\`\`\`bash
|
|
3761
|
+
npx pdfx-cli@latest theme init # scaffold blank theme
|
|
3762
|
+
npx pdfx-cli@latest theme switch modern # switch preset: professional | modern | minimal
|
|
3763
|
+
npx pdfx-cli@latest theme validate # validate your theme file
|
|
3764
|
+
\`\`\`
|
|
3765
|
+
|
|
3766
|
+
---
|
|
3767
|
+
|
|
3768
|
+
## CLI Reference
|
|
3769
|
+
|
|
3770
|
+
\`\`\`bash
|
|
3771
|
+
# Setup
|
|
3772
|
+
npx pdfx-cli@latest init # Initialize PDFx in project
|
|
3773
|
+
npx pdfx-cli@latest add <component> # Add a component
|
|
3774
|
+
npx pdfx-cli@latest add <comp1> <comp2> # Add multiple
|
|
3775
|
+
npx pdfx-cli@latest block add <block> # Add a block
|
|
3776
|
+
|
|
3777
|
+
# Theme
|
|
3778
|
+
npx pdfx-cli@latest theme init # Create theme file
|
|
3779
|
+
npx pdfx-cli@latest theme switch professional # Switch preset
|
|
3780
|
+
npx pdfx-cli@latest theme validate # Validate theme
|
|
3781
|
+
|
|
3782
|
+
# MCP (AI editor integration)
|
|
3783
|
+
npx pdfx-cli@latest mcp # Start MCP server
|
|
3784
|
+
npx pdfx-cli@latest mcp init # Configure editor (interactive)
|
|
3785
|
+
npx pdfx-cli@latest mcp init --client claude # Claude Code (.mcp.json)
|
|
3786
|
+
npx pdfx-cli@latest mcp init --client cursor # Cursor (.cursor/mcp.json)
|
|
3787
|
+
npx pdfx-cli@latest mcp init --client vscode # VS Code (.vscode/mcp.json)
|
|
3788
|
+
npx pdfx-cli@latest mcp init --client windsurf # Windsurf (mcp_config.json)
|
|
3789
|
+
npx pdfx-cli@latest mcp init --client qoder # Qoder (.qoder/mcp.json)
|
|
3790
|
+
npx pdfx-cli@latest mcp init --client opencode # opencode (opencode.json)
|
|
3791
|
+
npx pdfx-cli@latest mcp init --client antigravity # Antigravity (.antigravity/mcp.json)
|
|
3792
|
+
|
|
3793
|
+
# Skills file (AI context document)
|
|
3794
|
+
npx pdfx-cli@latest skills init # Write skills file (interactive)
|
|
3795
|
+
npx pdfx-cli@latest skills init --platform claude # CLAUDE.md
|
|
3796
|
+
npx pdfx-cli@latest skills init --platform cursor # .cursor/rules/pdfx.mdc
|
|
3797
|
+
npx pdfx-cli@latest skills init --platform vscode # .github/copilot-instructions.md
|
|
3798
|
+
npx pdfx-cli@latest skills init --platform windsurf # .windsurf/rules/pdfx.md
|
|
3799
|
+
npx pdfx-cli@latest skills init --platform opencode # AGENTS.md
|
|
3800
|
+
npx pdfx-cli@latest skills init --platform antigravity # .antigravity/context.md
|
|
3801
|
+
\`\`\`
|
|
3802
|
+
|
|
3803
|
+
---
|
|
3804
|
+
|
|
3805
|
+
## Common patterns
|
|
3806
|
+
|
|
3807
|
+
### Full invoice from scratch
|
|
3808
|
+
\`\`\`tsx
|
|
3809
|
+
import { Document, Page } from '@react-pdf/renderer';
|
|
3810
|
+
import { Heading } from '@/components/pdfx/heading/pdfx-heading';
|
|
3811
|
+
import { KeyValue } from '@/components/pdfx/key-value/pdfx-key-value';
|
|
3812
|
+
import { Table } from '@/components/pdfx/table/pdfx-table';
|
|
3813
|
+
import { Divider } from '@/components/pdfx/divider/pdfx-divider';
|
|
3814
|
+
import { Badge } from '@/components/pdfx/badge/pdfx-badge';
|
|
3815
|
+
import { PageFooter } from '@/components/pdfx/page-footer/pdfx-page-footer';
|
|
3816
|
+
|
|
3817
|
+
export function InvoiceDoc() {
|
|
3818
|
+
return (
|
|
3819
|
+
<Document>
|
|
3820
|
+
<Page size="A4" style={{ padding: 48, fontFamily: 'Helvetica' }}>
|
|
3821
|
+
<Heading level={1}>Invoice #INV-001</Heading>
|
|
3822
|
+
<KeyValue
|
|
3823
|
+
items={[
|
|
3824
|
+
{ label: 'Date', value: 'Jan 1, 2025' },
|
|
3825
|
+
{ label: 'Due', value: 'Jan 31, 2025' },
|
|
3826
|
+
]}
|
|
3827
|
+
columns={2}
|
|
3828
|
+
/>
|
|
3829
|
+
<Divider spacing="md" />
|
|
3830
|
+
<Table
|
|
3831
|
+
headers={['Description', 'Qty', 'Total']}
|
|
3832
|
+
rows={[
|
|
3833
|
+
['Design System', '1', '$4,800'],
|
|
3834
|
+
['Development', '2', '$9,600'],
|
|
3835
|
+
]}
|
|
3836
|
+
striped
|
|
3837
|
+
bordered
|
|
3838
|
+
columnWidths={[3, 1, 1]}
|
|
3839
|
+
/>
|
|
3840
|
+
<Badge label="PAID" variant="success" />
|
|
3841
|
+
<PageFooter left="Acme Corp" right="Page 1 of 1" bordered />
|
|
3842
|
+
</Page>
|
|
3843
|
+
</Document>
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3846
|
+
\`\`\`
|
|
3847
|
+
|
|
3848
|
+
### Preventing page splits
|
|
3849
|
+
\`\`\`tsx
|
|
3850
|
+
// Wrap anything that must stay together across page boundaries
|
|
3851
|
+
<KeepTogether>
|
|
3852
|
+
<Heading level={3}>Q3 Summary</Heading>
|
|
3853
|
+
<Table headers={['Metric', 'Value']} rows={data} />
|
|
3854
|
+
</KeepTogether>
|
|
3855
|
+
\`\`\`
|
|
3856
|
+
|
|
3857
|
+
### Server-side generation (Next.js)
|
|
3858
|
+
\`\`\`typescript
|
|
3859
|
+
// app/api/invoice/route.ts
|
|
3860
|
+
import { renderToBuffer } from '@react-pdf/renderer';
|
|
3861
|
+
import { InvoiceDoc } from '@/components/pdf/invoice';
|
|
3862
|
+
|
|
3863
|
+
export async function GET(req: Request) {
|
|
3864
|
+
const { searchParams } = new URL(req.url);
|
|
3865
|
+
const id = searchParams.get('id');
|
|
3866
|
+
const data = await fetchInvoice(id);
|
|
3867
|
+
const buf = await renderToBuffer(<InvoiceDoc data={data} />);
|
|
3868
|
+
return new Response(buf, {
|
|
3869
|
+
headers: {
|
|
3870
|
+
'Content-Type': 'application/pdf',
|
|
3871
|
+
'Content-Disposition': \`inline; filename="invoice-\${id}.pdf"\`,
|
|
3872
|
+
},
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
\`\`\`
|
|
3876
|
+
|
|
3877
|
+
---
|
|
3878
|
+
|
|
3879
|
+
## react-pdf layout constraints (CRITICAL)
|
|
3880
|
+
|
|
3881
|
+
@react-pdf/renderer enforces strict separation between layout containers and text:
|
|
3882
|
+
|
|
3883
|
+
- **View** is a layout container (like a div). It can contain other Views and Text nodes.
|
|
3884
|
+
- **Text** is a text container. It can contain strings or nested Text nodes.
|
|
3885
|
+
- **NEVER mix View and inline text in a flex row.** This causes irrecoverable layout failures.
|
|
3886
|
+
|
|
3887
|
+
\`\`\`tsx
|
|
3888
|
+
// \u2717 WRONG \u2014 mixing View and text siblings in a flex row
|
|
3889
|
+
<View style={{ flexDirection: 'row' }}>
|
|
3890
|
+
<View style={{ width: 100 }}>...</View>
|
|
3891
|
+
Some text here {/* \u2190 this text sibling crashes the layout */}
|
|
3892
|
+
</View>
|
|
3893
|
+
|
|
3894
|
+
// \u2713 CORRECT \u2014 wrap all text siblings in <Text>
|
|
3895
|
+
<View style={{ flexDirection: 'row' }}>
|
|
3896
|
+
<View style={{ width: 100 }}>...</View>
|
|
3897
|
+
<Text>Some text here</Text>
|
|
3898
|
+
</View>
|
|
3899
|
+
\`\`\`
|
|
3900
|
+
|
|
3901
|
+
---
|
|
3902
|
+
|
|
3903
|
+
## Anti-patterns to avoid
|
|
3904
|
+
|
|
3905
|
+
- DO NOT use HTML elements inside PDFx components (no <div>, <p>, <span>)
|
|
3906
|
+
- DO NOT import from @react-pdf/renderer inside PDFx component files \u2014 they already wrap it
|
|
3907
|
+
- DO NOT use CSS classes or Tailwind inside PDF components \u2014 use style props or theme tokens
|
|
3908
|
+
- DO NOT use window, document, or browser APIs in server-rendered PDF routes
|
|
3909
|
+
- DO NOT install components with npm \u2014 always use the CLI: npx pdfx-cli@latest add <name>
|
|
3910
|
+
- DO NOT place raw text siblings next to View elements in a flex row (react-pdf constraint)
|
|
3911
|
+
- DO NOT pass React nodes (JSX) as Badge children \u2014 only plain strings are supported
|
|
3912
|
+
|
|
3913
|
+
---
|
|
3914
|
+
|
|
3915
|
+
## MCP Server (for AI editors)
|
|
3916
|
+
|
|
3917
|
+
The PDFx MCP server gives AI editors live access to the entire registry:
|
|
3918
|
+
\`\`\`bash
|
|
3919
|
+
npx pdfx-cli@latest mcp init # interactive setup for your editor
|
|
3920
|
+
\`\`\`
|
|
3921
|
+
Supported: Claude Code, Cursor, VS Code, Windsurf, Qoder, opencode, Antigravity
|
|
3922
|
+
|
|
3923
|
+
Tools: list_components, get_component, list_blocks, get_block, search_registry,
|
|
3924
|
+
get_theme, get_installation, get_add_command, get_audit_checklist
|
|
3925
|
+
|
|
3926
|
+
---
|
|
3927
|
+
# End of PDFx AI Context Guide
|
|
3928
|
+
`;
|
|
3929
|
+
|
|
3930
|
+
// src/commands/skills.ts
|
|
3931
|
+
var PLATFORMS = [
|
|
3932
|
+
{
|
|
3933
|
+
name: "claude",
|
|
3934
|
+
label: "Claude Code",
|
|
3935
|
+
file: "CLAUDE.md",
|
|
3936
|
+
hint: "Project-level CLAUDE.md \u2014 read automatically by Claude Code",
|
|
3937
|
+
verifyStep: "Claude Code picks up CLAUDE.md automatically on next session."
|
|
3938
|
+
},
|
|
3939
|
+
{
|
|
3940
|
+
// .cursor/rules/*.mdc with alwaysApply: true — Cursor's current rules format (not .cursorrules).
|
|
3941
|
+
name: "cursor",
|
|
3942
|
+
label: "Cursor",
|
|
3943
|
+
file: ".cursor/rules/pdfx.mdc",
|
|
3944
|
+
hint: ".cursor/rules/pdfx.mdc \u2014 Cursor rules file with alwaysApply frontmatter",
|
|
3945
|
+
verifyStep: "Cursor reads .cursor/rules/*.mdc automatically. Restart Cursor to be sure.",
|
|
3946
|
+
frontmatter: "---\ndescription: PDFx PDF component library AI context\nglobs: \nalwaysApply: true\n---\n\n"
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
name: "vscode",
|
|
3950
|
+
label: "VS Code (Copilot)",
|
|
3951
|
+
file: ".github/copilot-instructions.md",
|
|
3952
|
+
hint: ".github/copilot-instructions.md \u2014 read by GitHub Copilot Chat",
|
|
3953
|
+
verifyStep: "GitHub Copilot reads copilot-instructions.md from .github/. Commit the file so teammates get it too."
|
|
3954
|
+
},
|
|
3955
|
+
{
|
|
3956
|
+
name: "windsurf",
|
|
3957
|
+
label: "Windsurf",
|
|
3958
|
+
file: ".windsurfrules",
|
|
3959
|
+
hint: ".windsurfrules \u2014 applied to all Cascade AI interactions",
|
|
3960
|
+
verifyStep: "Windsurf applies .windsurfrules automatically."
|
|
3961
|
+
},
|
|
3962
|
+
{
|
|
3963
|
+
name: "qoder",
|
|
3964
|
+
label: "Qoder",
|
|
3965
|
+
file: ".qoder/rules.md",
|
|
3966
|
+
hint: ".qoder/rules.md \u2014 Qoder project-level AI context",
|
|
3967
|
+
verifyStep: "Restart Qoder and check that the AI context is loaded from .qoder/rules.md."
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
name: "opencode",
|
|
3971
|
+
label: "opencode",
|
|
3972
|
+
file: "AGENTS.md",
|
|
3973
|
+
hint: "AGENTS.md \u2014 opencode project context file",
|
|
3974
|
+
verifyStep: "opencode reads AGENTS.md from the project root. Start a new session to verify."
|
|
3975
|
+
},
|
|
3976
|
+
{
|
|
3977
|
+
name: "antigravity",
|
|
3978
|
+
label: "Antigravity",
|
|
3979
|
+
file: ".antigravity/context.md",
|
|
3980
|
+
hint: ".antigravity/context.md \u2014 Antigravity project context",
|
|
3981
|
+
verifyStep: "Verify the context path against your Antigravity version. Restart to activate."
|
|
3982
|
+
},
|
|
3983
|
+
{
|
|
3984
|
+
name: "other",
|
|
3985
|
+
label: "Generic (any AI tool)",
|
|
3986
|
+
file: "pdfx-context.md",
|
|
3987
|
+
hint: "pdfx-context.md \u2014 reference this from your editor's rules file",
|
|
3988
|
+
verifyStep: "Reference pdfx-context.md from your editor's rules file (e.g. .cursorrules, CLAUDE.md)."
|
|
3989
|
+
}
|
|
3990
|
+
];
|
|
3991
|
+
var PDFX_MARKER = "# PDFx \u2014 AI Context Guide";
|
|
3992
|
+
function fileHasPdfxContent(filePath) {
|
|
3993
|
+
try {
|
|
3994
|
+
return readFileSync(filePath, "utf-8").includes(PDFX_MARKER);
|
|
3995
|
+
} catch {
|
|
3996
|
+
return false;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
async function skillsInit(opts) {
|
|
4000
|
+
let platformName = opts.platform;
|
|
4001
|
+
if (!platformName) {
|
|
4002
|
+
const response = await prompts6({
|
|
4003
|
+
type: "select",
|
|
4004
|
+
name: "platform",
|
|
4005
|
+
message: "Which AI editor are you targeting?",
|
|
4006
|
+
choices: PLATFORMS.map((p) => ({
|
|
4007
|
+
title: p.label,
|
|
4008
|
+
value: p.name,
|
|
4009
|
+
description: p.hint
|
|
4010
|
+
}))
|
|
4011
|
+
});
|
|
4012
|
+
if (!response.platform) {
|
|
4013
|
+
process.exit(0);
|
|
4014
|
+
}
|
|
4015
|
+
platformName = response.platform;
|
|
4016
|
+
}
|
|
4017
|
+
const platform = PLATFORMS.find((p) => p.name === platformName);
|
|
4018
|
+
if (!platform) {
|
|
4019
|
+
process.stderr.write(chalk8.red(`\u2716 Unknown platform: "${platformName}"
|
|
4020
|
+
`));
|
|
4021
|
+
process.stderr.write(
|
|
4022
|
+
chalk8.dim(` Valid options: ${PLATFORMS.map((p) => p.name).join(", ")}
|
|
4023
|
+
`)
|
|
4024
|
+
);
|
|
4025
|
+
process.exit(1);
|
|
4026
|
+
}
|
|
4027
|
+
const filePath = path12.resolve(process.cwd(), platform.file);
|
|
4028
|
+
const fileDir = path12.dirname(filePath);
|
|
4029
|
+
const relativeFile = platform.file;
|
|
4030
|
+
const alreadyExists = existsSync2(filePath);
|
|
4031
|
+
const hasPdfxContent = alreadyExists && fileHasPdfxContent(filePath);
|
|
4032
|
+
let shouldAppend = opts.append ?? false;
|
|
4033
|
+
if (alreadyExists) {
|
|
4034
|
+
if (shouldAppend || opts.yes) {
|
|
4035
|
+
} else if (hasPdfxContent) {
|
|
4036
|
+
const { action: action2 } = await prompts6({
|
|
4037
|
+
type: "select",
|
|
4038
|
+
name: "action",
|
|
4039
|
+
message: `${relativeFile} already contains PDFx context. What would you like to do?`,
|
|
4040
|
+
choices: [
|
|
4041
|
+
{ title: "Overwrite (replace PDFx section with latest content)", value: "overwrite" },
|
|
4042
|
+
{ title: "Skip", value: "skip" }
|
|
4043
|
+
]
|
|
4044
|
+
});
|
|
4045
|
+
if (!action2 || action2 === "skip") {
|
|
4046
|
+
process.stdout.write(chalk8.dim("\nSkipped \u2014 existing file kept.\n\n"));
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
} else {
|
|
4050
|
+
const { action: action2 } = await prompts6({
|
|
4051
|
+
type: "select",
|
|
4052
|
+
name: "action",
|
|
4053
|
+
message: `${relativeFile} already exists. What would you like to do?`,
|
|
4054
|
+
choices: [
|
|
4055
|
+
{
|
|
4056
|
+
title: "Append (add PDFx context at the end, keep existing content)",
|
|
4057
|
+
value: "append"
|
|
4058
|
+
},
|
|
4059
|
+
{ title: "Overwrite (replace entire file with PDFx context)", value: "overwrite" },
|
|
4060
|
+
{ title: "Skip", value: "skip" }
|
|
4061
|
+
]
|
|
4062
|
+
});
|
|
4063
|
+
if (!action2 || action2 === "skip") {
|
|
4064
|
+
process.stdout.write(chalk8.dim("\nSkipped \u2014 existing file kept.\n\n"));
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
if (action2 === "append") {
|
|
4068
|
+
shouldAppend = true;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
if (!existsSync2(fileDir)) {
|
|
4073
|
+
await mkdir2(fileDir, { recursive: true });
|
|
4074
|
+
}
|
|
4075
|
+
const appendContent = `
|
|
4076
|
+
|
|
4077
|
+
---
|
|
4078
|
+
|
|
4079
|
+
${PDFX_SKILLS_CONTENT}`;
|
|
4080
|
+
const content = shouldAppend ? appendContent : PDFX_SKILLS_CONTENT;
|
|
4081
|
+
if (shouldAppend && alreadyExists) {
|
|
4082
|
+
const existing = readFileSync(filePath, "utf-8");
|
|
4083
|
+
await writeFile3(filePath, `${existing.trimEnd()}${content}
|
|
4084
|
+
`, "utf-8");
|
|
4085
|
+
} else {
|
|
4086
|
+
await writeFile3(filePath, `${content}
|
|
4087
|
+
`, "utf-8");
|
|
4088
|
+
}
|
|
4089
|
+
const action = shouldAppend && alreadyExists ? "Appended PDFx context to" : "Wrote";
|
|
4090
|
+
process.stdout.write(`
|
|
4091
|
+
${chalk8.green("\u2713")} ${action} ${chalk8.cyan(relativeFile)}
|
|
4092
|
+
`);
|
|
4093
|
+
process.stdout.write(`
|
|
4094
|
+
${chalk8.dim(platform.verifyStep)}
|
|
4095
|
+
|
|
4096
|
+
`);
|
|
4097
|
+
if (platform.name === "claude") {
|
|
4098
|
+
process.stdout.write(
|
|
4099
|
+
chalk8.dim(
|
|
4100
|
+
"Tip: if you also use the MCP server, CLAUDE.md + MCP gives the AI\nthe best possible PDFx knowledge \u2014 static props reference + live registry.\n\n"
|
|
4101
|
+
)
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
var skillsCommand = new Command2().name("skills").description("Manage the PDFx AI context (skills) file for your editor");
|
|
4106
|
+
skillsCommand.command("init").description("Write the PDFx skills file to your AI editor's context file").option(
|
|
4107
|
+
"-p, --platform <name>",
|
|
4108
|
+
`Target AI editor. One of: ${PLATFORMS.map((p) => p.name).join(", ")}`
|
|
4109
|
+
).option("-y, --yes", "Overwrite existing file without prompting").option("-a, --append", "Append PDFx context to an existing file instead of overwriting").action(skillsInit);
|
|
4110
|
+
skillsCommand.command("list").description("List all supported AI editor platforms").action(() => {
|
|
4111
|
+
process.stdout.write("\n");
|
|
4112
|
+
process.stdout.write(chalk8.bold(" Supported platforms\n\n"));
|
|
4113
|
+
for (const p of PLATFORMS) {
|
|
4114
|
+
process.stdout.write(` ${chalk8.cyan(p.name.padEnd(12))} ${chalk8.dim("\u2192")} ${p.file}
|
|
4115
|
+
`);
|
|
4116
|
+
}
|
|
4117
|
+
process.stdout.write("\n");
|
|
4118
|
+
process.stdout.write(
|
|
4119
|
+
chalk8.dim(" Usage: npx pdfx-cli@latest skills init --platform <name>\n\n")
|
|
4120
|
+
);
|
|
4121
|
+
});
|
|
4122
|
+
|
|
4123
|
+
// src/commands/theme.ts
|
|
4124
|
+
import fs8 from "fs";
|
|
4125
|
+
import path13 from "path";
|
|
4126
|
+
import chalk9 from "chalk";
|
|
4127
|
+
import ora7 from "ora";
|
|
4128
|
+
import prompts7 from "prompts";
|
|
4129
|
+
import ts from "typescript";
|
|
4130
|
+
async function themeInit() {
|
|
4131
|
+
const configPath = path13.join(process.cwd(), "pdfx.json");
|
|
4132
|
+
if (!checkFileExists(configPath)) {
|
|
4133
|
+
console.error(chalk9.red("\nError: pdfx.json not found"));
|
|
4134
|
+
console.log(chalk9.yellow("\n PDFx is not initialized in this project.\n"));
|
|
4135
|
+
console.log(chalk9.cyan(" Run: pdfx init"));
|
|
4136
|
+
console.log(chalk9.dim(" This will set up your project configuration and theme.\n"));
|
|
4137
|
+
process.exit(1);
|
|
4138
|
+
}
|
|
4139
|
+
console.log(chalk9.bold.cyan("\n PDFx Theme Setup\n"));
|
|
4140
|
+
const answers = await prompts7(
|
|
4141
|
+
[
|
|
4142
|
+
{
|
|
4143
|
+
type: "select",
|
|
4144
|
+
name: "preset",
|
|
4145
|
+
message: "Choose a theme preset:",
|
|
4146
|
+
choices: [
|
|
4147
|
+
{
|
|
4148
|
+
title: "Professional",
|
|
4149
|
+
description: "Serif headings, navy colors, generous margins",
|
|
4150
|
+
value: "professional"
|
|
4151
|
+
},
|
|
4152
|
+
{
|
|
4153
|
+
title: "Modern",
|
|
4154
|
+
description: "Sans-serif, vibrant purple, tight spacing",
|
|
4155
|
+
value: "modern"
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
4158
|
+
title: "Minimal",
|
|
4159
|
+
description: "Monospace headings, stark black, maximum whitespace",
|
|
4160
|
+
value: "minimal"
|
|
4161
|
+
}
|
|
4162
|
+
],
|
|
4163
|
+
initial: 0
|
|
4164
|
+
},
|
|
4165
|
+
{
|
|
4166
|
+
type: "text",
|
|
4167
|
+
name: "themePath",
|
|
4168
|
+
message: "Where should we create the theme file?",
|
|
4169
|
+
initial: DEFAULTS.THEME_FILE,
|
|
4170
|
+
format: normalizeThemePath,
|
|
4171
|
+
validate: validateThemePath
|
|
4172
|
+
}
|
|
4173
|
+
],
|
|
4174
|
+
{
|
|
4175
|
+
onCancel: () => {
|
|
4176
|
+
console.log(chalk9.yellow("\nTheme setup cancelled."));
|
|
4177
|
+
process.exit(0);
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
);
|
|
4181
|
+
if (!answers.preset || !answers.themePath) {
|
|
4182
|
+
console.error(chalk9.red("Missing required fields."));
|
|
4183
|
+
process.exit(1);
|
|
4184
|
+
}
|
|
4185
|
+
const presetName = answers.preset;
|
|
4186
|
+
const themePath = answers.themePath;
|
|
4187
|
+
const preset = themePresets[presetName];
|
|
4188
|
+
const spinner = ora7(`Scaffolding ${presetName} theme...`).start();
|
|
4189
|
+
try {
|
|
4190
|
+
const absThemePath = path13.resolve(process.cwd(), themePath);
|
|
4191
|
+
writeFile(absThemePath, generateThemeFile(preset));
|
|
4192
|
+
const contextPath = path13.join(path13.dirname(absThemePath), "pdfx-theme-context.tsx");
|
|
4193
|
+
writeFile(contextPath, generateThemeContextFile());
|
|
4194
|
+
spinner.succeed(`Created ${themePath} with ${presetName} theme`);
|
|
4195
|
+
if (checkFileExists(configPath)) {
|
|
4196
|
+
try {
|
|
4197
|
+
const rawConfig = readJsonFile(configPath);
|
|
4198
|
+
const result = configSchema.safeParse(rawConfig);
|
|
4199
|
+
if (result.success) {
|
|
4200
|
+
const updatedConfig = { ...result.data, theme: themePath };
|
|
4201
|
+
writeFile(configPath, JSON.stringify(updatedConfig, null, 2));
|
|
4202
|
+
console.log(chalk9.green(" Updated pdfx.json with theme path"));
|
|
4203
|
+
}
|
|
4204
|
+
} catch {
|
|
4205
|
+
console.log(chalk9.yellow(' Could not update pdfx.json \u2014 add "theme" field manually'));
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
console.log(chalk9.dim(`
|
|
4209
|
+
Edit ${themePath} to customize your theme.
|
|
4210
|
+
`));
|
|
4211
|
+
} catch (error) {
|
|
4212
|
+
spinner.fail("Failed to create theme file");
|
|
4213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4214
|
+
console.error(chalk9.dim(` ${message}`));
|
|
4215
|
+
process.exit(1);
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
async function themeSwitch(presetName) {
|
|
4219
|
+
const resolvedPreset = presetName === "default" ? "professional" : presetName;
|
|
4220
|
+
const validPresets = Object.keys(themePresets);
|
|
4221
|
+
if (!validPresets.includes(resolvedPreset)) {
|
|
4222
|
+
console.error(chalk9.red(`\u2716 Invalid theme preset: "${presetName}"`));
|
|
4223
|
+
console.log(chalk9.dim(` Available presets: ${validPresets.join(", ")}, default
|
|
4224
|
+
`));
|
|
4225
|
+
console.log(chalk9.dim(" Usage: pdfx theme switch <preset>"));
|
|
4226
|
+
process.exit(1);
|
|
4227
|
+
}
|
|
4228
|
+
const validatedPreset = resolvedPreset;
|
|
4229
|
+
const configPath = path13.join(process.cwd(), "pdfx.json");
|
|
4230
|
+
if (!checkFileExists(configPath)) {
|
|
4231
|
+
console.error(chalk9.red('No pdfx.json found. Run "npx pdfx-cli@latest init" first.'));
|
|
4232
|
+
process.exit(1);
|
|
4233
|
+
}
|
|
4234
|
+
const rawConfig = readJsonFile(configPath);
|
|
4235
|
+
const result = configSchema.safeParse(rawConfig);
|
|
4236
|
+
if (!result.success) {
|
|
4237
|
+
console.error(chalk9.red("Invalid pdfx.json configuration."));
|
|
4238
|
+
process.exit(1);
|
|
4239
|
+
}
|
|
4240
|
+
const config = result.data;
|
|
4241
|
+
if (!config.theme) {
|
|
4242
|
+
console.error(
|
|
4243
|
+
chalk9.red(
|
|
4244
|
+
'No theme path in pdfx.json. Run "npx pdfx-cli@latest theme init" to set up theming.'
|
|
4245
|
+
)
|
|
4246
|
+
);
|
|
4247
|
+
process.exit(1);
|
|
4248
|
+
}
|
|
4249
|
+
const answer = await prompts7({
|
|
4250
|
+
type: "confirm",
|
|
4251
|
+
name: "confirm",
|
|
4252
|
+
message: `This will overwrite ${config.theme} with the ${validatedPreset} preset. Continue?`,
|
|
4253
|
+
initial: false
|
|
4254
|
+
});
|
|
4255
|
+
if (!answer.confirm) {
|
|
4256
|
+
console.log(chalk9.yellow("Cancelled."));
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
const spinner = ora7(`Switching to ${validatedPreset} theme...`).start();
|
|
4260
|
+
try {
|
|
4261
|
+
const preset = themePresets[validatedPreset];
|
|
4262
|
+
const absThemePath = path13.resolve(process.cwd(), config.theme);
|
|
4263
|
+
writeFile(absThemePath, generateThemeFile(preset));
|
|
4264
|
+
const contextPath = path13.join(path13.dirname(absThemePath), "pdfx-theme-context.tsx");
|
|
4265
|
+
if (!checkFileExists(contextPath)) {
|
|
4266
|
+
writeFile(contextPath, generateThemeContextFile());
|
|
4267
|
+
}
|
|
4268
|
+
spinner.succeed(`Switched to ${validatedPreset} theme`);
|
|
4269
|
+
} catch (error) {
|
|
4270
|
+
spinner.fail("Failed to switch theme");
|
|
4271
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4272
|
+
console.error(chalk9.dim(` ${message}`));
|
|
4273
|
+
process.exit(1);
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
function toPlainValue(node) {
|
|
4277
|
+
if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isParenthesizedExpression(node)) {
|
|
4278
|
+
return toPlainValue(node.expression);
|
|
4279
|
+
}
|
|
4280
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
4281
|
+
return node.text;
|
|
4282
|
+
}
|
|
4283
|
+
if (ts.isNumericLiteral(node)) {
|
|
4284
|
+
return Number(node.text);
|
|
4285
|
+
}
|
|
4286
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
4287
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
4288
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
4289
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken) {
|
|
4290
|
+
const n = toPlainValue(node.operand);
|
|
4291
|
+
return typeof n === "number" ? -n : void 0;
|
|
4292
|
+
}
|
|
4293
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
4294
|
+
return node.elements.map(
|
|
4295
|
+
(el) => ts.isSpreadElement(el) ? void 0 : toPlainValue(el)
|
|
4296
|
+
);
|
|
4297
|
+
}
|
|
4298
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
4299
|
+
const out = {};
|
|
4300
|
+
for (const prop of node.properties) {
|
|
4301
|
+
if (!ts.isPropertyAssignment(prop)) return void 0;
|
|
4302
|
+
if (ts.isComputedPropertyName(prop.name)) return void 0;
|
|
4303
|
+
const key = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) || ts.isNumericLiteral(prop.name) ? prop.name.text : void 0;
|
|
4304
|
+
if (!key) return void 0;
|
|
4305
|
+
const value = toPlainValue(prop.initializer);
|
|
4306
|
+
if (value === void 0) return void 0;
|
|
4307
|
+
out[key] = value;
|
|
4308
|
+
}
|
|
4309
|
+
return out;
|
|
4310
|
+
}
|
|
4311
|
+
return void 0;
|
|
4312
|
+
}
|
|
4313
|
+
function parseThemeObject(themePath) {
|
|
4314
|
+
const content = fs8.readFileSync(themePath, "utf-8");
|
|
4315
|
+
const sourceFile = ts.createSourceFile(
|
|
4316
|
+
themePath,
|
|
4317
|
+
content,
|
|
4318
|
+
ts.ScriptTarget.Latest,
|
|
4319
|
+
true,
|
|
4320
|
+
ts.ScriptKind.TS
|
|
4321
|
+
);
|
|
4322
|
+
for (const stmt of sourceFile.statements) {
|
|
4323
|
+
if (!ts.isVariableStatement(stmt)) continue;
|
|
4324
|
+
const isExported = stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
4325
|
+
if (!isExported) continue;
|
|
4326
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
4327
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== "theme" || !decl.initializer) continue;
|
|
4328
|
+
const parsed = toPlainValue(decl.initializer);
|
|
4329
|
+
if (parsed === void 0) {
|
|
4330
|
+
throw new Error(
|
|
4331
|
+
"Could not statically parse exported theme object. Keep `export const theme = { ... }` as a plain object literal."
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
return parsed;
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
throw new Error("No exported `theme` object found.");
|
|
4338
|
+
}
|
|
4339
|
+
async function themeValidate() {
|
|
4340
|
+
const configPath = path13.join(process.cwd(), "pdfx.json");
|
|
4341
|
+
if (!checkFileExists(configPath)) {
|
|
4342
|
+
console.error(chalk9.red('No pdfx.json found. Run "npx pdfx-cli@latest init" first.'));
|
|
4343
|
+
process.exit(1);
|
|
4344
|
+
}
|
|
4345
|
+
const rawConfig = readJsonFile(configPath);
|
|
4346
|
+
const configResult = configSchema.safeParse(rawConfig);
|
|
4347
|
+
if (!configResult.success) {
|
|
4348
|
+
console.error(chalk9.red("Invalid pdfx.json configuration."));
|
|
4349
|
+
process.exit(1);
|
|
4350
|
+
}
|
|
4351
|
+
if (!configResult.data.theme) {
|
|
4352
|
+
console.error(
|
|
4353
|
+
chalk9.red(
|
|
4354
|
+
'No theme path in pdfx.json. Run "npx pdfx-cli@latest theme init" to set up theming.'
|
|
4355
|
+
)
|
|
4356
|
+
);
|
|
4357
|
+
process.exit(1);
|
|
4358
|
+
}
|
|
4359
|
+
const absThemePath = path13.resolve(process.cwd(), configResult.data.theme);
|
|
4360
|
+
if (!checkFileExists(absThemePath)) {
|
|
4361
|
+
console.error(chalk9.red(`Theme file not found: ${configResult.data.theme}`));
|
|
4362
|
+
process.exit(1);
|
|
4363
|
+
}
|
|
4364
|
+
const spinner = ora7("Validating theme file...").start();
|
|
4365
|
+
try {
|
|
4366
|
+
const parsedTheme = parseThemeObject(absThemePath);
|
|
4367
|
+
const result = themeSchema.safeParse(parsedTheme);
|
|
4368
|
+
if (!result.success) {
|
|
4369
|
+
const issues = result.error.issues.map((issue) => ` \u2192 ${chalk9.yellow(issue.path.join("."))}: ${issue.message}`).join("\n");
|
|
4370
|
+
spinner.fail("Theme validation failed");
|
|
4371
|
+
console.log(chalk9.red("\n Missing or invalid fields:\n"));
|
|
4372
|
+
console.log(issues);
|
|
4373
|
+
console.log(chalk9.dim("\n Fix these fields in your theme file and run validate again.\n"));
|
|
4374
|
+
process.exit(1);
|
|
4375
|
+
}
|
|
4376
|
+
spinner.succeed("Theme file is valid");
|
|
4377
|
+
console.log(chalk9.dim(`
|
|
4378
|
+
Validated: ${configResult.data.theme}
|
|
4379
|
+
`));
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
spinner.fail("Failed to validate theme");
|
|
4382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4383
|
+
console.error(chalk9.dim(` ${message}`));
|
|
4384
|
+
process.exit(1);
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
|
|
4388
|
+
// src/index.ts
|
|
4389
|
+
function getVersion() {
|
|
4390
|
+
try {
|
|
4391
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
4392
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
4393
|
+
return pkg.version ?? "0.0.0";
|
|
4394
|
+
} catch {
|
|
4395
|
+
return "0.0.0";
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
var program = new Command3();
|
|
4399
|
+
program.name("pdfx").description("CLI for PDFx components").version(getVersion());
|
|
4400
|
+
program.configureOutput({
|
|
4401
|
+
writeErr: (str) => {
|
|
4402
|
+
const message = str.replace(/^error:\s*/i, "").trimEnd();
|
|
4403
|
+
process.stderr.write(chalk10.red(`\u2716 ${message}
|
|
4404
|
+
`));
|
|
4405
|
+
}
|
|
4406
|
+
});
|
|
4407
|
+
program.command("init").description("Initialize pdfx in your project").option("-y, --yes", "Accept all defaults without prompting (non-interactive / CI mode)").action((options) => init(options));
|
|
4408
|
+
program.command("add <components...>").description("Add components to your project").option("-f, --force", "Overwrite existing files without prompting").option("-r, --registry <url>", "Override registry URL").option("--install-deps", "Install missing component dependencies without prompting").option("--strict-deps", "Fail when any runtime or dev dependency is missing").action(
|
|
4409
|
+
(components, options) => add(components, options)
|
|
4410
|
+
);
|
|
4411
|
+
program.command("list").description("List available components from registry").action(list);
|
|
4412
|
+
program.command("diff <components...>").description("Compare local components with registry versions").action(diff);
|
|
4413
|
+
var themeCmd = program.command("theme").description("Manage PDF themes");
|
|
4414
|
+
themeCmd.command("init").description("Initialize or replace the theme file").action(themeInit);
|
|
4415
|
+
themeCmd.command("switch <preset>").description("Switch to a preset theme (professional, modern, minimal)").action(themeSwitch);
|
|
4416
|
+
themeCmd.command("validate").description("Validate your theme file").action(themeValidate);
|
|
4417
|
+
program.addCommand(mcpCommand);
|
|
4418
|
+
program.addCommand(skillsCommand);
|
|
4419
|
+
var blockCmd = program.command("block").description("Manage PDF blocks (copy-paste designs)");
|
|
4420
|
+
blockCmd.command("add <blocks...>").description("Add blocks to your project").option("-f, --force", "Overwrite existing files without prompting").action((blocks, options) => blockAdd(blocks, options));
|
|
4421
|
+
blockCmd.command("list").description("List available blocks from registry").action(blockList);
|
|
4422
|
+
try {
|
|
4423
|
+
await program.parseAsync();
|
|
4424
|
+
} catch (err) {
|
|
4425
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4426
|
+
process.stderr.write(chalk10.red(`\u2716 ${message}
|
|
4427
|
+
`));
|
|
4428
|
+
process.exitCode = 1;
|
|
4429
|
+
}
|