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/seo.ts
DELETED
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
import type { ScannedRoute } from './routes.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build-time SEO for the (otherwise JS-only) SPA: bakes site-level metadata into the HTML `<head>`
|
|
5
|
-
* so JS-less crawlers and AI bots see real tags, and generates `robots.txt`, `sitemap.xml`, and
|
|
6
|
-
* `llms.txt`. All pure string builders here; `generate.ts` wires them into the build output.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* OpenGraph defaults baked into the HTML, read by Facebook, Discord, Slack, LinkedIn, GitHub, and
|
|
11
|
-
* most link-preview crawlers. `image` should be an absolute URL (≥1200×630 for a large card).
|
|
12
|
-
*/
|
|
13
|
-
export interface SeoOpenGraph {
|
|
14
|
-
readonly title?: string;
|
|
15
|
-
readonly description?: string;
|
|
16
|
-
/** `og:type`, e.g. `'website'` or `'article'`. */
|
|
17
|
-
readonly type?: string;
|
|
18
|
-
readonly siteName?: string;
|
|
19
|
-
/** `og:locale`, e.g. `'en_US'`. */
|
|
20
|
-
readonly locale?: string;
|
|
21
|
-
/** `og:image`, the preview image (absolute URL). */
|
|
22
|
-
readonly image?: string;
|
|
23
|
-
/** `og:image:alt`. */
|
|
24
|
-
readonly imageAlt?: string;
|
|
25
|
-
/** `og:image:width` in px (helps Facebook/LinkedIn render without a re-fetch). */
|
|
26
|
-
readonly imageWidth?: number;
|
|
27
|
-
/** `og:image:height` in px. */
|
|
28
|
-
readonly imageHeight?: number;
|
|
29
|
-
/** `og:image:type`, e.g. `'image/png'`. */
|
|
30
|
-
readonly imageType?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Twitter / X card. Unset fields fall back to the OpenGraph / top-level values. */
|
|
34
|
-
export interface SeoTwitter {
|
|
35
|
-
/** `'summary'` | `'summary_large_image'` | … Defaults by whether an image is present. */
|
|
36
|
-
readonly card?: string;
|
|
37
|
-
readonly site?: string;
|
|
38
|
-
readonly creator?: string;
|
|
39
|
-
readonly title?: string;
|
|
40
|
-
readonly description?: string;
|
|
41
|
-
readonly image?: string;
|
|
42
|
-
readonly imageAlt?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** A `robots.txt` group. */
|
|
46
|
-
export interface RobotsRule {
|
|
47
|
-
readonly userAgent?: string | readonly string[];
|
|
48
|
-
readonly allow?: readonly string[];
|
|
49
|
-
readonly disallow?: readonly string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** `robots.txt` configuration. */
|
|
53
|
-
export interface RobotsConfig {
|
|
54
|
-
readonly rules?: readonly RobotsRule[];
|
|
55
|
-
/** How to treat known AI crawlers (GPTBot, ClaudeBot, Google-Extended, …). Default `'allow'`. */
|
|
56
|
-
readonly ai?: 'allow' | 'disallow';
|
|
57
|
-
/** Explicit `Sitemap:` URL (defaults to `<url>/sitemap.xml` when `seo.url` is set). */
|
|
58
|
-
readonly sitemap?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A page listed in `llms.txt`. */
|
|
62
|
-
export interface LlmsPage {
|
|
63
|
-
readonly title: string;
|
|
64
|
-
readonly url: string;
|
|
65
|
-
readonly description?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** `llms.txt` configuration (the AI-crawler guidance file). */
|
|
69
|
-
export interface LlmsConfig {
|
|
70
|
-
readonly title?: string;
|
|
71
|
-
readonly summary?: string;
|
|
72
|
-
/** Free-form instructions for AI/LLM crawlers. */
|
|
73
|
-
readonly instructions?: string;
|
|
74
|
-
/** Key pages; defaults to the site's static routes. */
|
|
75
|
-
readonly pages?: readonly LlmsPage[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Build-time SEO configuration (under `client.seo`). */
|
|
79
|
-
export interface SeoConfig {
|
|
80
|
-
/** Absolute site base URL, e.g. `https://toil.dev`, required for `sitemap.xml` and canonical/OG URLs. */
|
|
81
|
-
readonly url?: string;
|
|
82
|
-
/** Default document title baked into the HTML. */
|
|
83
|
-
readonly title?: string;
|
|
84
|
-
/** Default meta description. */
|
|
85
|
-
readonly description?: string;
|
|
86
|
-
/** Default robots directive, e.g. `'index, follow'`. */
|
|
87
|
-
readonly robotsMeta?: string;
|
|
88
|
-
/** `<meta name="theme-color">`, also the accent color of Discord/Slack link embeds. */
|
|
89
|
-
readonly themeColor?: string;
|
|
90
|
-
readonly openGraph?: SeoOpenGraph;
|
|
91
|
-
readonly twitter?: SeoTwitter;
|
|
92
|
-
/** Facebook-specific tags (`fb:app_id`). OpenGraph above covers the rest of the FB card. */
|
|
93
|
-
readonly facebook?: { readonly appId?: string };
|
|
94
|
-
/** JSON-LD structured data injected as `<script type="application/ld+json">`. */
|
|
95
|
-
readonly jsonLd?: Record<string, unknown> | readonly Record<string, unknown>[];
|
|
96
|
-
/** Origins to `<link rel="preconnect">` (early connection hints). */
|
|
97
|
-
readonly preconnect?: readonly string[];
|
|
98
|
-
/** Origins to `<link rel="dns-prefetch">`. */
|
|
99
|
-
readonly dnsPrefetch?: readonly string[];
|
|
100
|
-
/** `robots.txt` generation; `false` to skip. */
|
|
101
|
-
readonly robots?: RobotsConfig | false;
|
|
102
|
-
/** `sitemap.xml` generation; defaults to on when `url` is set. */
|
|
103
|
-
readonly sitemap?: boolean;
|
|
104
|
-
/** `llms.txt` (AI guidance) generation; `false` to skip, `true`/object to configure. */
|
|
105
|
-
readonly llms?: LlmsConfig | boolean;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Known AI / LLM crawler user-agents, for explicit allow/disallow in `robots.txt`. */
|
|
109
|
-
const AI_CRAWLERS: readonly string[] = [
|
|
110
|
-
'GPTBot',
|
|
111
|
-
'OAI-SearchBot',
|
|
112
|
-
'ChatGPT-User',
|
|
113
|
-
'ClaudeBot',
|
|
114
|
-
'Claude-Web',
|
|
115
|
-
'anthropic-ai',
|
|
116
|
-
'Google-Extended',
|
|
117
|
-
'PerplexityBot',
|
|
118
|
-
'CCBot',
|
|
119
|
-
'Applebot-Extended',
|
|
120
|
-
'Bytespider',
|
|
121
|
-
'Amazonbot',
|
|
122
|
-
'Meta-ExternalAgent',
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
/** Escapes a value for use inside a double-quoted HTML attribute (prevents attribute-breakout XSS). */
|
|
126
|
-
export function escapeAttr(value: string): string {
|
|
127
|
-
return value
|
|
128
|
-
.replace(/&/g, '&')
|
|
129
|
-
.replace(/"/g, '"')
|
|
130
|
-
.replace(/'/g, ''')
|
|
131
|
-
.replace(/</g, '<')
|
|
132
|
-
.replace(/>/g, '>');
|
|
133
|
-
}
|
|
134
|
-
/** Escapes a value for HTML text content (e.g. `<title>`, XML text). */
|
|
135
|
-
export function escapeHtml(value: string): string {
|
|
136
|
-
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Neutralizes a string for safe single-line Markdown interpolation (e.g. `llms.txt`): collapses
|
|
140
|
-
* newlines/control chars to a space and backslash-escapes the characters that would break out of a
|
|
141
|
-
* `[text](url)` link or start a new block (`[ ] ( ) < > \``). Without this, a page title/description
|
|
142
|
-
* from `generateMetadata` could inject extra list items, links, or headings.
|
|
143
|
-
*/
|
|
144
|
-
function escapeMarkdownInline(value: string): string {
|
|
145
|
-
return value
|
|
146
|
-
.replace(/[\r\n\t\f\v]+/g, ' ')
|
|
147
|
-
.replace(/[\\[\]()<>`]/g, (c) => `\\${c}`)
|
|
148
|
-
.trim();
|
|
149
|
-
}
|
|
150
|
-
/** Escapes a URL for a Markdown link target: strips whitespace/control chars and parens. */
|
|
151
|
-
function escapeMarkdownUrl(value: string): string {
|
|
152
|
-
return value.replace(/\s+/g, '').replace(/[()<>]/g, encodeURIComponent);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Serializes a value for embedding in an inline `<script>` (JSON-LD). Escapes `<`, `>`, and `&`,
|
|
156
|
-
* which neutralizes `</script>` and `<!--` (the only HTML-significant sequences inside a script),
|
|
157
|
-
* so attacker-controlled data can't break out of the script element.
|
|
158
|
-
*/
|
|
159
|
-
function escapeJsonForScript(value: unknown): string {
|
|
160
|
-
return JSON.stringify(value)
|
|
161
|
-
.replace(/</g, '\\u003c')
|
|
162
|
-
.replace(/>/g, '\\u003e')
|
|
163
|
-
.replace(/&/g, '\\u0026');
|
|
164
|
-
}
|
|
165
|
-
function meta(attrs: Record<string, string | number | undefined>): string {
|
|
166
|
-
const pairs = Object.entries(attrs)
|
|
167
|
-
.filter((entry): entry is [string, string | number] => entry[1] !== undefined)
|
|
168
|
-
.map(([k, v]) => `${k}="${escapeAttr(String(v))}"`);
|
|
169
|
-
return ` <meta ${pairs.join(' ')} />`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Joins a base URL and a route path into a clean absolute URL. */
|
|
173
|
-
export function joinUrl(base: string, path: string): string {
|
|
174
|
-
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`.replace(/\/$/, '') || base;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Static (parameter-free) route patterns, the ones that can be listed in a sitemap. */
|
|
178
|
-
function staticPaths(routes: readonly ScannedRoute[]): string[] {
|
|
179
|
-
return routes
|
|
180
|
-
.filter((r) => r.slot === undefined && !r.intercept && !/[:*]/.test(r.pattern))
|
|
181
|
-
.map((r) => r.pattern)
|
|
182
|
-
.sort();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** The site-level `<head>` fragment baked into the built HTML (title is handled separately). */
|
|
186
|
-
export function seoHeadTags(seo: SeoConfig): string {
|
|
187
|
-
const lines: string[] = [];
|
|
188
|
-
if (seo.description !== undefined)
|
|
189
|
-
lines.push(meta({ name: 'description', content: seo.description }));
|
|
190
|
-
if (seo.robotsMeta !== undefined) lines.push(meta({ name: 'robots', content: seo.robotsMeta }));
|
|
191
|
-
if (seo.themeColor !== undefined)
|
|
192
|
-
lines.push(meta({ name: 'theme-color', content: seo.themeColor }));
|
|
193
|
-
if (seo.url !== undefined)
|
|
194
|
-
lines.push(` <link rel="canonical" href="${escapeAttr(seo.url)}" />`);
|
|
195
|
-
|
|
196
|
-
// OpenGraph (also drives Facebook, Discord, Slack, LinkedIn, GitHub previews).
|
|
197
|
-
const og = seo.openGraph;
|
|
198
|
-
const ogTitle = og?.title ?? seo.title;
|
|
199
|
-
const ogDesc = og?.description ?? seo.description;
|
|
200
|
-
if (ogTitle !== undefined) lines.push(meta({ property: 'og:title', content: ogTitle }));
|
|
201
|
-
if (ogDesc !== undefined) lines.push(meta({ property: 'og:description', content: ogDesc }));
|
|
202
|
-
lines.push(meta({ property: 'og:type', content: og?.type ?? 'website' }));
|
|
203
|
-
if (seo.url !== undefined) lines.push(meta({ property: 'og:url', content: seo.url }));
|
|
204
|
-
if (og?.siteName !== undefined)
|
|
205
|
-
lines.push(meta({ property: 'og:site_name', content: og.siteName }));
|
|
206
|
-
if (og?.locale !== undefined) lines.push(meta({ property: 'og:locale', content: og.locale }));
|
|
207
|
-
if (og?.image !== undefined) {
|
|
208
|
-
lines.push(meta({ property: 'og:image', content: og.image }));
|
|
209
|
-
if (og.imageAlt !== undefined)
|
|
210
|
-
lines.push(meta({ property: 'og:image:alt', content: og.imageAlt }));
|
|
211
|
-
if (og.imageType !== undefined)
|
|
212
|
-
lines.push(meta({ property: 'og:image:type', content: og.imageType }));
|
|
213
|
-
if (og.imageWidth !== undefined)
|
|
214
|
-
lines.push(meta({ property: 'og:image:width', content: og.imageWidth }));
|
|
215
|
-
if (og.imageHeight !== undefined) {
|
|
216
|
-
lines.push(meta({ property: 'og:image:height', content: og.imageHeight }));
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (seo.facebook?.appId !== undefined) {
|
|
220
|
-
lines.push(meta({ property: 'fb:app_id', content: seo.facebook.appId }));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Twitter / X card. Unset fields fall back to OpenGraph / top-level values.
|
|
224
|
-
const tw = seo.twitter;
|
|
225
|
-
if (tw) {
|
|
226
|
-
const twImage = tw.image ?? og?.image;
|
|
227
|
-
const card = tw.card ?? (twImage !== undefined ? 'summary_large_image' : 'summary');
|
|
228
|
-
lines.push(meta({ name: 'twitter:card', content: card }));
|
|
229
|
-
if (tw.site !== undefined) lines.push(meta({ name: 'twitter:site', content: tw.site }));
|
|
230
|
-
if (tw.creator !== undefined)
|
|
231
|
-
lines.push(meta({ name: 'twitter:creator', content: tw.creator }));
|
|
232
|
-
const twTitle = tw.title ?? ogTitle;
|
|
233
|
-
const twDesc = tw.description ?? ogDesc;
|
|
234
|
-
if (twTitle !== undefined) lines.push(meta({ name: 'twitter:title', content: twTitle }));
|
|
235
|
-
if (twDesc !== undefined)
|
|
236
|
-
lines.push(meta({ name: 'twitter:description', content: twDesc }));
|
|
237
|
-
if (twImage !== undefined) lines.push(meta({ name: 'twitter:image', content: twImage }));
|
|
238
|
-
const twImageAlt = tw.imageAlt ?? og?.imageAlt;
|
|
239
|
-
if (twImageAlt !== undefined)
|
|
240
|
-
lines.push(meta({ name: 'twitter:image:alt', content: twImageAlt }));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (const origin of seo.preconnect ?? []) {
|
|
244
|
-
lines.push(` <link rel="preconnect" href="${escapeAttr(origin)}" />`);
|
|
245
|
-
}
|
|
246
|
-
for (const origin of seo.dnsPrefetch ?? []) {
|
|
247
|
-
lines.push(` <link rel="dns-prefetch" href="${escapeAttr(origin)}" />`);
|
|
248
|
-
}
|
|
249
|
-
if (seo.jsonLd !== undefined) {
|
|
250
|
-
lines.push(
|
|
251
|
-
` <script type="application/ld+json">${escapeJsonForScript(seo.jsonLd)}</script>`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
return lines.join('\n');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** The default document title to bake into the HTML, if any. */
|
|
258
|
-
export function seoTitle(seo: SeoConfig): string | undefined {
|
|
259
|
-
return seo.title;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Bakes the SEO `<head>` into an HTML document: replaces the existing `<title>` and `description`
|
|
264
|
-
* meta (so they aren't duplicated) and inserts the rest before `</head>`. Used for the shell and,
|
|
265
|
-
* per route, by the prerenderer.
|
|
266
|
-
*/
|
|
267
|
-
export function injectSeoHtml(html: string, seo: SeoConfig): string {
|
|
268
|
-
let out = html;
|
|
269
|
-
const title = seoTitle(seo);
|
|
270
|
-
if (title !== undefined) {
|
|
271
|
-
const tag = `<title>${escapeHtml(title)}</title>`;
|
|
272
|
-
out = /<title>[\s\S]*?<\/title>/i.test(out)
|
|
273
|
-
? out.replace(/<title>[\s\S]*?<\/title>/i, tag)
|
|
274
|
-
: out.replace(/<\/head>/i, ` ${tag}\n </head>`);
|
|
275
|
-
}
|
|
276
|
-
if (seo.description !== undefined) {
|
|
277
|
-
out = out.replace(/[ \t]*<meta\s+name=["']description["'][^>]*>\s*\n?/i, '');
|
|
278
|
-
}
|
|
279
|
-
const tags = seoHeadTags(seo);
|
|
280
|
-
if (tags) {
|
|
281
|
-
out = out.includes('</head>')
|
|
282
|
-
? out.replace(/<\/head>/i, `${tags}\n </head>`)
|
|
283
|
-
: `${tags}\n${out}`;
|
|
284
|
-
}
|
|
285
|
-
return out;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function asString(value: unknown): string | undefined {
|
|
289
|
-
return typeof value === 'string' ? value : undefined;
|
|
290
|
-
}
|
|
291
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
292
|
-
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Overlays a route's extracted `metadata` (title/description/openGraph/…) onto the site-wide
|
|
297
|
-
* {@link SeoConfig}, and points the canonical/`og:url` at the route's own URL. The result is what
|
|
298
|
-
* the prerenderer bakes into that route's HTML, per-file metadata winning over the site defaults.
|
|
299
|
-
*/
|
|
300
|
-
export function routeSeo(
|
|
301
|
-
seo: SeoConfig,
|
|
302
|
-
metadata: Record<string, unknown> | null,
|
|
303
|
-
pattern: string,
|
|
304
|
-
): SeoConfig {
|
|
305
|
-
const routeUrl = seo.url !== undefined ? joinUrl(seo.url, pattern) : undefined;
|
|
306
|
-
if (!metadata) return { ...seo, url: routeUrl };
|
|
307
|
-
const og = asRecord(metadata.openGraph);
|
|
308
|
-
return {
|
|
309
|
-
...seo,
|
|
310
|
-
url: asString(metadata.canonical) ?? routeUrl,
|
|
311
|
-
title: asString(metadata.title) ?? seo.title,
|
|
312
|
-
description: asString(metadata.description) ?? seo.description,
|
|
313
|
-
robotsMeta: asString(metadata.robots) ?? seo.robotsMeta,
|
|
314
|
-
themeColor: asString(metadata.themeColor) ?? seo.themeColor,
|
|
315
|
-
openGraph: {
|
|
316
|
-
...seo.openGraph,
|
|
317
|
-
title: asString(og.title) ?? asString(metadata.title) ?? seo.openGraph?.title,
|
|
318
|
-
description:
|
|
319
|
-
asString(og.description) ??
|
|
320
|
-
asString(metadata.description) ??
|
|
321
|
-
seo.openGraph?.description,
|
|
322
|
-
type: asString(og.type) ?? seo.openGraph?.type,
|
|
323
|
-
image: asString(og.image) ?? seo.openGraph?.image,
|
|
324
|
-
imageAlt: asString(og.imageAlt) ?? seo.openGraph?.imageAlt,
|
|
325
|
-
siteName: asString(og.siteName) ?? seo.openGraph?.siteName,
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** `robots.txt` contents. */
|
|
331
|
-
export function robotsTxt(seo: SeoConfig): string {
|
|
332
|
-
if (seo.robots === false) return '';
|
|
333
|
-
const cfg: RobotsConfig = seo.robots ?? {};
|
|
334
|
-
const blocks: string[] = [];
|
|
335
|
-
|
|
336
|
-
const rules = cfg.rules ?? [{ userAgent: '*', allow: ['/'] }];
|
|
337
|
-
for (const rule of rules) {
|
|
338
|
-
const agents = rule.userAgent === undefined ? ['*'] : [rule.userAgent].flat();
|
|
339
|
-
const lines = agents.map((a) => `User-agent: ${a}`);
|
|
340
|
-
for (const p of rule.allow ?? []) lines.push(`Allow: ${p}`);
|
|
341
|
-
for (const p of rule.disallow ?? []) lines.push(`Disallow: ${p}`);
|
|
342
|
-
blocks.push(lines.join('\n'));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const aiDirective = cfg.ai === 'disallow' ? 'Disallow: /' : 'Allow: /';
|
|
346
|
-
blocks.push(
|
|
347
|
-
['# AI / LLM crawlers', ...AI_CRAWLERS.map((a) => `User-agent: ${a}\n${aiDirective}`)].join(
|
|
348
|
-
'\n\n',
|
|
349
|
-
),
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const sitemap =
|
|
353
|
-
cfg.sitemap ?? (seo.url !== undefined ? joinUrl(seo.url, 'sitemap.xml') : undefined);
|
|
354
|
-
if (sitemap !== undefined) blocks.push(`Sitemap: ${sitemap}`);
|
|
355
|
-
|
|
356
|
-
return blocks.join('\n\n') + '\n';
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* `sitemap.xml` from the site's static routes plus any `extra` concrete paths (e.g. SSG URLs from
|
|
361
|
-
* `generateStaticParams`); requires `seo.url`, empty when no base URL. `extra` is deduped against the
|
|
362
|
-
* static paths.
|
|
363
|
-
*/
|
|
364
|
-
export function sitemapXml(
|
|
365
|
-
seo: SeoConfig,
|
|
366
|
-
routes: readonly ScannedRoute[],
|
|
367
|
-
extra: readonly string[] = [],
|
|
368
|
-
): string {
|
|
369
|
-
if (seo.url === undefined || seo.sitemap === false) return '';
|
|
370
|
-
const paths = [...new Set([...staticPaths(routes), ...extra])];
|
|
371
|
-
const urls = paths
|
|
372
|
-
.map((p) => ` <url><loc>${escapeHtml(joinUrl(seo.url ?? '', p))}</loc></url>`)
|
|
373
|
-
.join('\n');
|
|
374
|
-
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** `llms.txt` (AI-crawler guidance) contents; empty when disabled. */
|
|
378
|
-
export function llmsTxt(
|
|
379
|
-
seo: SeoConfig,
|
|
380
|
-
routes: readonly ScannedRoute[],
|
|
381
|
-
pages?: readonly LlmsPage[],
|
|
382
|
-
): string {
|
|
383
|
-
if (seo.llms === false) return '';
|
|
384
|
-
const cfg: LlmsConfig = seo.llms === true || seo.llms === undefined ? {} : seo.llms;
|
|
385
|
-
const title = cfg.title ?? seo.title ?? seo.url ?? 'Site';
|
|
386
|
-
const out: string[] = [`# ${title}`];
|
|
387
|
-
const summary = cfg.summary ?? seo.description;
|
|
388
|
-
if (summary !== undefined) out.push(`\n> ${summary}`);
|
|
389
|
-
if (cfg.instructions !== undefined) out.push(`\n${cfg.instructions}`);
|
|
390
|
-
|
|
391
|
-
// Page list precedence: an explicit `seo.llms.pages`, else the build-supplied list (every route's
|
|
392
|
-
// resolved title/description, including SSG-enumerated dynamic pages), else just the static paths.
|
|
393
|
-
const resolvedPages: readonly LlmsPage[] =
|
|
394
|
-
cfg.pages ??
|
|
395
|
-
pages ??
|
|
396
|
-
(seo.url !== undefined
|
|
397
|
-
? staticPaths(routes).map(
|
|
398
|
-
(p): LlmsPage => ({
|
|
399
|
-
title: p === '/' ? 'Home' : p,
|
|
400
|
-
url: joinUrl(seo.url ?? '', p),
|
|
401
|
-
}),
|
|
402
|
-
)
|
|
403
|
-
: []);
|
|
404
|
-
if (resolvedPages.length) {
|
|
405
|
-
out.push('\n## Pages\n');
|
|
406
|
-
for (const page of resolvedPages) {
|
|
407
|
-
const title = escapeMarkdownInline(page.title);
|
|
408
|
-
const url = escapeMarkdownUrl(page.url);
|
|
409
|
-
const desc =
|
|
410
|
-
page.description !== undefined ? `: ${escapeMarkdownInline(page.description)}` : '';
|
|
411
|
-
out.push(`- [${title}](${url})${desc}`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return out.join('\n') + '\n';
|
|
415
|
-
}
|
package/src/compiler/sri.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import type { Plugin } from 'vite';
|
|
6
|
-
|
|
7
|
-
import { type ResolvedToilConfig } from './config.js';
|
|
8
|
-
|
|
9
|
-
/** SHA-384 Subresource-Integrity token (`sha384-<base64>`) for a local file, or `null` if it can't
|
|
10
|
-
* be read. Memoized per absolute path (the same asset is referenced by many baked pages). */
|
|
11
|
-
function computeSri(absPath: string, cache: Map<string, string | null>): string | null {
|
|
12
|
-
const cached = cache.get(absPath);
|
|
13
|
-
if (cached !== undefined) return cached;
|
|
14
|
-
let sri: string | null;
|
|
15
|
-
try {
|
|
16
|
-
sri = `sha384-${createHash('sha384').update(fs.readFileSync(absPath)).digest('base64')}`;
|
|
17
|
-
} catch {
|
|
18
|
-
sri = null;
|
|
19
|
-
}
|
|
20
|
-
cache.set(absPath, sri);
|
|
21
|
-
return sri;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Resolve a tag URL to a path relative to `outDir`, or `null` if it is not a local build asset
|
|
25
|
-
* (an absolute scheme, protocol-relative, `data:`, or a traversal are all skipped). */
|
|
26
|
-
function toLocalAsset(url: string, base: string): string | null {
|
|
27
|
-
const clean = url.split('?')[0].split('#')[0];
|
|
28
|
-
// scheme (`https:`, `data:`), protocol-relative (`//cdn`), or empty -> not ours to hash.
|
|
29
|
-
if (clean === '' || /^[a-z][a-z0-9+.-]*:/i.test(clean) || clean.startsWith('//')) return null;
|
|
30
|
-
let rel = clean;
|
|
31
|
-
if (base && base !== '/' && rel.startsWith(base)) rel = rel.slice(base.length);
|
|
32
|
-
else if (rel.startsWith('/')) rel = rel.slice(1);
|
|
33
|
-
else if (rel.startsWith('./')) rel = rel.slice(2);
|
|
34
|
-
if (rel === '' || rel.includes('..')) return null;
|
|
35
|
-
return rel;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Add Subresource Integrity to every LOCAL `<script src>`, `<link rel="modulepreload" href>`, and
|
|
40
|
-
* `<link rel="stylesheet" href>` in `html`: an `integrity="sha384-…"` computed from the asset's
|
|
41
|
-
* bytes plus `crossorigin="anonymous"` (required for the browser to verify + a no-op same-origin).
|
|
42
|
-
* External URLs, inline scripts, non-script/style links, and tags that already carry `integrity`
|
|
43
|
-
* are left untouched. Vite content-hashes these assets, so the integrity is stable and matches the
|
|
44
|
-
* immutable-cached bytes the edge serves.
|
|
45
|
-
*/
|
|
46
|
-
export function injectSri(html: string, outDir: string, base: string): string {
|
|
47
|
-
const cache = new Map<string, string | null>();
|
|
48
|
-
return html.replace(/<(script|link)\b([^>]*)>/gi, (full, tag: string, rawAttrs: string) => {
|
|
49
|
-
// `(?<![\w-])` (not `\b`) so a hyphenated decoy attr never counts as the real one: `\bintegrity`
|
|
50
|
-
// matches inside `data-integrity` (would wrongly skip us), `\bsrc` inside `data-src` (would hash
|
|
51
|
-
// the wrong URL). The guard requires the attr name to start at a real attribute boundary.
|
|
52
|
-
if (/(?<![\w-])integrity\s*=/i.test(rawAttrs)) return full; // already integrity-tagged
|
|
53
|
-
const selfClose = /\/\s*$/.test(rawAttrs);
|
|
54
|
-
const attrs = rawAttrs.replace(/\s*\/\s*$/, '');
|
|
55
|
-
const isScript = tag.toLowerCase() === 'script';
|
|
56
|
-
if (!isScript) {
|
|
57
|
-
const rel = /(?<![\w-])rel\s*=\s*["']?([^"'\s>]+)/i.exec(attrs)?.[1]?.toLowerCase();
|
|
58
|
-
if (rel !== 'stylesheet' && rel !== 'modulepreload') return full;
|
|
59
|
-
}
|
|
60
|
-
const attr = isScript ? 'src' : 'href';
|
|
61
|
-
const url = new RegExp(`(?<![\\w-])${attr}\\s*=\\s*["']([^"']+)["']`, 'i').exec(attrs)?.[1];
|
|
62
|
-
if (url === undefined) return full; // inline script or no url
|
|
63
|
-
const rel = toLocalAsset(url, base);
|
|
64
|
-
if (rel === null) return full;
|
|
65
|
-
const sri = computeSri(path.join(outDir, rel), cache);
|
|
66
|
-
if (sri === null) return full;
|
|
67
|
-
const cross = /(?<![\w-])crossorigin(?![\w-])/i.test(attrs) ? '' : ' crossorigin="anonymous"';
|
|
68
|
-
return `<${tag}${attrs} integrity="${sri}"${cross}${selfClose ? ' /' : ''}>`;
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Build an `<script type="importmap">` tag whose `integrity` section maps every emitted JS chunk
|
|
74
|
-
* (`assets/*.js`) to its sha384. Tag-level SRI only protects the ENTRY script: the rest of the
|
|
75
|
-
* module graph -- static imports (the react vendor chunk) and lazily `import()`ed route chunks --
|
|
76
|
-
* is fetched by the module loader, which does NOT inherit the tag's integrity. The import map's
|
|
77
|
-
* `integrity` field is the platform mechanism that extends verification to those fetches; browsers
|
|
78
|
-
* without support ignore the key (progressive enhancement, nothing breaks). Returns `null` when
|
|
79
|
-
* there are no chunks.
|
|
80
|
-
*/
|
|
81
|
-
export function buildImportMapIntegrity(
|
|
82
|
-
outDir: string,
|
|
83
|
-
base: string,
|
|
84
|
-
cache: Map<string, string | null> = new Map(),
|
|
85
|
-
): string | null {
|
|
86
|
-
const assetsDir = path.join(outDir, 'assets');
|
|
87
|
-
let names: string[];
|
|
88
|
-
try {
|
|
89
|
-
names = fs.readdirSync(assetsDir).filter((n) => n.endsWith('.js'));
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const prefix = base.endsWith('/') ? base : `${base}/`;
|
|
94
|
-
const integrity: Record<string, string> = {};
|
|
95
|
-
for (const name of names.sort()) {
|
|
96
|
-
const sri = computeSri(path.join(assetsDir, name), cache);
|
|
97
|
-
if (sri !== null) integrity[`${prefix}assets/${name}`] = sri;
|
|
98
|
-
}
|
|
99
|
-
if (Object.keys(integrity).length === 0) return null;
|
|
100
|
-
return `<script type="importmap">${JSON.stringify({ integrity })}</script>`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Insert the import-map tag where it takes effect: right after `<head>` (an import map must be
|
|
104
|
-
* parsed before any module load). Falls back to prepending before the first `<script`. No-op if
|
|
105
|
-
* an integrity import map is already present (idempotent on an already-processed shell). */
|
|
106
|
-
export function injectImportMap(html: string, importMapTag: string): string {
|
|
107
|
-
if (html.includes('<script type="importmap">{"integrity"')) return html;
|
|
108
|
-
const head = /<head[^>]*>/i.exec(html);
|
|
109
|
-
if (head !== null) {
|
|
110
|
-
const at = head.index + head[0].length;
|
|
111
|
-
return `${html.slice(0, at)}\n ${importMapTag}${html.slice(at)}`;
|
|
112
|
-
}
|
|
113
|
-
const script = html.indexOf('<script');
|
|
114
|
-
if (script !== -1) return `${html.slice(0, script)}${importMapTag}\n${html.slice(script)}`;
|
|
115
|
-
return html;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Build-only plugin: after the bundle is written, rewrites the built shell (`outDir/index.html`) so
|
|
120
|
-
* (a) every local script/stylesheet/modulepreload TAG carries an `integrity`, and (b) an import map
|
|
121
|
-
* `integrity` section covers every emitted JS chunk, so the FULL module graph -- the entry's static
|
|
122
|
-
* imports and every dynamically `import()`ed route chunk -- is verified, not just the entry tag. It
|
|
123
|
-
* runs in `closeBundle` (assets are on disk, so real bytes are hashed) and is registered BEFORE the
|
|
124
|
-
* prerender / SSR-template passes, which bake per-route HTML, bracket templates, and the SSR `.tmpl`
|
|
125
|
-
* FROM this shell -- so every emitted page inherits both and the `.tmpl` coherence hash covers them.
|
|
126
|
-
* Build-only: the dev server serves un-hashed modules straight from Vite, where SRI is meaningless.
|
|
127
|
-
*/
|
|
128
|
-
export function sriPlugin(cfg: ResolvedToilConfig): Plugin {
|
|
129
|
-
return {
|
|
130
|
-
name: 'toil:sri',
|
|
131
|
-
apply: 'build',
|
|
132
|
-
closeBundle() {
|
|
133
|
-
const outDir = path.resolve(cfg.root, cfg.outDir);
|
|
134
|
-
const shell = path.join(outDir, 'index.html');
|
|
135
|
-
if (!fs.existsSync(shell)) return;
|
|
136
|
-
const html = fs.readFileSync(shell, 'utf8');
|
|
137
|
-
const cache = new Map<string, string | null>();
|
|
138
|
-
let out = injectSri(html, outDir, cfg.base);
|
|
139
|
-
const importMap = buildImportMapIntegrity(outDir, cfg.base, cache);
|
|
140
|
-
if (importMap !== null) out = injectImportMap(out, importMap);
|
|
141
|
-
if (out !== html) fs.writeFileSync(shell, out);
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
}
|