@cparkerwebm/webmonterey 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +104 -0
  4. package/agents/.gitkeep +0 -0
  5. package/dist/webm.mjs +2381 -0
  6. package/hooks/.gitkeep +0 -0
  7. package/package.json +101 -0
  8. package/schema/design.json +118 -0
  9. package/skills/launch/SKILL.md +183 -0
  10. package/skills/new-component/SKILL.md +85 -0
  11. package/skills/start/SKILL.md +117 -0
  12. package/skills/traps/SKILL.md +333 -0
  13. package/skills/upgrade/SKILL.md +42 -0
  14. package/src/actions/index.ts +348 -0
  15. package/src/cli/checks.test.ts +711 -0
  16. package/src/cli/checks.ts +822 -0
  17. package/src/cli/codemods.test.ts +42 -0
  18. package/src/cli/codemods.ts +51 -0
  19. package/src/cli/compare.test.ts +144 -0
  20. package/src/cli/compare.ts +222 -0
  21. package/src/cli/design-extract.test.ts +96 -0
  22. package/src/cli/design-extract.ts +229 -0
  23. package/src/cli/doctor.ts +187 -0
  24. package/src/cli/mcp.test.ts +57 -0
  25. package/src/cli/mcp.ts +110 -0
  26. package/src/cli/new.ts +148 -0
  27. package/src/cli/package-root.ts +58 -0
  28. package/src/cli/scaffold.test.ts +230 -0
  29. package/src/cli/scaffold.ts +424 -0
  30. package/src/cli/seed.ts +133 -0
  31. package/src/cli/slug.test.ts +70 -0
  32. package/src/cli/slug.ts +109 -0
  33. package/src/cli/sync.test.ts +137 -0
  34. package/src/cli/sync.ts +266 -0
  35. package/src/cli/upgrade.ts +93 -0
  36. package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
  37. package/src/design/brand.ts +49 -0
  38. package/src/design/compile.test.ts +98 -0
  39. package/src/design/compile.ts +155 -0
  40. package/src/design/defaults.ts +315 -0
  41. package/src/design/index.ts +18 -0
  42. package/src/design/resolve.test.ts +80 -0
  43. package/src/design/resolve.ts +108 -0
  44. package/src/design/types.ts +96 -0
  45. package/src/emails/autoresponse.test.ts +82 -0
  46. package/src/emails/autoresponse.ts +83 -0
  47. package/src/emails/footer.test.ts +102 -0
  48. package/src/emails/footer.ts +91 -0
  49. package/src/emails/index.ts +24 -0
  50. package/src/emails/subject.test.ts +66 -0
  51. package/src/emails/subject.ts +80 -0
  52. package/src/emails/submission-notification.ts +76 -0
  53. package/src/env.d.ts +12 -0
  54. package/src/includes/cloudflare/d1/client.ts +86 -0
  55. package/src/includes/cloudflare/r2/README.md +68 -0
  56. package/src/includes/cloudflare/r2/media.ts +41 -0
  57. package/src/includes/cloudflare/r2/url.test.ts +44 -0
  58. package/src/includes/cloudflare/r2/url.ts +34 -0
  59. package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
  60. package/src/includes/cloudflare/turnstile/verify.ts +157 -0
  61. package/src/includes/cloudflare/workers/env.ts +56 -0
  62. package/src/includes/google/tag-manager/TagManager.astro +73 -0
  63. package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
  64. package/src/includes/sinch/mailgun/redirect.ts +96 -0
  65. package/src/includes/sinch/mailgun/send.ts +186 -0
  66. package/src/includes/webmonterey/animations/animations.css +347 -0
  67. package/src/includes/webmonterey/animations/observe.test.ts +88 -0
  68. package/src/includes/webmonterey/animations/observe.ts +209 -0
  69. package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
  70. package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
  71. package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
  72. package/src/includes/webmonterey/compliance/consent.ts +142 -0
  73. package/src/includes/webmonterey/config.test.ts +94 -0
  74. package/src/includes/webmonterey/config.ts +346 -0
  75. package/src/includes/webmonterey/copy-defaults.ts +148 -0
  76. package/src/includes/webmonterey/copy.ts +13 -0
  77. package/src/includes/webmonterey/credits/Credit.astro +80 -0
  78. package/src/includes/webmonterey/credits/credit.test.ts +111 -0
  79. package/src/includes/webmonterey/credits/credit.ts +59 -0
  80. package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
  81. package/src/includes/webmonterey/forms/honeypot.ts +66 -0
  82. package/src/includes/webmonterey/prose/inline.test.ts +94 -0
  83. package/src/includes/webmonterey/prose/inline.ts +71 -0
  84. package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
  85. package/src/includes/webmonterey/site.ts +136 -0
  86. package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
  87. package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
  88. package/src/integration/adapter.ts +53 -0
  89. package/src/integration/app-middleware.ts +43 -0
  90. package/src/integration/config.ts +96 -0
  91. package/src/integration/content.ts +66 -0
  92. package/src/integration/image-size.test.ts +83 -0
  93. package/src/integration/image-size.ts +100 -0
  94. package/src/integration/index.ts +386 -0
  95. package/src/integration/virtual.d.ts +101 -0
  96. package/src/layouts/base.astro +402 -0
  97. package/src/package.test.ts +121 -0
  98. package/src/pages/404.astro +33 -0
  99. package/src/pages/[...slug].astro +126 -0
  100. package/src/pages/robots.txt.ts +41 -0
  101. package/src/pages/webm.astro +23 -0
  102. package/src/scripts/.gitkeep +0 -0
  103. package/src/styles/base.css +185 -0
  104. package/src/styles/global.css +47 -0
  105. package/src/styles/layers.test.ts +32 -0
  106. package/src/styles/layers.ts +21 -0
  107. package/src/styles/layout.css +235 -0
  108. package/src/styles/reset.css +135 -0
  109. package/src/styles/utilities.css +90 -0
  110. package/src/worker.ts +76 -0
  111. package/template/assets/logo.svg +14 -0
  112. package/template/assets/open-graph.png +0 -0
  113. package/template/migrations/0001_create_submissions.sql +40 -0
  114. package/template/migrations/README.md +41 -0
  115. package/template/public/android-chrome-192x192.png +0 -0
  116. package/template/public/android-chrome-512x512.png +0 -0
  117. package/template/public/apple-touch-icon.png +0 -0
  118. package/template/public/favicon-16x16.png +0 -0
  119. package/template/public/favicon-32x32.png +0 -0
  120. package/template/public/favicon.ico +0 -0
  121. package/template/public/favicon.svg +4 -0
  122. package/template/public/open-graph.png +0 -0
  123. package/template/scripts/check-node.mjs +72 -0
  124. package/template/scripts/test-hooks.mjs +96 -0
  125. package/template/site/.editorconfig +22 -0
  126. package/template/site/.prettierignore +15 -0
  127. package/template/site/.prettierrc.json +17 -0
  128. package/template/site/CLAUDE.md +245 -0
  129. package/template/site/CONTENT.md +135 -0
  130. package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
  131. package/template/site/public/_headers +70 -0
  132. package/template/site/src/forms/contact.json +45 -0
  133. package/template/workflows/ci.yml +52 -0
@@ -0,0 +1,135 @@
1
+ /*
2
+ * webm.reset — minimal, hand-rolled.
3
+ *
4
+ * Deliberately does NOT blanket `list-style: none`. That strips list semantics in
5
+ * Safari/VoiceOver; use the .webm-list-bare utility (which pairs with role="list") instead.
6
+ */
7
+
8
+ @layer webm.reset {
9
+ *,
10
+ *::before,
11
+ *::after {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ * {
16
+ margin: 0;
17
+ }
18
+
19
+ /*
20
+ * Put back what the rule above takes away.
21
+ *
22
+ * A modal <dialog> is centred by the UA stylesheet using `margin: auto`. The blanket
23
+ * `margin: 0` wipes that out, and the dialog silently pins itself to the top-left corner
24
+ * of the viewport. Every reset built on `* { margin: 0 }` has this bug; it only shows up
25
+ * the first time someone opens a modal.
26
+ *
27
+ * DO NOT DELETE. This has already been removed by accident once, while editing the rule
28
+ * beside it — the comment survived the edit and the rule did not, so the file still read
29
+ * as though the fix were present. Every modal on the site pinned to the top-left corner.
30
+ */
31
+ dialog {
32
+ margin: auto;
33
+ }
34
+
35
+ /*
36
+ * Make the `hidden` attribute actually hide things.
37
+ *
38
+ * The UA stylesheet's `[hidden] { display: none }` sits in the UA origin, so ANY author
39
+ * `display` beats it — no specificity contest required. An element styled
40
+ * `display: inline-flex`, or a grid/flex child, simply ignores the attribute, and
41
+ * `el.hidden = true` appears to do nothing at all. There is no error and no warning.
42
+ *
43
+ * `!important` is load-bearing, not belt-and-braces. webm.reset is the FIRST layer, so a
44
+ * plain declaration here loses to any component rule on layer order alone, whatever its
45
+ * specificity. For !important declarations that order is inverted — the earliest layer
46
+ * wins — so !important in the reset is the only construction that reaches.
47
+ *
48
+ * This replaced three hand-written copies of the same rule (two in CookieConsent, one in
49
+ * ScrollTop). A component needing to re-fix this is the signal that it belongs here.
50
+ */
51
+ [hidden] {
52
+ display: none !important;
53
+ }
54
+
55
+ html {
56
+ /* No px font-size on html — it defeats user zoom. */
57
+ -webkit-text-size-adjust: 100%;
58
+ text-size-adjust: 100%;
59
+ tab-size: 4;
60
+ }
61
+
62
+ body {
63
+ min-block-size: 100svb;
64
+ line-height: 1.5;
65
+ -webkit-font-smoothing: antialiased;
66
+ }
67
+
68
+ img,
69
+ picture,
70
+ video,
71
+ canvas,
72
+ svg {
73
+ display: block;
74
+ max-inline-size: 100%;
75
+ block-size: auto;
76
+ }
77
+
78
+ img,
79
+ video {
80
+ /* Reserve space from intrinsic dimensions to avoid layout shift. */
81
+ height: auto;
82
+ }
83
+
84
+ input,
85
+ button,
86
+ textarea,
87
+ select {
88
+ font: inherit;
89
+ color: inherit;
90
+ }
91
+
92
+ button {
93
+ cursor: pointer;
94
+ background: none;
95
+ border: none;
96
+ }
97
+
98
+ p,
99
+ h1,
100
+ h2,
101
+ h3,
102
+ h4,
103
+ h5,
104
+ h6 {
105
+ overflow-wrap: break-word;
106
+ }
107
+
108
+ h1,
109
+ h2,
110
+ h3,
111
+ h4,
112
+ h5,
113
+ h6 {
114
+ /* Inherit by default; the type scale in webm.base sets real sizes. */
115
+ font-size: inherit;
116
+ font-weight: inherit;
117
+ text-wrap: balance;
118
+ }
119
+
120
+ a {
121
+ color: inherit;
122
+ text-decoration-thickness: max(0.08em, 1px);
123
+ text-underline-offset: 0.15em;
124
+ }
125
+
126
+ table {
127
+ border-collapse: collapse;
128
+ inline-size: 100%;
129
+ }
130
+
131
+ /* Anything scrolled to by anchor gets clearance from the top of the viewport. */
132
+ :target {
133
+ scroll-margin-block-start: 5ex;
134
+ }
135
+ }
@@ -0,0 +1,90 @@
1
+ /*
2
+ * webm.utilities — single-purpose helpers. Deliberately small.
3
+ *
4
+ * This is NOT a utility framework. Layout primitives live in webm.layout; everything else
5
+ * belongs in a component. If you find yourself wanting to add a tenth utility, the thing you
6
+ * actually want is a component.
7
+ */
8
+
9
+ @layer webm.utilities {
10
+ /* Screen-reader-only, but revealed when focused (so it works for skip targets). */
11
+ .webm-visually-hidden:not(:focus):not(:active) {
12
+ clip-path: inset(50%);
13
+ block-size: 1px;
14
+ inline-size: 1px;
15
+ overflow: hidden;
16
+ position: absolute;
17
+ white-space: nowrap;
18
+ }
19
+
20
+ /*
21
+ * Removes list markers. Pair with role="list" so Safari/VoiceOver keeps list semantics:
22
+ * <ul class="webm-list-bare" role="list">
23
+ * The HTML validator flags role="list" on <ul> as redundant. It is technically correct
24
+ * and practically wrong — ignore it.
25
+ */
26
+ .webm-list-bare {
27
+ list-style: none;
28
+ padding-inline-start: 0;
29
+ }
30
+
31
+ .webm-text-center {
32
+ text-align: center;
33
+ }
34
+
35
+ .webm-text-balance {
36
+ text-wrap: balance;
37
+ }
38
+
39
+ .webm-hidden {
40
+ display: none !important;
41
+ }
42
+
43
+ /* Constrains prose to a readable measure inside a wider container. */
44
+ .webm-measure {
45
+ max-inline-size: var(--webm-width-text);
46
+ }
47
+
48
+ .webm-measure-center {
49
+ max-inline-size: var(--webm-width-text);
50
+ margin-inline: auto;
51
+ }
52
+
53
+ /* Fluid, ratio-locked media box. */
54
+ .webm-aspect {
55
+ aspect-ratio: var(--webm-aspect-ratio, 16 / 9);
56
+ inline-size: 100%;
57
+ object-fit: cover;
58
+ }
59
+
60
+ .webm-aspect:where([data-ratio='1']) {
61
+ --webm-aspect-ratio: 1 / 1;
62
+ }
63
+ .webm-aspect:where([data-ratio='4-3']) {
64
+ --webm-aspect-ratio: 4 / 3;
65
+ }
66
+ .webm-aspect:where([data-ratio='3-2']) {
67
+ --webm-aspect-ratio: 3 / 2;
68
+ }
69
+ .webm-aspect:where([data-ratio='21-9']) {
70
+ --webm-aspect-ratio: 21 / 9;
71
+ }
72
+
73
+ /*
74
+ * THE HONEYPOT FIELD. Off-screen, NOT `display: none`.
75
+ *
76
+ * Some bots skip an input that is not displayed, which defeats the whole trap - so it has to
77
+ * be genuinely rendered and merely out of sight. The other three parts of the contract are
78
+ * attributes rather than CSS and live in includes/webmonterey/forms/honeypot.ts.
79
+ *
80
+ * `!important` deliberately: a form component that lays its fields out with a grid or flex
81
+ * will otherwise reserve a cell for this one, leaving a visible gap that nobody can explain.
82
+ */
83
+ .webm-honeypot {
84
+ position: absolute !important;
85
+ inset-inline-start: -9999px !important;
86
+ inline-size: 1px !important;
87
+ block-size: 1px !important;
88
+ overflow: hidden !important;
89
+ }
90
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,76 @@
1
+ /*
2
+ * The building block for a site that needs a Cron Trigger.
3
+ *
4
+ * WHY A SITE WOULD REPLACE THE GENERATED ENTRYPOINT AT ALL. The adapter normally writes the
5
+ * Worker entrypoint itself, and that generated file exports `fetch` and nothing else. A Cron
6
+ * Trigger needs `scheduled()`, and there is no room for one in a file the adapter owns.
7
+ *
8
+ * Pointing wrangler's `main` at a SOURCE file that re-exports the adapter's handler is the
9
+ * documented path, and it is the one legitimate reason to set that key. The trap it replaces is
10
+ * silent: `triggers.crons` on its own merges into the generated config and deploys without a
11
+ * warning, and Cloudflare then invokes a handler that does not exist, on schedule, forever.
12
+ * `webm doctor` fails on that.
13
+ *
14
+ * A site uses this by writing its own src/worker.ts:
15
+ *
16
+ * import { defineWorker } from '@cparkerwebm/webmonterey/worker';
17
+ * import { runSweep } from './includes/sweep.ts';
18
+ *
19
+ * export default defineWorker({
20
+ * scheduled: (controller, env, ctx) => ctx.waitUntil(runSweep(env)),
21
+ * });
22
+ *
23
+ * and setting `"main": "./src/worker.ts"` in wrangler.jsonc.
24
+ *
25
+ * CRON RUNS IN UTC, ALWAYS, with no timezone setting anywhere in Cloudflare. A job that has to
26
+ * land at a local hour should fire hourly and let the handler check the clock in the site's own
27
+ * time zone - pinning it to a fixed UTC hour drifts by one twice a year. The other 23 runs cost
28
+ * a comparison and return.
29
+ */
30
+ import { handle } from '@astrojs/cloudflare/handler';
31
+
32
+ /*
33
+ * `env` IS THE SITE'S OWN `Env`, NOT `unknown`.
34
+ *
35
+ * `wrangler types` generates `interface Env` into each site's worker-configuration.d.ts, listing
36
+ * that site's bindings. The package cannot know them, but it does not need to: the empty
37
+ * interface below MERGES with the generated one, so inside a client repo `env.DB` is typed and
38
+ * a handler can hand it to anything expecting `Env`.
39
+ *
40
+ * It was `unknown` before, which meant every scheduled handler that passed env to a typed
41
+ * function failed to compile - and the site's own workaround for that would have been a cast,
42
+ * which is exactly the thing the generated types exist to avoid. In a repo with no generated
43
+ * types this stays an empty object, which is honest rather than wrong.
44
+ */
45
+ declare global {
46
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
47
+ interface Env {}
48
+ }
49
+
50
+ /** What a site supplies. `fetch` is deliberately absent - see below. */
51
+ export interface WorkerHandlers {
52
+ scheduled?: (
53
+ controller: { scheduledTime: number; cron: string },
54
+ env: Env,
55
+ ctx: { waitUntil(promise: Promise<unknown>): void },
56
+ ) => void | Promise<void>;
57
+ queue?: (batch: unknown, env: Env, ctx: unknown) => void | Promise<void>;
58
+ }
59
+
60
+ /**
61
+ * Build a Worker entrypoint: the adapter's own `fetch`, plus whatever else the site needs.
62
+ *
63
+ * `fetch` IS NOT OVERRIDABLE ON PURPOSE. Every page, action, API route and server island on the
64
+ * site arrives through it, so anything added there runs on all of them - and a site that meant to
65
+ * add a cron and accidentally replaced the request path takes the whole site down while the cron
66
+ * works perfectly. Site-wide request logic belongs in Astro middleware, which is scoped and
67
+ * testable; this stays a thin pass-through.
68
+ */
69
+ export function defineWorker(handlers: WorkerHandlers = {}) {
70
+ return {
71
+ fetch(request: Request, env: unknown, ctx: unknown): Response | Promise<Response> {
72
+ return handle(request as never, env as never, ctx as never);
73
+ },
74
+ ...handlers,
75
+ };
76
+ }
@@ -0,0 +1,14 @@
1
+ <svg width="4544" height="650" viewBox="0 0 4544 650" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4306.06 2.80177e-07H4129.13C4128.05 -0.00022093 4126.98 0.224794 4126 0.660606C4125.01 1.09642 4124.13 1.7334 4123.4 2.53068L3991.87 147.158C3989.77 149.464 3988.61 152.469 3988.61 155.585C3988.61 158.702 3989.77 161.707 3991.87 164.013L4089.79 271.684C4089.99 271.904 4090.23 272.08 4090.5 272.2C4090.78 272.32 4091.07 272.382 4091.37 272.382C4091.67 272.382 4091.96 272.32 4092.23 272.2C4092.5 272.08 4092.75 271.904 4092.95 271.684L4317.19 25.1349C4319.15 22.9809 4320.44 20.3043 4320.9 17.4304C4321.37 14.5565 4320.99 11.6092 4319.81 8.9467C4318.63 6.28422 4316.71 4.02128 4314.27 2.43298C4311.83 0.844685 4308.98 -0.000561826 4306.06 2.80177e-07ZM3859.26 308.124L3990.8 452.751C3991.52 453.549 3992.41 454.186 3993.39 454.621C3994.38 455.057 3995.45 455.282 3996.52 455.282H4173.46C4176.37 455.283 4179.22 454.437 4181.66 452.849C4184.1 451.261 4186.03 448.998 4187.21 446.335C4188.39 443.673 4188.76 440.725 4188.3 437.852C4187.83 434.978 4186.54 432.301 4184.58 430.147L3960.34 183.598C3960.14 183.378 3959.9 183.202 3959.63 183.082C3959.35 182.962 3959.06 182.899 3958.76 182.899C3958.46 182.899 3958.17 182.962 3957.9 183.082C3957.63 183.202 3957.38 183.378 3957.18 183.598L3859.26 291.269C3857.16 293.575 3856 296.58 3856 299.697C3856 302.813 3857.16 305.818 3859.26 308.124ZM4093.94 650H4270.87C4271.95 650 4273.02 649.775 4274 649.339C4274.99 648.904 4275.87 648.267 4276.6 647.469L4408.13 502.835C4410.23 500.529 4411.39 497.524 4411.39 494.408C4411.39 491.291 4410.23 488.286 4408.13 485.98L4310.21 378.316C4310.01 378.096 4309.77 377.92 4309.5 377.8C4309.22 377.68 4308.93 377.617 4308.63 377.617C4308.33 377.617 4308.04 377.68 4307.77 377.8C4307.5 377.92 4307.25 378.096 4307.05 378.316L4082.81 624.865C4080.85 627.019 4079.56 629.696 4079.1 632.57C4078.63 635.443 4079.01 638.391 4080.19 641.053C4081.37 643.716 4083.29 645.979 4085.73 647.567C4088.17 649.155 4091.02 650.001 4093.94 650ZM4540.74 341.876L4409.2 197.242C4408.48 196.446 4407.59 195.81 4406.61 195.376C4405.62 194.941 4404.55 194.717 4403.48 194.718H4226.54C4223.63 194.719 4220.78 195.564 4218.34 197.152C4215.9 198.74 4213.98 201.002 4212.8 203.663C4211.62 206.324 4211.24 209.27 4211.71 212.143C4212.17 215.016 4213.46 217.692 4215.42 219.846L4439.66 466.395C4439.86 466.615 4440.1 466.791 4440.37 466.911C4440.65 467.031 4440.94 467.094 4441.24 467.094C4441.54 467.094 4441.83 467.031 4442.1 466.911C4442.37 466.791 4442.62 466.615 4442.82 466.395L4540.74 358.731C4542.84 356.425 4544 353.42 4544 350.303C4544 347.187 4542.84 344.182 4540.74 341.876Z" fill="#0251FF"/>
3
+ <path d="M3655.68 188.98L3557.74 465.631C3553.32 479.279 3547.05 492.927 3538.93 506.575C3531.19 520.592 3521.23 533.134 3509.05 544.2C3497.25 555.266 3482.86 563.565 3465.9 569.098C3449.3 575 3429.56 576.475 3406.69 573.525L3400.61 527.047C3423.11 521.145 3440.63 513.215 3453.17 503.255C3465.71 493.296 3475.49 480.201 3482.5 463.971L3499.65 425.793C3502.97 417.678 3506.1 407.903 3509.05 396.468C3512.37 384.664 3515.14 374.521 3517.35 366.037L3568.26 188.98H3655.68ZM3438.78 188.98L3490.8 366.037C3492.27 373.045 3493.75 380.054 3495.22 387.062C3496.7 394.07 3497.99 401.079 3499.09 408.087H3514.03L3488.03 462.311H3468.11C3461.84 462.311 3456.31 460.467 3451.51 456.778C3446.72 453.089 3443.4 448.294 3441.55 442.392L3351.36 188.98H3438.78Z" fill="black"/>
4
+ <path d="M3200.55 181.787C3246.66 181.787 3279.67 189.718 3299.59 205.579C3319.88 221.072 3330.02 244.31 3330.02 275.295C3330.39 300.378 3324.12 319.19 3311.21 331.732C3298.3 343.904 3275.98 349.991 3244.26 349.991H3091.55V304.067H3221.58C3234.12 304.067 3242.05 301.485 3245.37 296.321C3249.06 290.788 3250.9 283.226 3250.9 273.635C3250.53 260.725 3246.66 251.872 3239.28 247.077C3232.27 241.913 3220.28 239.331 3203.32 239.331C3188.19 239.331 3176.39 241.359 3167.91 245.417C3159.42 249.474 3153.52 257.221 3150.2 268.655C3146.88 280.09 3145.22 296.689 3145.22 318.453C3145.22 341.322 3147.25 359.028 3151.31 371.57C3155.36 383.742 3162.19 392.226 3171.78 397.021C3181.74 401.817 3195.39 404.214 3212.72 404.214C3225.26 404.214 3240.76 403.845 3259.2 403.108C3278.01 402.001 3296.82 400.341 3315.64 398.128L3323.38 448.478C3312.69 453.643 3300.51 457.7 3286.86 460.651C3273.22 463.602 3259.2 465.631 3244.81 466.737C3230.43 468.213 3216.6 468.951 3203.32 468.951C3168.64 468.951 3140.61 463.971 3119.21 454.011C3098.19 443.683 3082.88 428.006 3073.29 406.981C3064.07 385.955 3059.46 359.028 3059.46 326.199C3059.46 290.419 3064.25 262.016 3073.84 240.99C3083.43 219.965 3098.56 204.841 3119.21 195.62C3139.87 186.398 3166.98 181.787 3200.55 181.787Z" fill="black"/>
5
+ <path d="M2903.46 188.98L2913.42 243.204L2921.16 250.397V462.311H2839.27V188.98H2903.46ZM3036.8 181.787L3029.06 257.589H3006.37C2993.46 257.589 2980 259.434 2965.98 263.122C2951.96 266.811 2934.26 272.344 2912.86 279.722L2907.33 233.798C2926.51 216.83 2945.88 203.919 2965.43 195.066C2984.98 186.214 3003.97 181.787 3022.42 181.787H3036.8Z" fill="black"/>
6
+ <path d="M2656.43 181.787C2702.54 181.787 2735.56 189.718 2755.48 205.579C2775.76 221.072 2785.91 244.31 2785.91 275.295C2786.28 300.378 2780 319.19 2767.09 331.732C2754.18 343.904 2731.87 349.991 2700.14 349.991H2547.43V304.067H2677.46C2690 304.067 2697.93 301.485 2701.25 296.321C2704.94 290.788 2706.78 283.226 2706.78 273.635C2706.42 260.725 2702.54 251.872 2695.17 247.077C2688.16 241.913 2676.17 239.331 2659.2 239.331C2644.08 239.331 2632.27 241.359 2623.79 245.417C2615.31 249.474 2609.4 257.221 2606.08 268.655C2602.76 280.09 2601.1 296.689 2601.1 318.453C2601.1 341.322 2603.13 359.028 2607.19 371.57C2611.25 383.742 2618.07 392.226 2627.66 397.021C2637.62 401.817 2651.27 404.214 2668.61 404.214C2681.15 404.214 2696.64 403.845 2715.08 403.108C2733.9 402.001 2752.71 400.341 2771.52 398.128L2779.27 448.478C2768.57 453.643 2756.4 457.7 2742.75 460.651C2729.1 463.602 2715.08 465.631 2700.7 466.737C2686.31 468.213 2672.48 468.951 2659.2 468.951C2624.53 468.951 2596.49 463.971 2575.1 454.011C2554.07 443.683 2538.77 428.006 2529.17 406.981C2519.95 385.955 2515.34 359.028 2515.34 326.199C2515.34 290.419 2520.14 262.016 2529.73 240.99C2539.32 219.965 2554.44 204.841 2575.1 195.62C2595.76 186.398 2622.87 181.787 2656.43 181.787Z" fill="black"/>
7
+ <path d="M2406.81 113.178V374.889C2406.81 384.849 2409.02 392.042 2413.45 396.468C2418.24 400.526 2425.99 402.554 2436.69 402.554H2472.1L2480.95 457.884C2474.31 460.098 2466.2 461.942 2456.61 463.418C2447.02 465.262 2437.42 466.553 2427.83 467.291C2418.24 468.397 2410.13 468.951 2403.49 468.951C2378.77 468.951 2359.41 462.311 2345.39 449.032C2331.74 435.752 2324.92 417.125 2324.92 393.148V113.178H2406.81ZM2483.16 188.98V242.65H2276.78V191.747L2329.9 188.98H2483.16Z" fill="black"/>
8
+ <path d="M2158.05 181.787C2209.69 181.787 2235.51 207.055 2235.51 257.589V462.311H2153.63V283.041C2153.63 270.869 2151.41 262.569 2146.99 258.143C2142.56 253.347 2135 250.95 2124.3 250.95C2112.5 250.95 2100.32 253.347 2087.78 258.143C2075.24 262.569 2059.75 269.578 2041.31 279.168L2037.43 233.244C2056.98 216.645 2076.9 203.919 2097.19 195.066C2117.85 186.214 2138.13 181.787 2158.05 181.787ZM2033.56 188.98L2042.97 243.204L2049.61 250.397V462.311H1967.72V188.98H2033.56Z" fill="black"/>
9
+ <path d="M1771.38 181.787C1805.69 181.787 1833.17 186.582 1853.82 196.173C1874.48 205.395 1889.23 220.518 1898.09 241.544C1907.31 262.569 1911.92 290.788 1911.92 326.199C1911.92 360.872 1907.31 388.722 1898.09 409.747C1889.23 430.773 1874.48 445.896 1853.82 455.118C1833.17 464.34 1805.69 468.951 1771.38 468.951C1737.45 468.951 1709.96 464.34 1688.94 455.118C1668.28 445.896 1653.34 430.773 1644.12 409.747C1634.9 388.722 1630.29 360.872 1630.29 326.199C1630.29 290.788 1634.9 262.569 1644.12 241.544C1653.34 220.518 1668.28 205.395 1688.94 196.173C1709.96 186.582 1737.45 181.787 1771.38 181.787ZM1771.38 241.544C1757.36 241.544 1746.3 243.941 1738.18 248.737C1730.07 253.532 1724.35 262.016 1721.03 274.189C1717.71 286.361 1716.05 303.698 1716.05 326.199C1716.05 348.331 1717.71 365.483 1721.03 377.656C1724.35 389.828 1730.07 398.312 1738.18 403.108C1746.3 407.534 1757.36 409.747 1771.38 409.747C1785.4 409.747 1796.46 407.534 1804.58 403.108C1812.69 398.312 1818.41 389.828 1821.73 377.656C1825.05 365.483 1826.71 348.331 1826.71 326.199C1826.71 303.698 1825.05 286.361 1821.73 274.189C1818.41 262.016 1812.69 253.532 1804.58 248.737C1796.46 243.941 1785.4 241.544 1771.38 241.544Z" fill="black"/>
10
+ <path d="M1531.23 80.533C1551.15 80.533 1561.47 90.8613 1562.21 111.518L1576.6 462.311H1498.58L1485.86 145.823H1475.9L1414.48 383.742C1411.16 401.448 1400.09 410.301 1381.28 410.301H1328.17C1309.35 410.301 1297.92 401.448 1293.86 383.742L1232.44 145.823H1223.04L1211.42 462.311H1132.85L1147.24 111.518C1147.97 90.8613 1158.49 80.533 1178.77 80.533H1257.9C1275.97 80.533 1287.04 89.5703 1291.09 107.645L1340.34 299.64C1342.55 308.493 1344.4 317.53 1345.87 326.752C1347.35 335.605 1349.01 344.642 1350.85 353.864H1358.04C1359.89 344.642 1361.55 335.605 1363.02 326.752C1364.87 317.53 1366.71 308.309 1368.56 299.087L1417.25 107.645C1420.94 89.5703 1432 80.533 1450.44 80.533H1531.23Z" fill="black"/>
11
+ <path d="M971.614 187.32C1005.55 187.32 1030.08 198.202 1045.2 219.965C1060.7 241.359 1068.44 277.324 1068.44 327.859C1068.44 351.835 1066.41 372.676 1062.36 390.382C1058.67 408.087 1052.4 422.842 1043.54 434.646C1034.69 446.081 1022.89 454.749 1008.13 460.651C993.378 466.184 975.119 468.951 953.355 468.951C944.134 468.951 934.359 468.582 924.03 467.844C914.071 467.106 903.927 465.815 893.599 463.971C883.271 462.495 872.942 460.651 862.614 458.438C852.655 456.225 843.064 453.458 833.842 450.138L862.061 426.346C873.127 429.297 883.824 431.879 894.152 434.093C904.48 435.937 914.44 437.228 924.03 437.966C933.621 438.335 943.212 438.519 952.802 438.519C971.983 438.519 987.291 435.015 998.726 428.006C1010.53 420.998 1019.01 409.378 1024.18 393.148C1029.34 376.918 1031.92 355.155 1031.92 327.859C1031.92 300.931 1029.71 279.537 1025.28 263.676C1021.23 247.446 1014.03 235.826 1003.71 228.818C993.378 221.809 978.992 218.305 960.548 218.305C945.056 218.305 929.932 222.731 915.178 231.584C900.792 240.068 884.008 251.872 864.827 266.996L862.614 242.097C874.418 230.293 886.221 220.518 898.025 212.772C909.829 204.657 921.817 198.386 933.99 193.96C946.531 189.533 959.073 187.32 971.614 187.32ZM868.7 75V182.34C868.7 193.775 868.516 203.919 868.147 212.772C867.778 221.625 866.671 230.662 864.827 239.884L868.147 248.183L868.7 451.798L833.842 450.138V75H868.7Z" fill="black"/>
12
+ <path d="M646.714 187.32C685.076 187.32 713.11 194.329 730.816 208.346C748.89 222.363 758.112 243.204 758.481 270.869C758.85 294.845 754.239 313.288 744.649 326.199C735.058 339.109 719.012 345.564 696.511 345.564H541.034V317.899H688.765C702.413 317.899 711.635 313.473 716.43 304.62C721.225 295.767 723.439 284.701 723.07 271.422C722.701 252.241 716.615 238.593 704.811 230.478C693.007 221.994 674.195 217.752 648.374 217.752C626.98 217.752 610.012 221.072 597.47 227.711C584.929 234.351 576.076 245.601 570.912 261.463C565.748 276.955 563.166 298.165 563.166 325.092C563.166 354.97 566.301 378.209 572.572 394.808C578.843 411.038 588.802 422.473 602.45 429.113C616.098 435.384 633.988 438.519 656.12 438.519C671.613 438.519 687.658 437.966 704.258 436.859C721.225 435.384 735.98 433.724 748.522 431.879L752.395 455.671C744.649 458.622 734.689 461.02 722.516 462.864C710.713 464.709 698.356 466.184 685.445 467.291C672.904 468.397 661.653 468.951 651.694 468.951C621.447 468.951 597.101 464.34 578.658 455.118C560.584 445.527 547.304 430.404 538.82 409.747C530.336 388.722 526.094 361.241 526.094 327.305C526.094 292.263 530.152 264.598 538.267 244.31C546.751 224.023 559.846 209.452 577.552 200.599C595.257 191.747 618.311 187.32 646.714 187.32Z" fill="black"/>
13
+ <path d="M485.799 80.533L400.037 451.245C398.93 454.196 397.086 456.778 394.504 458.991C391.922 461.204 388.602 462.311 384.544 462.311H340.834C337.145 462.311 333.825 461.204 330.874 458.991C328.292 456.778 326.632 454.011 325.895 450.692L258.392 151.356C256.916 143.978 255.256 136.785 253.412 129.777C251.937 122.4 250.461 115.207 248.986 108.198H236.26C234.784 115.207 233.309 122.4 231.833 129.777C230.358 137.154 228.882 144.347 227.407 151.356L159.904 450.692C159.166 454.011 157.322 456.778 154.371 458.991C151.789 461.204 148.654 462.311 144.965 462.311H100.701C97.0122 462.311 93.6924 461.204 90.7414 458.991C88.1594 456.778 86.4994 454.196 85.7617 451.245L0 80.533H38.1778L107.894 390.382C109.738 397.021 111.214 404.03 112.32 411.407C113.796 418.785 115.271 426.346 116.747 434.093H130.579C132.055 426.346 133.53 418.969 135.006 411.961C136.481 404.583 137.956 397.39 139.432 390.382L206.935 93.259C207.672 89.2014 209.332 86.0661 211.914 83.8529C214.865 81.6396 218.185 80.533 221.874 80.533H263.925C267.613 80.533 270.749 81.6396 273.331 83.8529C275.913 86.0661 277.573 89.2014 278.311 93.259L346.367 390.382C348.211 397.759 349.686 405.137 350.793 412.514C352.269 419.522 353.56 426.715 354.666 434.093H369.052C370.527 426.715 371.818 419.522 372.925 412.514C374.401 405.137 375.876 397.944 377.352 390.935L447.621 80.533H485.799Z" fill="black"/>
14
+ </svg>
Binary file
@@ -0,0 +1,40 @@
1
+ -- Form submissions.
2
+ --
3
+ -- Apply with:
4
+ -- npx wrangler d1 migrations apply <DATABASE_NAME> --local
5
+ -- npx wrangler d1 migrations apply <DATABASE_NAME> --remote <- production
6
+ --
7
+ -- `--remote` is the one people forget. A deploy does not run migrations for you.
8
+
9
+ CREATE TABLE IF NOT EXISTS submissions (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+
12
+ -- Which form this came from, e.g. 'contact'. Matches the filename in src/forms/.
13
+ form TEXT NOT NULL,
14
+
15
+ -- The submitted fields as a JSON object. Deliberately schemaless: every client site has
16
+ -- different fields, and a migration per form would be unmanageable across a fleet.
17
+ -- Query individual values with SQLite's JSON operators:
18
+ -- SELECT data ->> '$.email' FROM submissions WHERE form = 'contact';
19
+ data TEXT NOT NULL,
20
+
21
+ -- Set once the notification email is accepted by Mailgun, so a failed send can be found
22
+ -- and retried rather than silently lost. NULL means "not sent yet".
23
+ notified_at TEXT,
24
+
25
+ -- Request metadata, for spam triage and abuse reports. Keep this minimal: it is personal
26
+ -- data under CCPA, and anything stored here has to be disclosed and deletable.
27
+ ip TEXT,
28
+ user_agent TEXT,
29
+ country TEXT,
30
+
31
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
32
+ );
33
+
34
+ -- The common query: "show me this form's submissions, newest first".
35
+ CREATE INDEX IF NOT EXISTS idx_submissions_form_created
36
+ ON submissions (form, created_at DESC);
37
+
38
+ -- Finds submissions whose notification email never went out.
39
+ CREATE INDEX IF NOT EXISTS idx_submissions_unnotified
40
+ ON submissions (created_at) WHERE notified_at IS NULL;
@@ -0,0 +1,41 @@
1
+ # D1 migrations
2
+
3
+ Sequential SQL files applied to the site's Cloudflare D1 database. Empty until a site needs
4
+ private data — form submissions, user accounts, anything that is not public page content.
5
+
6
+ ## Create one
7
+
8
+ ```sh
9
+ npx wrangler d1 migrations create <DATABASE_NAME> "add submissions table"
10
+ ```
11
+
12
+ That writes `0001_add_submissions_table.sql` here. Numbers are sequential and must not be
13
+ reordered or renamed after they have been applied anywhere.
14
+
15
+ ## Apply
16
+
17
+ ```sh
18
+ npx wrangler d1 migrations apply <DATABASE_NAME> --local # local dev database
19
+ npx wrangler d1 migrations apply <DATABASE_NAME> --remote # PRODUCTION
20
+ ```
21
+
22
+ `--remote` is the one people forget. Applying locally does nothing to production, and a
23
+ deploy does not run migrations for you.
24
+
25
+ ## Check what has been applied
26
+
27
+ ```sh
28
+ npx wrangler d1 migrations list <DATABASE_NAME> --remote
29
+ ```
30
+
31
+ Wrangler tracks applied migrations in a `d1_migrations` table inside the database itself.
32
+
33
+ ## Before any of this works
34
+
35
+ The database must exist and be bound in `wrangler.jsonc`:
36
+
37
+ ```sh
38
+ npx wrangler d1 create webm-<domain-dashed>-db --update-config
39
+ ```
40
+
41
+ `--update-config` writes the `d1_databases` binding into `wrangler.jsonc` for you.
Binary file
@@ -0,0 +1,4 @@
1
+ <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1024" height="1024" fill="#0251FF"/>
3
+ <path d="M622.502 173.056H438.162C437.039 173.056 435.928 173.29 434.901 173.745C433.874 174.199 432.954 174.864 432.198 175.695L295.152 326.527C292.967 328.933 291.757 332.066 291.757 335.317C291.757 338.567 292.967 341.701 295.152 344.106L397.175 456.396C397.383 456.626 397.637 456.809 397.921 456.935C398.204 457.06 398.51 457.125 398.82 457.125C399.13 457.125 399.436 457.06 399.72 456.935C400.003 456.809 400.257 456.626 400.465 456.396L634.093 199.269C636.135 197.023 637.48 194.231 637.966 191.234C638.451 188.237 638.056 185.163 636.829 182.386C635.601 179.61 633.593 177.25 631.05 175.593C628.507 173.937 625.537 173.055 622.502 173.056ZM156.996 494.4L294.041 645.232C294.797 646.064 295.717 646.728 296.745 647.183C297.772 647.637 298.882 647.872 300.005 647.872H484.346C487.38 647.872 490.35 646.991 492.893 645.334C495.436 643.678 497.444 641.318 498.672 638.541C499.9 635.764 500.295 632.691 499.809 629.693C499.323 626.696 497.978 623.905 495.936 621.658L262.309 364.531C262.1 364.302 261.846 364.118 261.563 363.993C261.28 363.867 260.973 363.803 260.663 363.803C260.354 363.803 260.047 363.867 259.764 363.993C259.481 364.118 259.227 364.302 259.018 364.531L156.996 476.822C154.811 479.227 153.6 482.361 153.6 485.611C153.6 488.861 154.811 491.995 156.996 494.4ZM401.497 850.944H585.838C586.961 850.944 588.072 850.709 589.099 850.255C590.126 849.8 591.046 849.136 591.802 848.305L728.847 697.465C731.033 695.06 732.243 691.926 732.243 688.676C732.243 685.426 731.033 682.292 728.847 679.887L626.825 567.604C626.617 567.374 626.363 567.191 626.079 567.065C625.796 566.94 625.49 566.875 625.18 566.875C624.87 566.875 624.563 566.94 624.28 567.065C623.997 567.191 623.743 567.374 623.535 567.604L389.907 824.731C387.865 826.977 386.52 829.769 386.034 832.766C385.548 835.763 385.944 838.837 387.171 841.613C388.399 844.39 390.407 846.75 392.95 848.407C395.493 850.063 398.463 850.945 401.497 850.944ZM867.004 529.6L729.958 378.76C729.203 377.93 728.282 377.267 727.255 376.814C726.228 376.361 725.117 376.127 723.995 376.128H539.654C536.621 376.129 533.653 377.011 531.111 378.667C528.569 380.323 526.563 382.682 525.335 385.457C524.108 388.233 523.712 391.305 524.197 394.301C524.681 397.297 526.024 400.088 528.064 402.334L761.691 659.461C761.9 659.691 762.154 659.874 762.437 660C762.72 660.125 763.027 660.19 763.336 660.19C763.646 660.19 763.953 660.125 764.236 660C764.519 659.874 764.773 659.691 764.982 659.461L867.004 547.178C869.189 544.773 870.4 541.639 870.4 538.389C870.4 535.139 869.189 532.005 867.004 529.6Z" fill="white"/>
4
+ </svg>
Binary file
@@ -0,0 +1,72 @@
1
+ // Preinstall guard. Runs BEFORE node_modules exists, so it may import nothing but
2
+ // node: builtins. Keep it dependency-free forever.
3
+ //
4
+ // Two checks, in the order that gives the most actionable message first:
5
+ //
6
+ // 1. Package manager. npm only. A stray `pnpm install` writes pnpm-lock.yaml beside the
7
+ // committed package-lock.json, and from then on the two disagree about the tree.
8
+ // 2. Node major, against .nvmrc. Warns locally — a newer major is fine for day-to-day
9
+ // work and CLAUDE.md says so — but fails hard in CI, where building on a major we do
10
+ // not ship is not a warning-level event.
11
+ //
12
+ // .nvmrc is the single source of truth. There is no volta key, no packageManager field,
13
+ // and `engines` deliberately stays wider (>=22.18.0) because it describes what the code
14
+ // requires, not what we develop on.
15
+
16
+ import { readFileSync } from 'node:fs';
17
+
18
+ const strict = Boolean(process.env.CI || process.env.STRICT_NODE);
19
+
20
+ // --- 1. npm only -------------------------------------------------------------------
21
+
22
+ // Set by every package manager that runs a lifecycle script; absent when this file is run
23
+ // directly with `node scripts/check-node.mjs`, which stays allowed.
24
+ const agent = process.env.npm_config_user_agent ?? '';
25
+ const manager = agent.split('/')[0];
26
+
27
+ if (manager && manager !== 'npm') {
28
+ console.error(
29
+ `\nThis project is npm only. Detected ${manager}.\n` +
30
+ ` package-lock.json is committed; a ${manager} install writes a second, competing\n` +
31
+ ` lockfile and the two drift apart on the next dependency change.\n` +
32
+ ` ${manager} resolves before it runs this check, so clean up after it:\n` +
33
+ ` rm -rf node_modules pnpm-lock.yaml yarn.lock bun.lock && npm install\n`,
34
+ );
35
+ process.exit(1);
36
+ }
37
+
38
+ // --- 2. Node major matches .nvmrc --------------------------------------------------
39
+
40
+ let pinned;
41
+ try {
42
+ pinned = readFileSync(new URL('../.nvmrc', import.meta.url), 'utf8').trim();
43
+ } catch {
44
+ // No pin in this checkout - nothing to enforce.
45
+ }
46
+
47
+ if (pinned) {
48
+ const want = pinned.replace(/^v/, '').split('.')[0];
49
+ const have = process.versions.node.split('.')[0];
50
+
51
+ if (want !== have) {
52
+ const summary = `Node ${process.versions.node} but .nvmrc pins ${pinned}, the major Workers Builds uses.`;
53
+
54
+ if (strict) {
55
+ console.error(
56
+ `\n${summary}\n` +
57
+ ` Workers Builds reads .nvmrc on its own. If this fired there, a NODE_VERSION\n` +
58
+ ` build variable is overriding it: Settings > Build > Build Variables and Secrets.\n` +
59
+ ` Locally: unset CI and STRICT_NODE to downgrade this back to a warning.\n`,
60
+ );
61
+ process.exit(1);
62
+ }
63
+
64
+ console.warn(
65
+ `\n⚠️ ${summary}\n` +
66
+ ` Switch with: nvm use (fnm and mise read .nvmrc too — see README)\n` +
67
+ ` No version manager at all? README "Node version" is a one-time setup.\n` +
68
+ ` Continuing. Day-to-day work on a newer major is fine, but reproduce CI\n` +
69
+ ` failures on ${want} before concluding they are not real.\n`,
70
+ );
71
+ }
72
+ }
@@ -0,0 +1,96 @@
1
+ /*
2
+ * Module hooks that let Node's built-in test runner load this codebase unmodified.
3
+ *
4
+ * Two things in `src/` are legal for Astro and Vite but not for plain Node ESM, and both
5
+ * would otherwise force a choice between "no tests" and "rewrite every import in the app to
6
+ * suit the test runner". The app wins that argument, so the adaptation lives here.
7
+ *
8
+ * 1. EXTENSIONLESS RELATIVE IMPORTS — `from '../credits/credit'`. Node ESM requires the
9
+ * extension; bundlers resolve it. The resolve hook appends `.ts` when the bare specifier
10
+ * does not resolve but the `.ts` file exists.
11
+ *
12
+ * 2. JSON IMPORTED WITHOUT AN ATTRIBUTE — `import site from '../../webmonterey.json'`.
13
+ * Node needs `with { type: 'json' }`; adding that to the source would be noise for a
14
+ * requirement no other toolchain in this project has. The load hook serves the file as
15
+ * an ES module with a default export instead.
16
+ *
17
+ * 3. TYPESCRIPT UNDER node_modules. The framework package ships .ts source, and Node REFUSES
18
+ * to strip types from anything under node_modules - ERR_UNSUPPORTED_NODE_MODULES_TYPE_
19
+ * STRIPPING, with no flag to change it. So a site test that imports a package module dies
20
+ * before it runs, and every test in that FILE dies with it, which reads as the test being
21
+ * broken rather than the loader. The load hook strips the types itself with
22
+ * `stripTypeScriptTypes`, which has no such restriction.
23
+ *
24
+ * `registerHooks`, NOT `register`: the older `register()` is deprecated, and it also runs
25
+ * hooks on a separate thread, which this does not need. These are synchronous and in-process.
26
+ *
27
+ * Dependency-free and test-only — loaded via `--import` from the `test` script and nowhere
28
+ * else. Nothing here runs in a build or in a Worker.
29
+ */
30
+ import { readFileSync, existsSync } from 'node:fs';
31
+ import { registerHooks, stripTypeScriptTypes } from 'node:module';
32
+ import { fileURLToPath } from 'node:url';
33
+
34
+ registerHooks({
35
+ resolve(specifier, context, nextResolve) {
36
+ const relative = specifier.startsWith('./') || specifier.startsWith('../');
37
+ const hasExtension = /\.[a-z0-9]+$/i.test(specifier);
38
+
39
+ if (relative && !hasExtension && context.parentURL) {
40
+ const candidate = new URL(`${specifier}.ts`, context.parentURL);
41
+ if (existsSync(fileURLToPath(candidate))) {
42
+ /*
43
+ * No `format` here. Setting it to 'module' would tell Node the file is plain
44
+ * JavaScript and skip its built-in type stripping, so the first `export type` in the
45
+ * target is a SyntaxError. Leaving it unset lets Node infer TypeScript from the
46
+ * extension.
47
+ */
48
+ return { url: candidate.href, shortCircuit: true };
49
+ }
50
+ }
51
+
52
+ return nextResolve(specifier, context);
53
+ },
54
+
55
+ load(url, context, nextLoad) {
56
+ if (url.endsWith('.json')) {
57
+ /*
58
+ * Parse then re-serialize, rather than inlining the raw text: the file is embedded into
59
+ * a module body here, and a lone `</script>` or an unescaped line separator in it would
60
+ * otherwise be a syntax error in the generated source.
61
+ */
62
+ const raw = readFileSync(fileURLToPath(url), 'utf8');
63
+ return {
64
+ format: 'module',
65
+ shortCircuit: true,
66
+ source: `export default ${JSON.stringify(JSON.parse(raw))};`,
67
+ };
68
+ }
69
+
70
+ return nextLoad(url, context);
71
+ },
72
+ });
73
+
74
+ /*
75
+ * TypeScript under node_modules. Node's own stripper refuses these; this one does not.
76
+ *
77
+ * STRIPPED UNCONDITIONALLY, not as a fallback. The obvious shape is to call nextLoad and catch
78
+ * ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING - and it never fires, because the throw happens
79
+ * during TRANSLATION, after the load hook has already returned its result. There is nothing to
80
+ * catch at the point you can catch it.
81
+ *
82
+ * Narrow on purpose: only .ts, only under node_modules. A site's own .ts is handled by Node
83
+ * directly and is not touched here.
84
+ */
85
+ registerHooks({
86
+ load(url, context, nextLoad) {
87
+ if (!url.endsWith('.ts') || !url.includes('/node_modules/')) return nextLoad(url, context);
88
+
89
+ const source = readFileSync(fileURLToPath(url), 'utf8');
90
+ return {
91
+ format: 'module',
92
+ shortCircuit: true,
93
+ source: stripTypeScriptTypes(source, { mode: 'strip', sourceUrl: url }),
94
+ };
95
+ },
96
+ });
@@ -0,0 +1,22 @@
1
+ # https://editorconfig.org
2
+ # Matches .prettierrc.json — keep the two in sync if either changes.
3
+ root = true
4
+
5
+ [*]
6
+ charset = utf-8
7
+ end_of_line = lf
8
+ indent_style = space
9
+ indent_size = 2
10
+ insert_final_newline = true
11
+ trim_trailing_whitespace = true
12
+ max_line_length = 100
13
+
14
+ [*.md]
15
+ # Trailing double-space is a hard line break in Markdown.
16
+ trim_trailing_whitespace = false
17
+
18
+ [*.{sql,sh}]
19
+ indent_size = 2
20
+
21
+ [Makefile]
22
+ indent_style = tab
@@ -0,0 +1,15 @@
1
+ # build output and generated files
2
+ dist/
3
+ .astro/
4
+ .wrangler/
5
+ worker-configuration.d.ts
6
+
7
+ # dependencies and lockfiles
8
+ node_modules/
9
+ package-lock.json
10
+
11
+ # copied verbatim to the build — leave byte-identical
12
+ public/
13
+
14
+ # not ours to format
15
+ .git/