create-website-build-kit 0.1.7 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-website-build-kit",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Scaffold a production marketing site \u2014 Astro on Cloudflare Workers, with the gates, the migration playbook and the accessibility work already wired.",
5
5
  "keywords": [
6
6
  "astro",
@@ -11,6 +11,7 @@ npm run media # after adding or replacing any ima
11
11
  npm run build:staging && npx wrangler dev # localhost:8788, real KV + secrets
12
12
  npm run a11y # accessibility check, one URL per family
13
13
  npm run tells # what is undecided, and the design tells
14
+ npm run check:copy # author notes that reached the rendered page
14
15
  npm run recon -- https://old-site.com # inventory the old site BEFORE designing routes
15
16
  npm run dns -- old-site.com # capture the zone. MX loss kills client email
16
17
  npm run seo -- https://old-site.com # optional: SEO baseline to diff after cutover
@@ -93,6 +94,14 @@ The rules whose failure looks like success — double-counted pageviews, a conve
93
94
  two pipes, a trigger on `sent=1` that catches almost nothing — are in `docs/analytics.md`.
94
95
  Read it before adding any tag.
95
96
 
97
+ **Notes to yourself never ship.** `check:copy` reads the text a browser would show — not the
98
+ source, not comments, not `<script>` — and looks for the markers people actually leave: `TODO`,
99
+ `FIXME`, `⚠ CONFIRM:`, `Lorem ipsum`, an unrendered `{{ placeholder }}`. It warns on staging and
100
+ **refuses on production**, because a note is normal while building and unacceptable at go-live.
101
+ One shipped as body copy on a service page — *"⚠ CONFIRM: the old site advertised classes every
102
+ Saturday at 9am…"* — past clean types, clean axe and clean tells. **The question is usually real:
103
+ move it to `BUILD-STATE.md`, do not just delete it.**
104
+
96
105
  **Evidence, not assertions.** `npm run a11y:evidence` writes a dated pack to
97
106
  `docs/a11y-evidence/` — commit it. It fills in the machine half and leaves the keyboard,
98
107
  screen-reader and forms passes **blank**, because no tool does those; a pack with them still
@@ -287,6 +287,7 @@ degrades into. Run the script; keep the commands below for when you need to see
287
287
  | --- | --- |
288
288
  | One pageview per visit | Only visible in analytics Realtime |
289
289
  | A valid submission stores **and** emails | Sending one creates a real lead and a real notification |
290
+ | **Data the client may not hold is dropped** | Only if `compliance.md` §10 named something — PHI, GDPR special categories, card numbers. Craft a submission containing exactly it, send it, then read the stored record back. The form not *asking* is not the control; the message box accepts anything. `verify` cannot do this: it would need the export token and the record schema |
290
291
  | The analytics container is the client's own | Fetch `gtm.js` and read it — see `analytics.md` |
291
292
  | A redirect lands on the **right** page | The script proves it resolves, not that it is correct |
292
293
  | It looks right on a phone | — |
@@ -490,16 +491,27 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
490
491
  3. **Move Search Console verification to DNS TXT** if it currently relies on an HTML file, and
491
492
  confirm it still shows verified. File-based verification breaks the moment the file stops
492
493
  resolving, and losing verification loses the property's history.
493
- 4. **Full verification matrix against staging.** Every row.
494
- 5. **Deploy production** `npm run deploy:production`. Read the bindings table in the output;
494
+ 4. **Name what enforces every retention period the privacy notice states.** KV is done for you —
495
+ `leadRetentionDays` becomes an `expirationTtl` and the store enforces it. **Anything else is
496
+ not.** R2 keeps uploaded files forever unless the bucket has a **lifecycle rule**, and that
497
+ rule lives in the Cloudflare dashboard, so nothing in the repo and no gate here can see it.
498
+ D1 needs a scheduled delete you wrote.
499
+
500
+ ⚠ A notice claiming a period nothing enforces is a false statement, and it fails silently:
501
+ clean build, clean deploy, correct-looking policy, data still there a year later. Caught on a
502
+ real build where résumés went to R2 while the retention value drove only the KV record and the
503
+ page copy. If you cannot name the mechanism, build it or change the notice —
504
+ `stacks.md` §6.
505
+ 5. **Full verification matrix against staging.** Every row.
506
+ 6. **Deploy production** — `npm run deploy:production`. Read the bindings table in the output;
495
507
  it is the only visible signal that the right environment was built.
496
- 6. **Cut DNS over.** Watch, do not assume:
508
+ 7. **Cut DNS over.** Watch, do not assume:
497
509
  ```bash
498
510
  dig +short $PROD; dig +short www.$PROD
499
511
  curl -sI "https://$PROD/" | head -3
500
512
  ```
501
- 7. **Remove the staging route** from the worker, or staging becomes an indexable duplicate.
502
- 8. **Re-run the matrix against production**, including `robots.txt` (must now allow) and
513
+ 8. **Remove the staging route** from the worker, or staging becomes an indexable duplicate.
514
+ 9. **Re-run the matrix against production**, including `robots.txt` (must now allow) and
503
515
  `noindex` (must now be absent).
504
516
 
505
517
  ```bash
@@ -511,7 +523,7 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
511
523
  `check:secrets` already ran as part of `deploy:production`. Run it again here because
512
524
  go-live is when it is most likely to fail: a secret set on the staging worker is not
513
525
  automatically on this one, and the failure is silent — leads store, nothing emails.
514
- 9. **Submit the sitemap** in [Search Console](https://search.google.com/search-console) and
526
+ 10. **Submit the sitemap** in [Search Console](https://search.google.com/search-console) and
515
527
  [Bing Webmaster Tools](https://www.bing.com/webmasters). If you kept the old filename, the
516
528
  existing entry keeps working and there is nothing to resubmit.
517
529
 
@@ -526,7 +538,7 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
526
538
  submission and rejects the whole batch with a 403 if it is not live yet. And it is Bing,
527
539
  Yandex, Seznam and Naver: **Google does not participate**, so this is never the reason a
528
540
  page is or is not in Google. The script prints both of those on every run.
529
- 10. **Diff the zone against the capture.** The one step that catches a launch taking the
541
+ 11. **Diff the zone against the capture.** The one step that catches a launch taking the
530
542
  client's email with it:
531
543
 
532
544
  ```bash
@@ -536,10 +548,10 @@ In this order. Steps 1–3 happen days ahead, not on launch day.
536
548
  MX, SPF, DMARC, verification TXT and nameservers, compared against what was published
537
549
  before you touched anything. A dead site gets a phone call; dead email is silent.
538
550
 
539
- 11. **Send one real enquiry through the live form** and confirm the client received it in the
551
+ 12. **Send one real enquiry through the live form** and confirm the client received it in the
540
552
  inbox they actually read.
541
- 12. **Restore the DNS TTL** to something sane (3600s).
542
- 13. **Point the uptime monitor at a real page and the form endpoint** — not just the homepage.
553
+ 13. **Restore the DNS TTL** to something sane (3600s).
554
+ 14. **Point the uptime monitor at a real page and the form endpoint** — not just the homepage.
543
555
  The endpoint is what breaks.
544
556
 
545
557
  ---
@@ -34,7 +34,8 @@
34
34
  "preview": "wrangler dev",
35
35
  "deploy:staging": "npm run build:staging && wrangler deploy && node scripts/check-secrets.mjs",
36
36
  "deploy:production": "npm run build:production && wrangler deploy && node scripts/check-secrets.mjs",
37
- "check:secrets": "node scripts/check-secrets.mjs"
37
+ "check:secrets": "node scripts/check-secrets.mjs",
38
+ "check:copy": "node scripts/check-copy.mjs"
38
39
  },
39
40
  "dependencies": {
40
41
  "@astrojs/cloudflare": "^14.1.7",
@@ -102,6 +102,11 @@ if (env === 'staging') {
102
102
  step(process.execPath, ['scripts/staging-headers.mjs']);
103
103
  }
104
104
 
105
+ /* Warn on staging, refuse on production. A note in the copy is normal WHILE
106
+ building and unacceptable at go-live — the same split as
107
+ `tells --undecided-only`. */
108
+ step(process.execPath, ['scripts/check-copy.mjs', ...(env === 'production' ? ['--strict'] : [])]);
109
+
105
110
  step(process.execPath, ['scripts/check-env.mjs']);
106
111
 
107
112
  if (env === 'production') {
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Refuse to ship a build whose visible copy still talks to the author.
3
+ *
4
+ * node scripts/check-copy.mjs # warn
5
+ * node scripts/check-copy.mjs --strict # exit 1 on any marker
6
+ *
7
+ * `build:production` runs it strict; `build:staging` runs it as a warning,
8
+ * because a note in the copy is normal WHILE building and unacceptable at
9
+ * go-live. Same split as `tells --undecided-only`.
10
+ *
11
+ * ── THE FAILURE THIS EXISTS FOR ────────────────────────────────────────────
12
+ * A real build shipped this, on a service page, as body text a parent would
13
+ * read:
14
+ *
15
+ * "⚠ CONFIRM: the old site advertised classes every Saturday at 9am. Does
16
+ * this continue under the concierge model? Emitting a class time nobody is
17
+ * running is a locked door."
18
+ *
19
+ * It was written inline in the content file while drafting, and every gate
20
+ * passed over it: `astro check` clean, axe clean, `tells` clean, links fine.
21
+ * Nothing in a build can tell a sentence meant for the client from a sentence
22
+ * meant for the reader — except a list of the markers people actually use.
23
+ *
24
+ * ⚠ THE QUESTION IS USUALLY REAL. The fix is not to delete the note, it is to
25
+ * move it to `BUILD-STATE.md` where the other open questions live. A build
26
+ * record is read before go-live; a paragraph on a service page is read by a
27
+ * customer.
28
+ *
29
+ * ── WHY THIS LOOKS AT RENDERED TEXT, NOT THE FILE ──────────────────────────
30
+ * Searching source would flag every code comment that mentions TODO, and
31
+ * searching raw HTML would flag markers inside `<script>` and inside HTML
32
+ * comments — which are invisible to a reader and often deliberate. The check
33
+ * runs on the text a browser would show, plus JSON-LD, because a placeholder in
34
+ * structured data is quoted straight back by Google.
35
+ *
36
+ * `check-sitemap.mjs` carries the same lesson in the opposite direction: match
37
+ * the thing, not the word, or you train people to ignore the check.
38
+ */
39
+
40
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
41
+ import { join, relative, sep } from 'node:path';
42
+
43
+ const RESET = '\x1b[0m';
44
+ const RED = '\x1b[31m';
45
+ const GREEN = '\x1b[32m';
46
+ const YELLOW = '\x1b[33m';
47
+ const DIM = '\x1b[2m';
48
+
49
+ const strict = process.argv.includes('--strict');
50
+
51
+ const root = ['dist/client', 'dist'].find((d) => existsSync(d));
52
+ if (!root) {
53
+ console.error('check-copy: no dist/ — run a build first.');
54
+ process.exit(1);
55
+ }
56
+
57
+ /*
58
+ * ⚠ EVERY PATTERN HERE IS DELIBERATELY NARROW.
59
+ *
60
+ * `TODO` in caps is never ordinary prose. Lowercase "todo" is a Spanish word
61
+ * and an English noun. `CONFIRM` needs its colon or its warning sign, because
62
+ * "confirm your email address" is a real sentence a form says. Bare `TK` is
63
+ * excluded entirely — two capitals appear inside acronyms and product names,
64
+ * and a check that fires on those is a check people switch off.
65
+ */
66
+ const MARKERS = [
67
+ { name: 'TODO', re: /\bTODO\b/ },
68
+ { name: 'FIXME', re: /\bFIXME\b/ },
69
+ { name: 'XXX', re: /\bXXX\b/ },
70
+ { name: 'TKTK', re: /\bTKTK\b/ },
71
+ { name: 'CONFIRM:', re: /(?:⚠\s*)?\bCONFIRM\s*:/ },
72
+ { name: 'placeholder text', re: /\bLorem ipsum\b/i },
73
+ { name: 'unresolved template', re: /\{\{\s*[\w.]+\s*\}\}/ },
74
+ ];
75
+
76
+ const walk = (dir) =>
77
+ readdirSync(dir).flatMap((e) => {
78
+ const full = join(dir, e);
79
+ return statSync(full).isDirectory() ? walk(full) : [full];
80
+ });
81
+
82
+ /** What a reader actually sees, plus structured data. Never script or comments. */
83
+ function visibleText(html) {
84
+ const jsonLd = [...html.matchAll(/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)]
85
+ .map((m) => m[1])
86
+ .join('\n');
87
+
88
+ const body = html
89
+ .replace(/<!--[\s\S]*?-->/g, ' ')
90
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
91
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
92
+ .replace(/<[^>]+>/g, ' ');
93
+
94
+ return `${body}\n${jsonLd}`;
95
+ }
96
+
97
+ const hits = [];
98
+
99
+ for (const file of walk(root).filter((f) => f.endsWith('.html'))) {
100
+ const text = visibleText(readFileSync(file, 'utf8'));
101
+ for (const { name, re } of MARKERS) {
102
+ if (!re.test(text)) continue;
103
+ /* Show the sentence, so the reader can judge it without opening the file. */
104
+ const at = text.search(re);
105
+ const context = text
106
+ .slice(Math.max(0, at - 60), at + 120)
107
+ .replace(/\s+/g, ' ')
108
+ .trim();
109
+ hits.push({
110
+ route: '/' + relative(root, file).split(sep).join('/').replace(/index\.html$/, ''),
111
+ name,
112
+ context,
113
+ });
114
+ }
115
+ }
116
+
117
+ if (!hits.length) {
118
+ console.log(`${GREEN}✓${RESET} no author notes in the rendered copy`);
119
+ process.exit(0);
120
+ }
121
+
122
+ const label = strict ? `${RED}✗${RESET}` : `${YELLOW}!${RESET}`;
123
+ console[strict ? 'error' : 'log'](
124
+ `\n${label} ${hits.length} author marker(s) in text a reader would see\n`,
125
+ );
126
+ for (const h of hits) {
127
+ console[strict ? 'error' : 'log'](` ${h.route} ${DIM}[${h.name}]${RESET}\n …${h.context}…`);
128
+ }
129
+
130
+ console[strict ? 'error' : 'log'](
131
+ `\n ${DIM}The question is usually real. Move it to BUILD-STATE.md, where the other\n` +
132
+ ` open questions live and someone reads it before go-live — do not just delete it.${RESET}\n`,
133
+ );
134
+
135
+ process.exit(strict ? 1 : 0);
@@ -313,6 +313,56 @@ tell(
313
313
  );
314
314
  }
315
315
 
316
+ /*
317
+ * Two rows adopted after running pbakaus/impeccable's detector over a real
318
+ * build. Both are signals `tells` did not have; both needed an exclusion its
319
+ * version does not have, and the exclusions are the reason they are usable.
320
+ */
321
+
322
+ /*
323
+ * "bounce and overshoot easing" — a cubic-bezier whose control points leave
324
+ * [0,1] on the y axis overshoots the target and springs back.
325
+ *
326
+ * ⚠ NOT ALWAYS WRONG, which is why it is a tell and not an error. Overshoot is
327
+ * right when the gesture itself carried momentum — a flick, a drag release.
328
+ * It is wrong on a menu that faded in, where nothing threw it.
329
+ */
330
+ {
331
+ const overshoot = [...sourceCss.matchAll(/cubic-bezier\(([^)]+)\)/g)]
332
+ .map((m) => m[1].split(',').map((n) => Number(n.trim())))
333
+ .filter((p) => p.length === 4 && p.every((n) => Number.isFinite(n)))
334
+ .filter(([, y1, , y2]) => y1 > 1 || y2 > 1 || y1 < 0 || y2 < 0);
335
+ tell(
336
+ 'bounce or overshoot easing',
337
+ overshoot.length > 0,
338
+ `${overshoot.length} easing curve(s) overshoot. Right after a flick or a drag release, where momentum came from the gesture; dated on anything that simply appeared.`,
339
+ );
340
+ }
341
+
342
+ /*
343
+ * "a thick accent bar down one side of a card" — border-inline-start (or
344
+ * border-left) at 3px or more.
345
+ *
346
+ * ⚠ BLOCKQUOTES ARE EXCLUDED, AND THAT EXCLUSION IS THE WHOLE ROW. A rule
347
+ * flagged `.prose blockquote { border-inline-start: 3px solid }` on a real
348
+ * build — a left rule on a quotation is a typographic convention older than
349
+ * the web, and reporting it would teach people to skim past the row. The
350
+ * pattern actually meant is a notice or card wearing a coloured tab.
351
+ */
352
+ {
353
+ const bars = [...sourceCss.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
354
+ .filter(([, selector]) => !/blockquote|\bquote\b|\bcite\b/i.test(selector))
355
+ .filter(([, , body]) => {
356
+ const m = /border-(?:inline-start|left):\s*([\d.]+)px\s+solid/.exec(body);
357
+ return m && Number(m[1]) >= 3;
358
+ });
359
+ tell(
360
+ 'a thick accent bar down one side',
361
+ bars.length > 1,
362
+ `${bars.length} rule(s) put a 3px+ coloured border on one edge. On a card or a notice it is one of the most recognisable generated-UI tells. Blockquotes are excluded — a rule beside a quotation is a convention, not a tell.`,
363
+ );
364
+ }
365
+
316
366
  // "the 404, the empty state or the form's invalid state was never designed"
317
367
  tell(
318
368
  'no invalid / busy form state',
@@ -29,7 +29,22 @@ const isCurrent = (href: string) => (href === '/' ? path === '/' : path.startsWi
29
29
  {/* Text until there is a logo. A brand mark referenced before the file
30
30
  exists renders as a broken image on every page, which is worse than
31
31
  plain type — swap this for <img> once the artwork is in place, and
32
- look at it on both a light and a dark background before shipping. */}
32
+ look at it on both a light and a dark background before shipping.
33
+
34
+ ⚠ SIZE IT BY HEIGHT, NOT WIDTH, AND KEEP IT UNDER --header-h.
35
+ `inline-size: 12rem; block-size: auto` on a wide mark computes to
36
+ whatever its aspect ratio says — on a real build a 520×227 logo came
37
+ out 87px tall in an 88px bar, so the LOGO was setting the header's
38
+ height instead of the token.
39
+
40
+ That is not cosmetic. FOUR offsets are computed from --header-h:
41
+ this header's min-block-size, `.under-header`'s reserve, and
42
+ scroll-padding-top / scroll-margin-top, which are what stop an
43
+ anchor target landing underneath the fixed nav. A header taller than
44
+ its token leaves all four short by the same amount, and none of them
45
+ errors.
46
+
47
+ `block-size: clamp(2.25rem, 3.4vw, 2.75rem); inline-size: auto` */}
33
48
  {business.name}
34
49
  </a>
35
50
 
@@ -29,7 +29,7 @@ const { title, lede, breadcrumbs, class: className = '' } = Astro.props;
29
29
  const crumbs = breadcrumbs ?? [];
30
30
  ---
31
31
 
32
- <section class:list={['hero', 'under-header', 'section--tight', className]}>
32
+ <section class:list={['hero', 'under-header', className]}>
33
33
  <div class="container">
34
34
  {
35
35
  crumbs.length > 0 && (
@@ -59,6 +59,31 @@ const crumbs = breadcrumbs ?? [];
59
59
  </section>
60
60
 
61
61
  <style>
62
+ /*
63
+ * ⚠ THE RHYTHM BELONGS TO THE NEXT SECTION, NOT TO THIS ONE.
64
+ *
65
+ * This carried `.section--tight`, which is a SHORTHAND — it sets padding-block
66
+ * at both ends. Every page opens its following section with a rhythm class of
67
+ * its own, so two stacked: a measured 160px hole on the template's own
68
+ * /contact/, 176px on all four PageHero pages of a real build, and up to 232px
69
+ * where the next section is `.section` rather than `.section--tight`.
70
+ *
71
+ * Nothing could see it. Build green, types green, axe green, tells green — a
72
+ * hole is only visible by looking at the page.
73
+ *
74
+ * ⚠ AND THE SHORTHAND CLOBBERED THE HEADER OFFSET. `.under-header` reserves
75
+ * the fixed header's height on padding-block-START, and global.css warns in
76
+ * as many words that "a scoped component style would out-specify" it. A
77
+ * shorthand from this component is exactly such a style: on a real build it
78
+ * discarded the reserve and put the hero behind the nav.
79
+ *
80
+ * padding-block-END only, therefore. The offset stays with `.under-header`,
81
+ * the rhythm stays with whatever section comes next.
82
+ */
83
+ .hero {
84
+ padding-block-end: 0;
85
+ }
86
+
62
87
  .hero__crumbs ol {
63
88
  display: flex;
64
89
  flex-wrap: wrap;
@@ -241,6 +241,16 @@ b {
241
241
  * height or the heading sits behind the nav. Applied by each opening section
242
242
  * rather than a blanket `main > :first-child` rule, which a scoped component
243
243
  * style would out-specify.
244
+ *
245
+ * ⚠ AND ONE DID. A component wrote `padding-block: <a> <b>` on the same element
246
+ * — a SHORTHAND, so it set padding-block-start too, out-specified this, and
247
+ * the hero sat behind the nav on a real build. On the element that carries
248
+ * `.under-header`, set `padding-block-end` and never the shorthand.
249
+ *
250
+ * ⚠ NOT MOVED TO `main`, WHICH WOULD BE UN-OVERRIDABLE. The header's background
251
+ * is opaque `var(--bg)`, so padding on `main` would leave a strip of body
252
+ * colour behind it wherever a first section has a background of its own. The
253
+ * per-section reserve is deliberate; the shorthand is the bug.
244
254
  */
245
255
  .under-header {
246
256
  padding-block-start: calc(var(--header-h) + var(--space-2xl));
@@ -189,9 +189,19 @@
189
189
  --dur-slow: 420ms;
190
190
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
191
191
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
192
- --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
192
+ /* ⚠ NO SPRING TOKEN, DELIBERATELY. An overshoot curve is a look, and the
193
+ template does not ship looks — it shipped one anyway, unused, until a
194
+ detector found it in a client project that had inherited the dead token
195
+ without ever referencing it. Add one where a gesture carries real
196
+ momentum, and only there: a flick or a drag release. Nothing that merely
197
+ appeared should spring. */
193
198
 
194
199
  /* ── Chrome ────────────────────────────────────────────────────────────── */
200
+ /* ⚠ FOUR THINGS READ THIS: the header's min-block-size, `.under-header`'s
201
+ reserve, and scroll-padding-top / scroll-margin-top, which keep an anchor
202
+ target clear of the fixed nav. If the rendered header is taller than this
203
+ — a logo sized by width is the usual cause — all four are short by the same
204
+ amount and nothing reports it. Measure the bar, do not assume it. */
195
205
  --header-h: 4.5rem;
196
206
  --z-header: 100;
197
207
  --z-menu: 110;
@@ -7,6 +7,21 @@
7
7
  // the Astro adapter resolves its environment at build time and silently
8
8
  // ignores `deploy --env`, which is how a staging deploy lands on production.
9
9
  //
10
+ // ⚠ AND IT IGNORES `--config` THE SAME WAY, which is the next flag anyone
11
+ // reaches for. @astrojs/cloudflare generates dist/server/wrangler.json —
12
+ // the config actually deployed, the one carrying `main` and the real asset
13
+ // directory — and builds it from the DEFAULT config path only. So
14
+ // `wrangler deploy --config wrangler.production.jsonc` either fails with
15
+ // "Cannot use assets with a binding in an assets-only Worker" (the
16
+ // hand-written file has no `main`), or, if you work around that by
17
+ // deploying the generated file instead, ships with whatever name and routes
18
+ // THIS file held, whatever was built.
19
+ //
20
+ // One config is the reason that cannot happen here. A project that grows a
21
+ // second one has to rewrite dist/server/wrangler.json between build and
22
+ // deploy, because there is no adapter option for it. Found on a shipped
23
+ // build that needed two.
24
+ //
10
25
  // npm run deploy:staging → builds staging, deploys this worker
11
26
  // npm run deploy:production → builds production, deploys this worker
12
27
  //