@protobase/cli 0.66.3 → 0.66.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/pdfkit-fonts.js +22 -0
- package/dist/build/serve-runtime.js +2 -1
- package/dist/cli/package.js +1 -1
- package/dist/serve/host-module-ids.js +1 -0
- package/dist/serve/host-modules.js +2 -0
- package/package.json +7 -7
- package/skills/protobase/SKILL.md +1 -0
- package/skills/protobase/references/features/email-and-public-forms.md +2 -1
- package/skills/protobase/references/features.md +1 -1
- package/skills/protobase/references/reference/cli.md +3 -0
- package/skills/protobase/references/reference/pdf.md +140 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/build/pdfkit-fonts.ts
|
|
2
|
+
const nodeBuild = /[\\/]pdfkit[\\/]js[\\/]pdfkit\.node\.mjs$/;
|
|
3
|
+
const fontLoad = /\b[\w$]+\('#standard-fonts\/(\w+)'\)/g;
|
|
4
|
+
/**
|
|
5
|
+
* Bundles pdfkit's standard fonts, which React PDF's renderer writes text in. pdfkit's Node build loads each with a
|
|
6
|
+
* `require` of its own subpath import, `#standard-fonts/Helvetica`, made at run time, which a bundle cannot resolve
|
|
7
|
+
* from its own folder; this turns each into an import of the font's module beside it.
|
|
8
|
+
*/
|
|
9
|
+
const pdfkitFonts = () => ({
|
|
10
|
+
name: "protobase-pdfkit-fonts",
|
|
11
|
+
transform(code, id) {
|
|
12
|
+
if (!nodeBuild.test(id)) return;
|
|
13
|
+
const fonts = new Set([...code.matchAll(fontLoad)].map(([, font]) => font));
|
|
14
|
+
if (fonts.size === 0) this.error("pdfkit no longer loads its standard fonts as `require(\"#standard-fonts/<Font>\")`; update pdfkitFonts");
|
|
15
|
+
return {
|
|
16
|
+
code: [...fonts].map((font) => `import __pdfkitFont${font} from './standard-fonts/${font}.mjs'\n`).join("") + code.replace(fontLoad, (_, font) => `__pdfkitFont${font}`),
|
|
17
|
+
map: null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
//#endregion
|
|
22
|
+
export { pdfkitFonts };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { moduleFile } from "../module-file.js";
|
|
2
2
|
import { viteBundle } from "./vite-bundle.js";
|
|
3
3
|
import { npmWs } from "./npm-ws.js";
|
|
4
|
+
import { pdfkitFonts } from "./pdfkit-fonts.js";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
//#region src/build/serve-runtime.ts
|
|
6
7
|
const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
@@ -11,7 +12,7 @@ const buildServeRuntime = async ({ outFile, bun }) => {
|
|
|
11
12
|
entry: serveMain,
|
|
12
13
|
outFile,
|
|
13
14
|
bun,
|
|
14
|
-
plugins: [npmWs()]
|
|
15
|
+
plugins: [npmWs(), pdfkitFonts()]
|
|
15
16
|
});
|
|
16
17
|
};
|
|
17
18
|
//#endregion
|
package/dist/cli/package.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as react from "react";
|
|
|
20
20
|
import * as reactJsxRuntime from "react/jsx-runtime";
|
|
21
21
|
import * as zod from "zod";
|
|
22
22
|
import * as protobaseConfig from "@protobase/config";
|
|
23
|
+
import * as pdf from "@protobase/server/pdf";
|
|
23
24
|
//#region src/serve/host-modules.ts
|
|
24
25
|
const hostModules = {
|
|
25
26
|
"@protobase/config": protobaseConfig,
|
|
@@ -28,6 +29,7 @@ const hostModules = {
|
|
|
28
29
|
"@protobase/layout/jsx-runtime": layout.jsxRuntime,
|
|
29
30
|
"@protobase/query": query,
|
|
30
31
|
"@protobase/server": server,
|
|
32
|
+
"@protobase/server/pdf": pdf,
|
|
31
33
|
kysely,
|
|
32
34
|
"kysely/helpers/postgres": kyselyPostgres,
|
|
33
35
|
pg: pg$1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.4",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@better-auth/core": "^1.7.12",
|
|
44
44
|
"@hono/node-server": "^2.1.3",
|
|
45
|
-
"@protobase/config": "0.66.
|
|
46
|
-
"@protobase/layout": "0.66.
|
|
47
|
-
"@protobase/query": "0.66.
|
|
48
|
-
"@protobase/schema": "0.66.
|
|
49
|
-
"@protobase/server": "0.66.
|
|
50
|
-
"@protobase/ui": "0.66.
|
|
45
|
+
"@protobase/config": "0.66.4",
|
|
46
|
+
"@protobase/layout": "0.66.4",
|
|
47
|
+
"@protobase/query": "0.66.4",
|
|
48
|
+
"@protobase/schema": "0.66.4",
|
|
49
|
+
"@protobase/server": "0.66.4",
|
|
50
|
+
"@protobase/ui": "0.66.4",
|
|
51
51
|
"@react-email/render": "^2.1.0",
|
|
52
52
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
53
53
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -89,6 +89,7 @@ Reference:
|
|
|
89
89
|
- When letting people without an account create records → read [Public forms](references/reference/forms.md)
|
|
90
90
|
- When running background jobs → read [Jobs](references/reference/jobs.md)
|
|
91
91
|
- When sending email → read [Email](references/reference/email.md)
|
|
92
|
+
- When making a PDF file, such as an invoice, a quote or a report → read [PDF documents](references/reference/pdf.md)
|
|
92
93
|
- When registering users' devices for push notifications → read [Notifications](references/reference/notifications.md)
|
|
93
94
|
- When people should set themselves busy, do not disturb or a status text, or see others' beside their names → read [Status](references/reference/status.md)
|
|
94
95
|
- When adding audio and video calls, through LiveKit or peer to peer → read [Calls](references/reference/calls.md)
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Email and public forms
|
|
4
4
|
|
|
5
|
-
Mail the app sends, and forms for people without an account.
|
|
5
|
+
Mail the app sends, the PDF documents it makes, and forms for people without an account.
|
|
6
6
|
|
|
7
7
|
- [Email](../reference/email.md#configuration): The app sends mail through one SMTP server or a mailer you supply.
|
|
8
8
|
- [Built-in emails](../reference/email.md#protobases-own-mail): Sign-in codes, password resets, invitations and mentions are sent in the app's name.
|
|
9
9
|
- [Email templates](../reference/email.md#templates): Your own emails are React components with typed data, sent from jobs, hooks or functions.
|
|
10
10
|
- [Attachments](../reference/email.md#attachments): A message carries files, such as an invoice's PDF, through the app's mail or someone's own mailbox.
|
|
11
|
+
- [PDF documents](../reference/pdf.md#documents): PDF files such as invoices and quotes are React components with typed data, rendered from jobs, hooks or functions and attached to mail as they are.
|
|
11
12
|
- [Mail from your own mailbox](../reference/email.md#from-someones-own-mailbox) (beta): People link their Gmail on the account page, and the app's functions send mail from it, with a clear error when the link is missing or expired.
|
|
12
13
|
- [Public forms](../reference/forms.md#what-the-visitor-sees): A form at its own address lets people without an account create a record, such as a contact request.
|
|
13
14
|
- [Bot check](../reference/forms.md#bot-check): Each submission passes a check in the browser, without a third-party service or a puzzle.
|
|
@@ -33,7 +33,7 @@ A feature without a mark is stable. The others say so after their name:
|
|
|
33
33
|
- [Events, webhooks and jobs](features/events-webhooks-and-jobs.md): React to changes, deliver them to other systems, and run work in the background.
|
|
34
34
|
- [Notifications](features/notifications.md): A bell in the top bar and push to browsers and phones, for what your app tells its users.
|
|
35
35
|
- [Calls](features/calls.md): Audio and video calls between an app's users, with Protobase deciding who is in a call and who hears whom.
|
|
36
|
-
- [Email and public forms](features/email-and-public-forms.md): Mail the app sends, and forms for people without an account.
|
|
36
|
+
- [Email and public forms](features/email-and-public-forms.md): Mail the app sends, the PDF documents it makes, and forms for people without an account.
|
|
37
37
|
- [AI](features/ai.md): Fields and actions filled by a model, the assistant beside the admin, and MCP.
|
|
38
38
|
- [Compatibility and moving from other systems](features/compatibility.md): Apps written for other systems that keep working on Protobase, and moving their data and code over.
|
|
39
39
|
- [Several apps](features/several-apps.md): Show another Protobase app's records and accept its users.
|
|
@@ -262,10 +262,13 @@ The modules the app shares with plugins, `react`, `react/jsx-runtime`, `react-do
|
|
|
262
262
|
| `better-auth`, `hono`, `jose`, `zod`, `aip-parsers`, `@hono/node-server`, `@scalar/hono-api-reference` | the main entry of each other dependency the `@protobase` packages run on the server |
|
|
263
263
|
| `hono/cors`, `hono/http-exception`, `hono/validator` | Hono middleware for [API functions](../guides/api-functions.md) |
|
|
264
264
|
| `react`, `react/jsx-runtime`, `@react-email/render` | what [email templates](email.md#in-a-bundle) render with, so a template's component and the renderer share one React |
|
|
265
|
+
| `@protobase/server/pdf` | React PDF's components, which [PDF documents](pdf.md#in-a-bundle) are built from; `definePdf` renders them with the runtime's React PDF, loaded on the first document rendered |
|
|
265
266
|
| `@protobase/server/email` | the [look of Protobase's own mail](email.md#the-look-of-protobases-own-mail), loaded when the bundle first imports it rather than when the runtime starts |
|
|
266
267
|
|
|
267
268
|
`react-email` is a dependency of `@protobase/server` that the runtime does not supply: the server loads it only to send its own mail, or for `@protobase/server/email`, so a config that imports it inlines its own copy, like any other package.
|
|
268
269
|
|
|
270
|
+
`@react-pdf/renderer` is a dependency of `@protobase/server` that the runtime keeps to itself: a config imports React PDF's components from `@protobase/server/pdf` instead.
|
|
271
|
+
|
|
269
272
|
Importing any other `@protobase` package or dependency of one (`@protobase/ui`, `react-dom`, `better-auth/plugins`, `zod/v4`, ...) fails the build with this list, also from inside an inlined package, when the finished bundle still imports it: an inlined package may re-export a module that imports one, as `react-email` does with `tailwindcss`, as long as the project leaves that export unused. The runtime supplies them at its own versions, whichever the project installed. The list is `packages/cli/src/serve/host-module-ids.ts`; the set of packages comes from the `package.json` of `@protobase/cli` and of every `@protobase` package it depends on.
|
|
270
273
|
- Packages with a native add-on stay imports too, and the bundle carries them in `node_modules/` (see [Native packages](#native-packages)). Node's built-in modules stay imports.
|
|
271
274
|
- Everything else (the project's files and its other packages) is inlined. Nothing is minified.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<!-- Generated by scripts/protobase-skill.ts from docs/website/src/content/docs/reference/pdf.mdx: run `bun run docs:skill`, do not edit. -->
|
|
2
|
+
|
|
3
|
+
# PDF documents
|
|
4
|
+
|
|
5
|
+
PDF files an app makes, such as invoices, quotes and reports, written as React components with typed data and rendered from jobs, hooks and API functions.
|
|
6
|
+
|
|
7
|
+
An app makes a PDF file, such as an invoice, a quote or a report, from a React component of its data, the way it writes its [emails](email.md#templates). The component is built from the components of [React PDF](https://react-pdf.org) (`@react-pdf/renderer`), and Protobase renders it to the bytes of a PDF file with React PDF's renderer.
|
|
8
|
+
|
|
9
|
+
## Documents
|
|
10
|
+
|
|
11
|
+
A document is `definePdf`: a name and a React component whose props are the document's data, with React PDF's `<Document>` at its root. `@protobase/server` brings React PDF; the project adds React for its JSX, which a project with a UI has already:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add react
|
|
15
|
+
bun add -d @types/react
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```tsx title="documents/repair-quote.tsx"
|
|
19
|
+
import { definePdf } from '@protobase/server'
|
|
20
|
+
import { Document, Page, StyleSheet, Text, View } from '@protobase/server/pdf'
|
|
21
|
+
|
|
22
|
+
export type RepairQuote = { number: string; customer: string; bike: string; problem: string; estimate: string }
|
|
23
|
+
|
|
24
|
+
const styles = StyleSheet.create({
|
|
25
|
+
page: { padding: 48, fontSize: 11, fontFamily: 'Helvetica' },
|
|
26
|
+
heading: { fontSize: 20, fontFamily: 'Helvetica-Bold', marginBottom: 16 },
|
|
27
|
+
row: { flexDirection: 'row', justifyContent: 'space-between', borderBottom: '1pt solid #e4e4e7', paddingVertical: 6 },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// The document's data is the component's props: `render` takes exactly these.
|
|
31
|
+
const RepairQuoteDocument = ({ number, customer, bike, problem, estimate }: RepairQuote) => (
|
|
32
|
+
<Document title={`Quote ${number}`}>
|
|
33
|
+
<Page size="A4" style={styles.page}>
|
|
34
|
+
<Text style={styles.heading}>Quote {number}</Text>
|
|
35
|
+
<Text>For {customer}</Text>
|
|
36
|
+
<View style={styles.row}>
|
|
37
|
+
<Text>
|
|
38
|
+
{bike}: {problem}
|
|
39
|
+
</Text>
|
|
40
|
+
<Text>{estimate}</Text>
|
|
41
|
+
</View>
|
|
42
|
+
</Page>
|
|
43
|
+
</Document>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
export const repairQuote = definePdf({ name: 'repair-quote', component: RepairQuoteDocument })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| `definePdf` option | |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `name` | Names the document. |
|
|
52
|
+
| `component` | The document, a React component of the data with a `<Document>` at its root. |
|
|
53
|
+
|
|
54
|
+
The definition that comes back has one method, typed by the component's props: `render(data)` gives the bytes of the PDF file, a `Uint8Array`. An email's [attachment](email.md#attachments) and a `Response` take them as they are.
|
|
55
|
+
|
|
56
|
+
React and React PDF's renderer are loaded on the first document rendered, not when the app starts.
|
|
57
|
+
|
|
58
|
+
## Components
|
|
59
|
+
|
|
60
|
+
`@protobase/server/pdf` exports React PDF's components, typed as `@react-pdf/renderer` types them: `Document`, `Page`, `View`, `Text`, `Link`, `Image`, `ImageBackground`, `Note`, `Canvas`, the form fields `FieldSet`, `TextInput`, `Checkbox`, `Select` and `List`, and `Svg` with its shapes (`G`, `Path`, `Rect`, `Line`, `Circle`, `Ellipse`, `Polygon`, `Polyline`, `Tspan`, `Defs`, `Stop`, `ClipPath`, `Marker`, `LinearGradient`, `RadialGradient`). `StyleSheet.create` types a document's styles. React PDF's documentation explains each component and the styles they take: [components](https://react-pdf.org/components), [styling](https://react-pdf.org/styling).
|
|
61
|
+
|
|
62
|
+
Importing them loads nothing of React PDF: they are only the names its renderer knows its elements by.
|
|
63
|
+
|
|
64
|
+
- **Fonts:** text is set in the PDF standard fonts, `Helvetica` (the default), `Times-Roman` and `Courier`, each with its bold and italic faces, such as `Helvetica-Bold`, chosen with the `fontFamily` style. They write the characters of Windows-1252, the Latin script of Western European languages, the euro sign included.
|
|
65
|
+
- <gap>Documents cannot use fonts of their own, nor scripts beyond Windows-1252, because registering a font with React PDF's renderer is not implemented.</gap>
|
|
66
|
+
- **Images:** the renderer fetches an `<Image>` whose `src` is a URL, and reads one whose `src` is a file path, on the server. Give it the image's bytes, as a `Buffer` or a `data:` URI, or an address the app chose, never one a user typed.
|
|
67
|
+
|
|
68
|
+
## Rendering
|
|
69
|
+
|
|
70
|
+
An **[API function](../guides/api-functions.md)** may answer with the file, for the browser to show or save:
|
|
71
|
+
|
|
72
|
+
```ts title="functions/repair-quote.ts"
|
|
73
|
+
import { defineFunction } from '@protobase/server'
|
|
74
|
+
import { repairQuote } from '../documents/repair-quote'
|
|
75
|
+
|
|
76
|
+
// GET /api/functions/repair-quote/7: the repair's quote as a PDF file, which the browser shows or saves.
|
|
77
|
+
export default defineFunction({ roles: ['desk'] }, async (request, { records }) => {
|
|
78
|
+
const id = Number(new URL(request.url).pathname.slice(1))
|
|
79
|
+
const { record: repair } = await records.get('repairs', id)
|
|
80
|
+
const { record: customer } = await records.get('customers', repair.customer_id as number)
|
|
81
|
+
const number = repair.number as string
|
|
82
|
+
const pdf = await repairQuote.render({
|
|
83
|
+
number,
|
|
84
|
+
customer: customer.name as string,
|
|
85
|
+
bike: repair.bike as string,
|
|
86
|
+
problem: repair.problem as string,
|
|
87
|
+
estimate: `${repair.currency} ${repair.estimate}`,
|
|
88
|
+
})
|
|
89
|
+
return new Response(pdf, { headers: { 'content-type': 'application/pdf', 'content-disposition': `inline; filename="quote-${number}.pdf"` } })
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A **job** renders it to send by mail, so the email goes out after the write that asked for it committed, and a message the server refuses is retried. A **write hook** enqueues such a job rather than rendering in the write's transaction, as for [email](email.md#sending):
|
|
94
|
+
|
|
95
|
+
```ts title="jobs/email-repair-quote.ts"
|
|
96
|
+
import { FatalJobError, type JobRun } from '@protobase/server'
|
|
97
|
+
import { repairQuote } from '../documents/repair-quote'
|
|
98
|
+
|
|
99
|
+
// Emails the customer their repair's quote, the PDF attached as it was rendered.
|
|
100
|
+
export default (async ({ repairId }, { db, mail }) => {
|
|
101
|
+
if (!mail) throw new FatalJobError('Mail is off: set PROTOBASE_SMTP_URL and PROTOBASE_MAIL_FROM')
|
|
102
|
+
const repair = await db
|
|
103
|
+
.selectFrom('repairs')
|
|
104
|
+
.innerJoin('customers', 'customers.id', 'repairs.customer_id')
|
|
105
|
+
.select(['repairs.number', 'repairs.bike', 'repairs.problem', 'repairs.estimate', 'repairs.currency', 'customers.name', 'customers.email'])
|
|
106
|
+
.where('repairs.id', '=', repairId)
|
|
107
|
+
.executeTakeFirst()
|
|
108
|
+
if (!repair?.email) return
|
|
109
|
+
const pdf = await repairQuote.render({
|
|
110
|
+
number: repair.number,
|
|
111
|
+
customer: repair.name,
|
|
112
|
+
bike: repair.bike,
|
|
113
|
+
problem: repair.problem,
|
|
114
|
+
estimate: `${repair.currency} ${repair.estimate}`,
|
|
115
|
+
})
|
|
116
|
+
await mail.send({
|
|
117
|
+
to: repair.email,
|
|
118
|
+
subject: `Quote ${repair.number} for your ${repair.bike}`,
|
|
119
|
+
text: `Hi ${repair.name}, the quote for your ${repair.bike} is attached.`,
|
|
120
|
+
attachments: [{ filename: `quote-${repair.number}.pdf`, content: pdf, contentType: 'application/pdf' }],
|
|
121
|
+
})
|
|
122
|
+
}) satisfies JobRun<{ repairId: number }>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts title="protobase.config.ts"
|
|
126
|
+
import { defineConfig } from '@protobase/config'
|
|
127
|
+
|
|
128
|
+
export default defineConfig({
|
|
129
|
+
jobs: {
|
|
130
|
+
// jobs/email-repair-quote.ts
|
|
131
|
+
'email-repair-quote': { description: "Emails a customer their repair's quote as a PDF.", arguments: { repairId: 'integer' } },
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A [template's](email.md#templates) `send` takes the same attachment as its fourth argument, and so does a function's `personalMail`, to send it from [someone's own mailbox](email.md#from-someones-own-mailbox).
|
|
137
|
+
|
|
138
|
+
## In a bundle
|
|
139
|
+
|
|
140
|
+
A [bundle](cli.md#the-config-module) gets `@protobase/server/pdf` from the serve runtime, as it gets `@protobase/server`, whose `definePdf` renders with the runtime's own React PDF. A served config imports React PDF's components from `@protobase/server/pdf`: `@react-pdf/renderer` is a dependency of `@protobase/server` that the runtime does not hand to a bundle, so importing it fails the build.
|