toiljs 0.0.115 → 0.0.117
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/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
package/src/compiler/emails.ts
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build-time email pipeline: compile `emails/*.tsx` React components into a
|
|
3
|
-
* generated AssemblyScript module the server WASM uses to send mail.
|
|
4
|
-
*
|
|
5
|
-
* Why build-time: email clients run NO JavaScript and strip `<style>`/external
|
|
6
|
-
* CSS, and the edge server is WASM (no React at send time). So each email is
|
|
7
|
-
* rendered to STATIC, inline-CSS HTML once at build, with `{{token}}` holes
|
|
8
|
-
* where component props were read; the edge fills those holes per send via the
|
|
9
|
-
* `EmailTemplate` global (see `server/globals/email.ts`) and calls `email_send`.
|
|
10
|
-
*
|
|
11
|
-
* Per-send data is therefore FIELD SUBSTITUTION only (`{{name}}`, `{{code}}`) --
|
|
12
|
-
* a build-time `{items.map(...)}` or conditional bakes into the output, it does
|
|
13
|
-
* not re-run per send. Plenty for transactional / 2FA / confirmation email.
|
|
14
|
-
*
|
|
15
|
-
* The dynamic fields are discovered without parsing types: the component is
|
|
16
|
-
* rendered with a Proxy whose every prop read returns the literal `{{prop}}`
|
|
17
|
-
* and records the name, so `({name}) => <h1>Hi {name}` renders `Hi {{name}}`
|
|
18
|
-
* and we learn the field is `name`.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import fs from 'node:fs';
|
|
22
|
-
import path from 'node:path';
|
|
23
|
-
|
|
24
|
-
import pc from 'picocolors';
|
|
25
|
-
import { createServer, type ViteDevServer } from 'vite';
|
|
26
|
-
|
|
27
|
-
import type { ResolvedToilConfig } from './config.js';
|
|
28
|
-
import { createViteConfig } from './vite.js';
|
|
29
|
-
|
|
30
|
-
/** What an `emails/*.tsx` module may export. `default` is the React component. */
|
|
31
|
-
interface EmailModule {
|
|
32
|
-
default: unknown;
|
|
33
|
-
/** Subject line, a token template (e.g. `'Welcome, {{name}}'`). Defaults to the name. */
|
|
34
|
-
subject?: unknown;
|
|
35
|
-
/** Optional plain-text body template; derived by stripping the HTML when absent. */
|
|
36
|
-
text?: unknown;
|
|
37
|
-
/** Optional dedup/abuse `purpose` tag; defaults to the email name lowercased. */
|
|
38
|
-
purpose?: unknown;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** One email rendered to its baked, token-holed parts. */
|
|
42
|
-
export interface RenderedEmail {
|
|
43
|
-
name: string;
|
|
44
|
-
subject: string;
|
|
45
|
-
html: string;
|
|
46
|
-
text: string;
|
|
47
|
-
/** Sorted, unique `{{token}}` field names this email interpolates. */
|
|
48
|
-
tokens: string[];
|
|
49
|
-
purpose: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Keys React (or the renderer) reads on props that must never become tokens. */
|
|
53
|
-
const REACT_INTERNAL = new Set([
|
|
54
|
-
'children',
|
|
55
|
-
'key',
|
|
56
|
-
'ref',
|
|
57
|
-
'$$typeof',
|
|
58
|
-
'__self',
|
|
59
|
-
'__source',
|
|
60
|
-
'toJSON',
|
|
61
|
-
'prototype',
|
|
62
|
-
'constructor',
|
|
63
|
-
'then', // so a thenable check never tokenizes
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Reserved `emails/*.tsx` module names (by PascalCase basename) that OVERRIDE a
|
|
68
|
-
* built-in auth email instead of becoming a generic `Emails.<Name>.send(...)`
|
|
69
|
-
* entry. When one is present, the auth pipeline (src/compiler/auth-emails.ts)
|
|
70
|
-
* bakes it into `_auth_emails.ts` in place of the default; here we just make sure
|
|
71
|
-
* it does NOT also surface as an app-callable `Emails.*` template.
|
|
72
|
-
*/
|
|
73
|
-
export const RESERVED_AUTH_EMAIL_NAMES = new Set(['AuthConfirm', 'AuthReset', 'Auth2fa']);
|
|
74
|
-
|
|
75
|
-
const TOKEN_RE = /\{\{\s*([A-Za-z_$][\w$]*)\s*\}\}/g;
|
|
76
|
-
|
|
77
|
-
function extractTokens(s: string): string[] {
|
|
78
|
-
const out: string[] = [];
|
|
79
|
-
let m: RegExpExecArray | null;
|
|
80
|
-
TOKEN_RE.lastIndex = 0;
|
|
81
|
-
while ((m = TOKEN_RE.exec(s)) !== null) out.push(m[1]!);
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* A props object whose every (non-internal) read returns the literal `{{key}}`
|
|
87
|
-
* and records `key`. Renders the component with placeholders and discovers the
|
|
88
|
-
* field names in one pass — no TS type parsing.
|
|
89
|
-
*/
|
|
90
|
-
function tokenProps(seen: Set<string>): Record<string, unknown> {
|
|
91
|
-
return new Proxy(
|
|
92
|
-
{},
|
|
93
|
-
{
|
|
94
|
-
get(_target, key): unknown {
|
|
95
|
-
if (typeof key !== 'string' || REACT_INTERNAL.has(key)) return undefined;
|
|
96
|
-
seen.add(key);
|
|
97
|
-
return `{{${key}}}`;
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Inline `<style>`/CSS into element `style=""` (juice) so email clients honor it.
|
|
104
|
-
* Optional: without juice installed, inline `style={{}}` props still work. */
|
|
105
|
-
async function inlineCss(html: string): Promise<string> {
|
|
106
|
-
try {
|
|
107
|
-
// Variable specifier: keep `juice` an OPTIONAL dependency (tsc won't
|
|
108
|
-
// require its types, and a missing install falls back below).
|
|
109
|
-
const specifier = 'juice';
|
|
110
|
-
const mod = (await import(specifier)) as { default: (html: string) => string };
|
|
111
|
-
return mod.default(html);
|
|
112
|
-
} catch {
|
|
113
|
-
return html;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** A crude HTML→text fallback for the plain-text part (better deliverability). */
|
|
118
|
-
function htmlToText(html: string): string {
|
|
119
|
-
return html
|
|
120
|
-
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
121
|
-
.replace(/<(?:br|\/p|\/h[1-6]|\/tr|\/div)\s*\/?>/gi, '\n')
|
|
122
|
-
.replace(/<[^>]+>/g, '')
|
|
123
|
-
.replace(/ /g, ' ')
|
|
124
|
-
.replace(/'|'/g, "'")
|
|
125
|
-
.replace(/"|"/g, '"')
|
|
126
|
-
.replace(/</g, '<')
|
|
127
|
-
.replace(/>/g, '>')
|
|
128
|
-
.replace(/&/g, '&') // last: so "&lt;" -> "<", not "<"
|
|
129
|
-
.replace(/[ \t]+/g, ' ')
|
|
130
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
131
|
-
.replace(/[ \t]*\n[ \t]*/g, '\n')
|
|
132
|
-
.trim();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Render one loaded email module to its baked parts, or `null` if it has no
|
|
136
|
-
* component. The default export must be a FUNCTION component (not a class) of
|
|
137
|
-
* its props: we call it directly with the token Proxy and render the element
|
|
138
|
-
* tree it returns. Going through `createElement(Component, proxy)` would not
|
|
139
|
-
* work -- React copies the config into a plain props object, so the component
|
|
140
|
-
* would never see the Proxy and every field would render empty. */
|
|
141
|
-
async function renderModule(
|
|
142
|
-
name: string,
|
|
143
|
-
mod: EmailModule,
|
|
144
|
-
render: (el: unknown) => string,
|
|
145
|
-
css = '',
|
|
146
|
-
): Promise<RenderedEmail | null> {
|
|
147
|
-
if (typeof mod.default !== 'function') return null;
|
|
148
|
-
|
|
149
|
-
const seen = new Set<string>();
|
|
150
|
-
const component = mod.default as (props: unknown) => unknown;
|
|
151
|
-
let html = render(component(tokenProps(seen)));
|
|
152
|
-
// CSS the component imported (e.g. `import 'client/styles/email.css'`) is
|
|
153
|
-
// prepended as a <style> block so it gets inlined into element style="" like
|
|
154
|
-
// an inline block would -- under SSR a bare CSS import otherwise has no effect.
|
|
155
|
-
html = await inlineCss(css ? `<style>${css}</style>${html}` : html);
|
|
156
|
-
|
|
157
|
-
const subject = typeof mod.subject === 'string' ? mod.subject : name;
|
|
158
|
-
const text = typeof mod.text === 'string' ? mod.text : htmlToText(html);
|
|
159
|
-
const purpose =
|
|
160
|
-
typeof mod.purpose === 'string' && mod.purpose.length > 0
|
|
161
|
-
? mod.purpose
|
|
162
|
-
: name.toLowerCase();
|
|
163
|
-
|
|
164
|
-
// Union the proxy-observed fields with any literal {{token}} authored in the
|
|
165
|
-
// subject/text/html, so a hand-written placeholder is also a parameter.
|
|
166
|
-
const tokens = [
|
|
167
|
-
...new Set([
|
|
168
|
-
...seen,
|
|
169
|
-
...extractTokens(subject),
|
|
170
|
-
...extractTokens(html),
|
|
171
|
-
...extractTokens(text),
|
|
172
|
-
]),
|
|
173
|
-
].sort();
|
|
174
|
-
|
|
175
|
-
return { name, subject, html, text, tokens, purpose };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const CSS_RE = /\.(css|scss|sass|less|styl|pcss|postcss)(\?|$)/;
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Collect the CSS an email module transitively imports, as one string. Under SSR
|
|
182
|
-
* a bare `import 'client/styles/email.css'` produces no output, so we walk the
|
|
183
|
-
* Vite module graph from the email module, collect its CSS deps, and re-import
|
|
184
|
-
* each with `?inline` (Vite then returns the processed CSS as the default export,
|
|
185
|
-
* Tailwind/PostCSS included). The caller hands the result to `renderModule`,
|
|
186
|
-
* which inlines it into the HTML. Best-effort: a CSS dep that can't be inlined is
|
|
187
|
-
* skipped (the component's inline `style={{}}` props still render).
|
|
188
|
-
*/
|
|
189
|
-
export async function collectModuleCss(server: ViteDevServer, moduleId: string): Promise<string> {
|
|
190
|
-
const seen = new Set<string>();
|
|
191
|
-
const cssIds = new Set<string>();
|
|
192
|
-
const visit = (id: string): void => {
|
|
193
|
-
if (seen.has(id)) return;
|
|
194
|
-
seen.add(id);
|
|
195
|
-
const mod = server.moduleGraph.getModuleById(id);
|
|
196
|
-
if (!mod) return;
|
|
197
|
-
for (const dep of mod.importedModules) {
|
|
198
|
-
const depId = dep.id ?? dep.url;
|
|
199
|
-
if (!depId) continue;
|
|
200
|
-
if (CSS_RE.test(depId)) cssIds.add(depId);
|
|
201
|
-
else visit(depId);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
visit(moduleId);
|
|
205
|
-
|
|
206
|
-
let css = '';
|
|
207
|
-
for (const id of cssIds) {
|
|
208
|
-
const base = id.split('?')[0] ?? id;
|
|
209
|
-
try {
|
|
210
|
-
const mod = (await server.ssrLoadModule(`${base}?inline`)) as { default?: unknown };
|
|
211
|
-
if (typeof mod.default === 'string') css += mod.default + '\n';
|
|
212
|
-
} catch {
|
|
213
|
-
// skip a CSS dep we can't inline
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return css;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Load one `emails/*.tsx` through `server` (SSR), collect any CSS it imports, and
|
|
221
|
-
* render it to its baked, token-holed parts. Shared by the build/codegen pass and
|
|
222
|
-
* the dev preview tool so both produce byte-identical output. Throws if the module
|
|
223
|
-
* fails to load; returns `null` if it has no default-exported component.
|
|
224
|
-
*/
|
|
225
|
-
export async function renderEmailFile(
|
|
226
|
-
server: ViteDevServer,
|
|
227
|
-
emailsDir: string,
|
|
228
|
-
file: string,
|
|
229
|
-
render: (el: unknown) => string,
|
|
230
|
-
): Promise<RenderedEmail | null> {
|
|
231
|
-
const name = toPascal(path.basename(file).replace(/\.(tsx|jsx)$/, ''));
|
|
232
|
-
const filePath = path.join(emailsDir, file);
|
|
233
|
-
const mod = (await server.ssrLoadModule(filePath)) as EmailModule;
|
|
234
|
-
const node =
|
|
235
|
-
server.moduleGraph.getModuleById(filePath) ??
|
|
236
|
-
(await server.moduleGraph.getModuleByUrl(filePath));
|
|
237
|
-
const css = node?.id ? await collectModuleCss(server, node.id) : '';
|
|
238
|
-
return renderModule(name, mod, render, css);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** `welcome-email` / `welcome_email` -> `WelcomeEmail`. */
|
|
242
|
-
export function toPascal(base: string): string {
|
|
243
|
-
return base
|
|
244
|
-
.split(/[-_\s.]+/)
|
|
245
|
-
.filter(Boolean)
|
|
246
|
-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
247
|
-
.join('');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Server source dir (where the generated module must live to be compiled): the
|
|
251
|
-
* dir of the first toilconfig entry, else `<root>/server`. */
|
|
252
|
-
export function serverDir(root: string): string {
|
|
253
|
-
try {
|
|
254
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8')) as {
|
|
255
|
-
entries?: unknown;
|
|
256
|
-
};
|
|
257
|
-
const first = Array.isArray(cfg.entries)
|
|
258
|
-
? cfg.entries.find((e): e is string => typeof e === 'string')
|
|
259
|
-
: undefined;
|
|
260
|
-
if (first) return path.dirname(path.resolve(root, first));
|
|
261
|
-
} catch {
|
|
262
|
-
// fall through to the default
|
|
263
|
-
}
|
|
264
|
-
return path.join(root, 'server');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** A valid AS/JS string literal for `s` (double-quoted, fully escaped). */
|
|
268
|
-
function asLit(s: string): string {
|
|
269
|
-
return JSON.stringify(s);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** A unique, valid AS parameter identifier for `token`, avoiding the fixed
|
|
273
|
-
* `to`/`purpose` params, AS keywords, and earlier tokens. */
|
|
274
|
-
function paramName(token: string, used: Set<string>): string {
|
|
275
|
-
const RESERVED = new Set(['to', 'purpose', 'class', 'function', 'new', 'this', 'type', 'in']);
|
|
276
|
-
let p = token;
|
|
277
|
-
while (used.has(p) || RESERVED.has(p)) p = p + '_';
|
|
278
|
-
used.add(p);
|
|
279
|
-
return p;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** Codegen the `Emails` AssemblyScript module from the rendered set. */
|
|
283
|
-
function renderModuleSource(rendered: RenderedEmail[]): string {
|
|
284
|
-
const out: string[] = [];
|
|
285
|
-
out.push('// GENERATED by toiljs from emails/*.tsx -- DO NOT EDIT.');
|
|
286
|
-
out.push('// Each email is rendered to static, inline-CSS HTML at build time;');
|
|
287
|
-
out.push('// {{tokens}} are filled per send. `EmailTemplate`/`EmailStatus` are');
|
|
288
|
-
out.push('// toiljs globals (server/globals/email.ts), so no import is needed.');
|
|
289
|
-
out.push('');
|
|
290
|
-
out.push('export namespace Emails {');
|
|
291
|
-
for (const e of rendered) {
|
|
292
|
-
out.push(` export namespace ${e.name} {`);
|
|
293
|
-
out.push(` const SUBJECT: string = ${asLit(e.subject)};`);
|
|
294
|
-
out.push(` const TEXT: string = ${asLit(e.text)};`);
|
|
295
|
-
out.push(` const HTML: string = ${asLit(e.html)};`);
|
|
296
|
-
const used = new Set<string>();
|
|
297
|
-
const params = e.tokens.map((t) => ({ token: t, param: paramName(t, used) }));
|
|
298
|
-
const sig = ['to: string']
|
|
299
|
-
.concat(params.map((p) => `${p.param}: string`))
|
|
300
|
-
.concat(`purpose: string = ${asLit(e.purpose)}`)
|
|
301
|
-
.join(', ');
|
|
302
|
-
out.push(
|
|
303
|
-
` /** Render and send this email to \`to\`. Returns the send's EmailStatus. */`,
|
|
304
|
-
);
|
|
305
|
-
out.push(` export function send(${sig}): EmailStatus {`);
|
|
306
|
-
out.push(` const __v = new Map<string, string>();`);
|
|
307
|
-
for (const p of params) out.push(` __v.set(${asLit(p.token)}, ${p.param});`);
|
|
308
|
-
out.push(` return new EmailTemplate(SUBJECT, TEXT, HTML).send(to, __v, purpose);`);
|
|
309
|
-
out.push(` }`);
|
|
310
|
-
out.push(` }`);
|
|
311
|
-
}
|
|
312
|
-
out.push('}');
|
|
313
|
-
return out.join('\n') + '\n';
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const GENERATED_BASENAME = '_emails.ts';
|
|
317
|
-
|
|
318
|
-
function warn(msg: string): void {
|
|
319
|
-
process.stderr.write(` toil: emails ${msg}\n`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Render every `emails/*.tsx` and (re)write the generated `<server>/_emails.ts`
|
|
324
|
-
* module, BEFORE the toilscript server build so it is compiled in. A no-op when
|
|
325
|
-
* there is no `emails/` directory. Removes a stale generated module when the
|
|
326
|
-
* directory becomes empty.
|
|
327
|
-
*/
|
|
328
|
-
export async function renderEmails(cfg: ResolvedToilConfig, authOn = false): Promise<void> {
|
|
329
|
-
const emailsDir = path.join(cfg.root, 'emails');
|
|
330
|
-
const generatedPath = path.join(serverDir(cfg.root), GENERATED_BASENAME);
|
|
331
|
-
|
|
332
|
-
if (!fs.existsSync(emailsDir)) return;
|
|
333
|
-
const files = fs
|
|
334
|
-
.readdirSync(emailsDir)
|
|
335
|
-
.filter((f) => /\.(tsx|jsx)$/.test(f))
|
|
336
|
-
.sort();
|
|
337
|
-
if (files.length === 0) {
|
|
338
|
-
if (fs.existsSync(generatedPath)) fs.rmSync(generatedPath);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// React DOM is only needed when a project actually has emails; load it
|
|
343
|
-
// lazily so a server-only project without emails never pays for it.
|
|
344
|
-
const { renderToStaticMarkup } = await import('react-dom/server');
|
|
345
|
-
|
|
346
|
-
const server = await createServer({
|
|
347
|
-
...(await createViteConfig(cfg)),
|
|
348
|
-
server: { middlewareMode: true, hmr: false },
|
|
349
|
-
appType: 'custom',
|
|
350
|
-
logLevel: 'silent',
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const rendered: RenderedEmail[] = [];
|
|
354
|
-
try {
|
|
355
|
-
for (const file of files) {
|
|
356
|
-
// Reserved auth-override templates (emails/auth-confirm.tsx, …) are
|
|
357
|
-
// consumed by the auth pipeline, not surfaced as generic `Emails.*`.
|
|
358
|
-
// Only reserved WHEN AUTH IS ON: a non-auth project keeping an email
|
|
359
|
-
// by that name still gets its normal `Emails.<Name>.send(...)`.
|
|
360
|
-
if (
|
|
361
|
-
authOn &&
|
|
362
|
-
RESERVED_AUTH_EMAIL_NAMES.has(toPascal(path.basename(file).replace(/\.(tsx|jsx)$/, '')))
|
|
363
|
-
)
|
|
364
|
-
continue;
|
|
365
|
-
try {
|
|
366
|
-
const r = await renderEmailFile(
|
|
367
|
-
server,
|
|
368
|
-
emailsDir,
|
|
369
|
-
file,
|
|
370
|
-
renderToStaticMarkup as (el: unknown) => string,
|
|
371
|
-
);
|
|
372
|
-
if (r) rendered.push(r);
|
|
373
|
-
else warn(`skipped ${file} (no default-exported component)`);
|
|
374
|
-
} catch (err) {
|
|
375
|
-
warn(`skipped ${file} (${err instanceof Error ? err.message : String(err)})`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} finally {
|
|
379
|
-
await server.close();
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (rendered.length === 0) return;
|
|
383
|
-
// Only (re)write when the output actually changed: an unconditional write
|
|
384
|
-
// bumps the file's mtime every rebuild, which under `dev` would re-trigger
|
|
385
|
-
// the watcher. (The watcher also ignores this file by name; this is belt-and-
|
|
386
|
-
// suspenders and avoids needless work.)
|
|
387
|
-
const next = renderModuleSource(rendered);
|
|
388
|
-
const prev = fs.existsSync(generatedPath) ? fs.readFileSync(generatedPath, 'utf8') : null;
|
|
389
|
-
if (prev === next) return;
|
|
390
|
-
fs.mkdirSync(path.dirname(generatedPath), { recursive: true });
|
|
391
|
-
fs.writeFileSync(generatedPath, next);
|
|
392
|
-
process.stdout.write(
|
|
393
|
-
pc.green(' ✓ ') +
|
|
394
|
-
pc.dim(
|
|
395
|
-
`emails: generated ${String(rendered.length)} template${rendered.length === 1 ? '' : 's'} (${rendered
|
|
396
|
-
.map((r) => r.name)
|
|
397
|
-
.join(', ')})`,
|
|
398
|
-
) +
|
|
399
|
-
'\n',
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Exported for unit testing the pure render/codegen without a Vite server.
|
|
404
|
-
export const __test = { renderModule, renderModuleSource, tokenProps, htmlToText, toPascal };
|
package/src/compiler/fonts.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { HtmlTagDescriptor, Logger, Plugin } from 'vite';
|
|
2
|
-
|
|
3
|
-
import { type ResolvedToilConfig } from './config.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Build-time font optimization. Bundled font files (`@font-face` `url(...)` imports) are emitted +
|
|
7
|
-
* hashed by Vite, but without a hint the browser only discovers them after parsing CSS, delaying
|
|
8
|
-
* text paint. This injects a `<link rel="preload" as="font" crossorigin>` for each bundled font so
|
|
9
|
-
* it loads in parallel with the CSS, and logs what it preloaded (mirrors the image-optimization log).
|
|
10
|
-
*/
|
|
11
|
-
const FONT_RE = /\.(woff2|woff|ttf|otf)$/i;
|
|
12
|
-
const FONT_TYPE: Record<string, string> = {
|
|
13
|
-
woff2: 'font/woff2',
|
|
14
|
-
woff: 'font/woff',
|
|
15
|
-
ttf: 'font/ttf',
|
|
16
|
-
otf: 'font/otf',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function kb(bytes: number): string {
|
|
20
|
-
return `${(bytes / 1000).toFixed(2)} kB`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Builds the `<link rel="preload">` head tags for a set of bundled font file names. */
|
|
24
|
-
export function fontPreloadTags(fileNames: readonly string[], base: string): HtmlTagDescriptor[] {
|
|
25
|
-
const prefix = base.endsWith('/') ? base : `${base}/`;
|
|
26
|
-
return fileNames
|
|
27
|
-
.filter((name) => FONT_RE.test(name))
|
|
28
|
-
.map((name) => {
|
|
29
|
-
const ext = name.split('.').pop()?.toLowerCase() ?? '';
|
|
30
|
-
return {
|
|
31
|
-
tag: 'link',
|
|
32
|
-
attrs: {
|
|
33
|
-
rel: 'preload',
|
|
34
|
-
as: 'font',
|
|
35
|
-
type: FONT_TYPE[ext] ?? `font/${ext}`,
|
|
36
|
-
href: `${prefix}${name}`,
|
|
37
|
-
crossorigin: '',
|
|
38
|
-
},
|
|
39
|
-
injectTo: 'head',
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Build-only plugin that preloads bundled fonts and logs them. Disabled by `client.fonts: false`. */
|
|
45
|
-
export function fontPreloadPlugin(cfg: ResolvedToilConfig): Plugin {
|
|
46
|
-
let logger: Logger | undefined;
|
|
47
|
-
let logged = false;
|
|
48
|
-
return {
|
|
49
|
-
name: 'toil:font-preload',
|
|
50
|
-
apply: 'build',
|
|
51
|
-
configResolved(config) {
|
|
52
|
-
logger = config.logger;
|
|
53
|
-
},
|
|
54
|
-
transformIndexHtml: {
|
|
55
|
-
order: 'post',
|
|
56
|
-
handler(html, ctx) {
|
|
57
|
-
const bundle = ctx.bundle ?? {};
|
|
58
|
-
const fonts = Object.values(bundle).filter(
|
|
59
|
-
(file) => file.type === 'asset' && FONT_RE.test(file.fileName),
|
|
60
|
-
);
|
|
61
|
-
if (fonts.length === 0) return html;
|
|
62
|
-
|
|
63
|
-
// Log once (the same template's HTML is transformed per emitted page).
|
|
64
|
-
if (!logged && logger) {
|
|
65
|
-
logged = true;
|
|
66
|
-
logger.info('');
|
|
67
|
-
logger.info(
|
|
68
|
-
` ✓ preloaded ${String(fonts.length)} font${fonts.length === 1 ? '' : 's'}`,
|
|
69
|
-
);
|
|
70
|
-
for (const file of fonts) {
|
|
71
|
-
const size =
|
|
72
|
-
file.type === 'asset' && typeof file.source !== 'string'
|
|
73
|
-
? kb(file.source.byteLength)
|
|
74
|
-
: '';
|
|
75
|
-
logger.info(` → ${file.fileName} ${size}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
html,
|
|
81
|
-
tags: fontPreloadTags(
|
|
82
|
-
fonts.map((f) => f.fileName),
|
|
83
|
-
cfg.base,
|
|
84
|
-
),
|
|
85
|
-
};
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|