@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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/agents/.gitkeep +0 -0
- package/dist/webm.mjs +2381 -0
- package/hooks/.gitkeep +0 -0
- package/package.json +101 -0
- package/schema/design.json +118 -0
- package/skills/launch/SKILL.md +183 -0
- package/skills/new-component/SKILL.md +85 -0
- package/skills/start/SKILL.md +117 -0
- package/skills/traps/SKILL.md +333 -0
- package/skills/upgrade/SKILL.md +42 -0
- package/src/actions/index.ts +348 -0
- package/src/cli/checks.test.ts +711 -0
- package/src/cli/checks.ts +822 -0
- package/src/cli/codemods.test.ts +42 -0
- package/src/cli/codemods.ts +51 -0
- package/src/cli/compare.test.ts +144 -0
- package/src/cli/compare.ts +222 -0
- package/src/cli/design-extract.test.ts +96 -0
- package/src/cli/design-extract.ts +229 -0
- package/src/cli/doctor.ts +187 -0
- package/src/cli/mcp.test.ts +57 -0
- package/src/cli/mcp.ts +110 -0
- package/src/cli/new.ts +148 -0
- package/src/cli/package-root.ts +58 -0
- package/src/cli/scaffold.test.ts +230 -0
- package/src/cli/scaffold.ts +424 -0
- package/src/cli/seed.ts +133 -0
- package/src/cli/slug.test.ts +70 -0
- package/src/cli/slug.ts +109 -0
- package/src/cli/sync.test.ts +137 -0
- package/src/cli/sync.ts +266 -0
- package/src/cli/upgrade.ts +93 -0
- package/src/design/__fixtures__/tokens-v1.4.1.css +227 -0
- package/src/design/brand.ts +49 -0
- package/src/design/compile.test.ts +98 -0
- package/src/design/compile.ts +155 -0
- package/src/design/defaults.ts +315 -0
- package/src/design/index.ts +18 -0
- package/src/design/resolve.test.ts +80 -0
- package/src/design/resolve.ts +108 -0
- package/src/design/types.ts +96 -0
- package/src/emails/autoresponse.test.ts +82 -0
- package/src/emails/autoresponse.ts +83 -0
- package/src/emails/footer.test.ts +102 -0
- package/src/emails/footer.ts +91 -0
- package/src/emails/index.ts +24 -0
- package/src/emails/subject.test.ts +66 -0
- package/src/emails/subject.ts +80 -0
- package/src/emails/submission-notification.ts +76 -0
- package/src/env.d.ts +12 -0
- package/src/includes/cloudflare/d1/client.ts +86 -0
- package/src/includes/cloudflare/r2/README.md +68 -0
- package/src/includes/cloudflare/r2/media.ts +41 -0
- package/src/includes/cloudflare/r2/url.test.ts +44 -0
- package/src/includes/cloudflare/r2/url.ts +34 -0
- package/src/includes/cloudflare/turnstile/Turnstile.astro +161 -0
- package/src/includes/cloudflare/turnstile/verify.ts +157 -0
- package/src/includes/cloudflare/workers/env.ts +56 -0
- package/src/includes/google/tag-manager/TagManager.astro +73 -0
- package/src/includes/sinch/mailgun/redirect.test.ts +96 -0
- package/src/includes/sinch/mailgun/redirect.ts +96 -0
- package/src/includes/sinch/mailgun/send.ts +186 -0
- package/src/includes/webmonterey/animations/animations.css +347 -0
- package/src/includes/webmonterey/animations/observe.test.ts +88 -0
- package/src/includes/webmonterey/animations/observe.ts +209 -0
- package/src/includes/webmonterey/compliance/ConsentInit.astro +74 -0
- package/src/includes/webmonterey/compliance/CookieConsent.astro +558 -0
- package/src/includes/webmonterey/compliance/consent-styles.test.ts +83 -0
- package/src/includes/webmonterey/compliance/consent.ts +142 -0
- package/src/includes/webmonterey/config.test.ts +94 -0
- package/src/includes/webmonterey/config.ts +346 -0
- package/src/includes/webmonterey/copy-defaults.ts +148 -0
- package/src/includes/webmonterey/copy.ts +13 -0
- package/src/includes/webmonterey/credits/Credit.astro +80 -0
- package/src/includes/webmonterey/credits/credit.test.ts +111 -0
- package/src/includes/webmonterey/credits/credit.ts +59 -0
- package/src/includes/webmonterey/forms/honeypot.test.ts +40 -0
- package/src/includes/webmonterey/forms/honeypot.ts +66 -0
- package/src/includes/webmonterey/prose/inline.test.ts +94 -0
- package/src/includes/webmonterey/prose/inline.ts +71 -0
- package/src/includes/webmonterey/scroll-top/ScrollTop.astro +209 -0
- package/src/includes/webmonterey/site.ts +136 -0
- package/src/includes/webmonterey/structured-data/nodes.ts +315 -0
- package/src/includes/webmonterey/zoned-hour.test.ts +49 -0
- package/src/integration/adapter.ts +53 -0
- package/src/integration/app-middleware.ts +43 -0
- package/src/integration/config.ts +96 -0
- package/src/integration/content.ts +66 -0
- package/src/integration/image-size.test.ts +83 -0
- package/src/integration/image-size.ts +100 -0
- package/src/integration/index.ts +386 -0
- package/src/integration/virtual.d.ts +101 -0
- package/src/layouts/base.astro +402 -0
- package/src/package.test.ts +121 -0
- package/src/pages/404.astro +33 -0
- package/src/pages/[...slug].astro +126 -0
- package/src/pages/robots.txt.ts +41 -0
- package/src/pages/webm.astro +23 -0
- package/src/scripts/.gitkeep +0 -0
- package/src/styles/base.css +185 -0
- package/src/styles/global.css +47 -0
- package/src/styles/layers.test.ts +32 -0
- package/src/styles/layers.ts +21 -0
- package/src/styles/layout.css +235 -0
- package/src/styles/reset.css +135 -0
- package/src/styles/utilities.css +90 -0
- package/src/worker.ts +76 -0
- package/template/assets/logo.svg +14 -0
- package/template/assets/open-graph.png +0 -0
- package/template/migrations/0001_create_submissions.sql +40 -0
- package/template/migrations/README.md +41 -0
- package/template/public/android-chrome-192x192.png +0 -0
- package/template/public/android-chrome-512x512.png +0 -0
- package/template/public/apple-touch-icon.png +0 -0
- package/template/public/favicon-16x16.png +0 -0
- package/template/public/favicon-32x32.png +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/favicon.svg +4 -0
- package/template/public/open-graph.png +0 -0
- package/template/scripts/check-node.mjs +72 -0
- package/template/scripts/test-hooks.mjs +96 -0
- package/template/site/.editorconfig +22 -0
- package/template/site/.prettierignore +15 -0
- package/template/site/.prettierrc.json +17 -0
- package/template/site/CLAUDE.md +245 -0
- package/template/site/CONTENT.md +135 -0
- package/template/site/PRIVACY-POLICY-DRAFT.md +81 -0
- package/template/site/public/_headers +70 -0
- package/template/site/src/forms/contact.json +45 -0
- package/template/workflows/ci.yml +52 -0
package/dist/webm.mjs
ADDED
|
@@ -0,0 +1,2381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res, err) => function __init() {
|
|
5
|
+
if (err) throw err[0];
|
|
6
|
+
try {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw err = [e], e;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/cli/package-root.ts
|
|
18
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
function isOurs(manifest2) {
|
|
22
|
+
try {
|
|
23
|
+
const pkg2 = JSON.parse(readFileSync(manifest2, "utf8"));
|
|
24
|
+
return typeof pkg2.bin?.webm === "string";
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function findRoot() {
|
|
30
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
for (let i = 0; i < 8; i++) {
|
|
32
|
+
const manifest2 = join(dir, "package.json");
|
|
33
|
+
if (existsSync(manifest2) && isOurs(manifest2)) return dir;
|
|
34
|
+
const up = dirname(dir);
|
|
35
|
+
if (up === dir) break;
|
|
36
|
+
dir = up;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
`webm: cannot locate the package root from ${fileURLToPath(import.meta.url)}. This usually means the CLI was moved out of the installed package.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
function manifest() {
|
|
43
|
+
return JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
function packageVersion() {
|
|
46
|
+
return manifest().version;
|
|
47
|
+
}
|
|
48
|
+
function packageName() {
|
|
49
|
+
return manifest().name;
|
|
50
|
+
}
|
|
51
|
+
var PACKAGE_ROOT;
|
|
52
|
+
var init_package_root = __esm({
|
|
53
|
+
"src/cli/package-root.ts"() {
|
|
54
|
+
"use strict";
|
|
55
|
+
PACKAGE_ROOT = findRoot();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// src/cli/slug.ts
|
|
60
|
+
function normalizeDomain(input) {
|
|
61
|
+
const cleaned = input.trim().toLowerCase().replace(/^[a-z]+:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "").replace(/\.+$/, "");
|
|
62
|
+
if (!cleaned || !cleaned.includes(".")) {
|
|
63
|
+
throw new DomainError(`"${input}" is not a domain. Expected something like example.com.`);
|
|
64
|
+
}
|
|
65
|
+
if (!/^[a-z0-9.-]+$/.test(cleaned)) {
|
|
66
|
+
throw new DomainError(`"${input}" contains characters a hostname cannot.`);
|
|
67
|
+
}
|
|
68
|
+
return cleaned;
|
|
69
|
+
}
|
|
70
|
+
function repoName(domain) {
|
|
71
|
+
return normalizeDomain(domain).replace(/\./g, "_");
|
|
72
|
+
}
|
|
73
|
+
function slugFor(domain) {
|
|
74
|
+
const clean = normalizeDomain(domain);
|
|
75
|
+
const parts = clean.split(".");
|
|
76
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
77
|
+
const drop = TWO_PART_SUFFIXES.has(lastTwo) ? 2 : 1;
|
|
78
|
+
const kept = parts.slice(0, Math.max(1, parts.length - drop));
|
|
79
|
+
return kept.join("-");
|
|
80
|
+
}
|
|
81
|
+
function resourceNames(domain) {
|
|
82
|
+
const slug = slugFor(domain);
|
|
83
|
+
return {
|
|
84
|
+
slug,
|
|
85
|
+
repo: repoName(domain),
|
|
86
|
+
worker: `webm-${slug}`,
|
|
87
|
+
d1: `webm-${slug}-db`,
|
|
88
|
+
r2Media: `webm-${slug}-media`,
|
|
89
|
+
r2App: `webm-${slug}-app`
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var TWO_PART_SUFFIXES, DomainError;
|
|
93
|
+
var init_slug = __esm({
|
|
94
|
+
"src/cli/slug.ts"() {
|
|
95
|
+
"use strict";
|
|
96
|
+
TWO_PART_SUFFIXES = /* @__PURE__ */ new Set([
|
|
97
|
+
"co.uk",
|
|
98
|
+
"org.uk",
|
|
99
|
+
"ac.uk",
|
|
100
|
+
"gov.uk",
|
|
101
|
+
"me.uk",
|
|
102
|
+
"net.uk",
|
|
103
|
+
"sch.uk",
|
|
104
|
+
"com.au",
|
|
105
|
+
"net.au",
|
|
106
|
+
"org.au",
|
|
107
|
+
"edu.au",
|
|
108
|
+
"gov.au",
|
|
109
|
+
"co.nz",
|
|
110
|
+
"net.nz",
|
|
111
|
+
"org.nz",
|
|
112
|
+
"com.br",
|
|
113
|
+
"com.mx",
|
|
114
|
+
"co.za",
|
|
115
|
+
"co.jp",
|
|
116
|
+
"co.in",
|
|
117
|
+
"com.sg"
|
|
118
|
+
]);
|
|
119
|
+
DomainError = class extends Error {
|
|
120
|
+
constructor(message) {
|
|
121
|
+
super(message);
|
|
122
|
+
this.name = "DomainError";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// src/cli/mcp.ts
|
|
129
|
+
function mcpConfig() {
|
|
130
|
+
return {
|
|
131
|
+
mcpServers: Object.fromEntries(
|
|
132
|
+
Object.entries(MCP_SERVERS).map(([name, s]) => [name, { type: s.type, url: s.url }])
|
|
133
|
+
)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function mcpGaps(declared, enabled) {
|
|
137
|
+
const d = declared ?? {};
|
|
138
|
+
const e = new Set(enabled ?? []);
|
|
139
|
+
const undeclared = MCP_NAMES.filter((n) => !(n in d));
|
|
140
|
+
return {
|
|
141
|
+
undeclared,
|
|
142
|
+
unapproved: MCP_NAMES.filter((n) => n in d && !e.has(n)),
|
|
143
|
+
wrongUrl: MCP_NAMES.filter((n) => n in d && d[n]?.url !== MCP_SERVERS[n].url)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
var MCP_SERVERS, MCP_NAMES;
|
|
147
|
+
var init_mcp = __esm({
|
|
148
|
+
"src/cli/mcp.ts"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
MCP_SERVERS = {
|
|
151
|
+
/*
|
|
152
|
+
* Astro's own, from docs.astro.build/en/guides/build-with-ai. Free, open source, and the
|
|
153
|
+
* documented way to give a tool current Astro knowledge.
|
|
154
|
+
*/
|
|
155
|
+
"astro-docs": {
|
|
156
|
+
type: "http",
|
|
157
|
+
url: "https://mcp.docs.astro.build/mcp",
|
|
158
|
+
purpose: "Current Astro documentation. Consult before using any Astro API."
|
|
159
|
+
},
|
|
160
|
+
/*
|
|
161
|
+
* Mozilla's, from developer.mozilla.org/en-US/mcp. Search, reference and - the part no model
|
|
162
|
+
* has current - browser compatibility data.
|
|
163
|
+
*
|
|
164
|
+
* EXPERIMENTAL, AND MOZILLA SAYS SO: it may be withdrawn at any time, and queries are logged
|
|
165
|
+
* while the experiment runs. It is a docs lookup, so nothing confidential should reach it in
|
|
166
|
+
* the first place; the rule in CLAUDE.md is what keeps it that way. If it disappears, the
|
|
167
|
+
* failure is a session that cannot reach it, not a build that breaks.
|
|
168
|
+
*/
|
|
169
|
+
mdn: {
|
|
170
|
+
type: "http",
|
|
171
|
+
url: "https://mcp.mdn.mozilla.net/",
|
|
172
|
+
purpose: "MDN reference and browser-compat data. Consult before using a web platform API."
|
|
173
|
+
},
|
|
174
|
+
/*
|
|
175
|
+
* The Website Specification and The Email Specification - Joost de Valk and contributors,
|
|
176
|
+
* MIT/CC BY. Search, per-topic pages and, the useful part here, generated checklists across
|
|
177
|
+
* SEO, accessibility, performance, privacy, security and i18n.
|
|
178
|
+
*
|
|
179
|
+
* GUIDANCE, NOT A STANDARD, WHATEVER THE NAME SAYS. These are opinionated best-practice
|
|
180
|
+
* documents, not normative specs, and they are at 0.2.0 and 0.1.0. They must never outrank MDN
|
|
181
|
+
* or the Astro docs on what an API does; they answer a different question - what a good site or
|
|
182
|
+
* a good email does. CLAUDE.md states that precedence, because a server called
|
|
183
|
+
* "specification" invites exactly the wrong assumption.
|
|
184
|
+
*/
|
|
185
|
+
"website-spec": {
|
|
186
|
+
type: "http",
|
|
187
|
+
url: "https://mcp.specification.website/mcp",
|
|
188
|
+
purpose: "Site-quality guidance: SEO, accessibility, performance, privacy, security, i18n."
|
|
189
|
+
},
|
|
190
|
+
/*
|
|
191
|
+
* Email is a SEPARATE server from the website one - the Website Specification excludes it by
|
|
192
|
+
* design. It earns its place because src/emails/ is where recall is worst: client rendering
|
|
193
|
+
* quirks and deliverability rules move, and a template that looks right in a browser is not
|
|
194
|
+
* evidence of anything.
|
|
195
|
+
*/
|
|
196
|
+
"email-spec": {
|
|
197
|
+
type: "http",
|
|
198
|
+
url: "https://mcp.specification.email/mcp",
|
|
199
|
+
purpose: "Email guidance: rendering, deliverability, accessibility. For src/emails/ work."
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
MCP_NAMES = Object.keys(MCP_SERVERS);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// src/cli/scaffold.ts
|
|
207
|
+
function shiftDays(date, days) {
|
|
208
|
+
const shifted = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
209
|
+
shifted.setUTCDate(shifted.getUTCDate() + days);
|
|
210
|
+
return shifted.toISOString().slice(0, 10);
|
|
211
|
+
}
|
|
212
|
+
function scaffold(options) {
|
|
213
|
+
const { domain, packageVersion: packageVersion2 } = options;
|
|
214
|
+
const n = resourceNames(domain);
|
|
215
|
+
const client = options.client ?? "CHANGEME";
|
|
216
|
+
const org = options.org ?? "webmonterey";
|
|
217
|
+
const { today } = options;
|
|
218
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(today)) {
|
|
219
|
+
throw new Error(`scaffold: today must be YYYY-MM-DD, got ${JSON.stringify(today)}`);
|
|
220
|
+
}
|
|
221
|
+
const RUNTIME_LAG_DAYS = 14;
|
|
222
|
+
const compatibilityDate = shiftDays(today, -RUNTIME_LAG_DAYS);
|
|
223
|
+
const files = {};
|
|
224
|
+
files["package.json"] = JSON.stringify(
|
|
225
|
+
{
|
|
226
|
+
name: n.worker,
|
|
227
|
+
private: true,
|
|
228
|
+
type: "module",
|
|
229
|
+
engines: { node: ">=22.18.0" },
|
|
230
|
+
scripts: {
|
|
231
|
+
/*
|
|
232
|
+
* preinstall runs BEFORE node_modules exists, so it cannot call a bin from the package.
|
|
233
|
+
* scripts/check-node.mjs is on disk for exactly that reason and `webm sync` refreshes it.
|
|
234
|
+
*/
|
|
235
|
+
preinstall: "node scripts/check-node.mjs",
|
|
236
|
+
postinstall: "webm sync",
|
|
237
|
+
dev: "wrangler types && astro dev",
|
|
238
|
+
build: "wrangler types && astro check && astro build",
|
|
239
|
+
preview: "astro preview",
|
|
240
|
+
check: "wrangler types && astro check",
|
|
241
|
+
doctor: "webm doctor",
|
|
242
|
+
format: "prettier --write .",
|
|
243
|
+
/*
|
|
244
|
+
* Component tests. --import scripts/test-hooks.mjs is what lets a test import './thing'
|
|
245
|
+
* without an extension and a .json without an import attribute, the way Astro and Vite
|
|
246
|
+
* do - so a test file reads like the app code it covers.
|
|
247
|
+
*/
|
|
248
|
+
test: 'node --import ./scripts/test-hooks.mjs --test "src/**/*.test.ts"'
|
|
249
|
+
},
|
|
250
|
+
dependencies: { "@cparkerwebm/webmonterey": `^${packageVersion2}`, astro: "^7.1.6" },
|
|
251
|
+
devDependencies: {
|
|
252
|
+
"@astrojs/check": "^0.9.10",
|
|
253
|
+
prettier: "^3.9.6",
|
|
254
|
+
typescript: "^6.0.3",
|
|
255
|
+
wrangler: "^4.118.0"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
null,
|
|
259
|
+
2
|
|
260
|
+
) + "\n";
|
|
261
|
+
files["astro.config.mjs"] = `// @ts-check
|
|
262
|
+
import { defineConfig } from 'astro/config';
|
|
263
|
+
import webmonterey, { adapter } from '@cparkerwebm/webmonterey';
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* Two lines of configuration. Everything else - imageService, the sitemap, deriving site
|
|
267
|
+
* from webmonterey.json, the noindex routes, the virtual modules - is decided inside the
|
|
268
|
+
* integration, so a fix propagates on npm update rather than never.
|
|
269
|
+
*
|
|
270
|
+
* THE ADAPTER IS NAMED HERE AND NOT SET BY THE INTEGRATION. An adapter registered through
|
|
271
|
+
* updateConfig does not run its own hooks, and the build then fails on the first on-demand
|
|
272
|
+
* route with an error naming nothing you wrote. adapter() is the package's own, already
|
|
273
|
+
* configured - the site takes no direct dependency on @astrojs/cloudflare.
|
|
274
|
+
*/
|
|
275
|
+
export default defineConfig({
|
|
276
|
+
adapter: adapter(),
|
|
277
|
+
integrations: [webmonterey()],
|
|
278
|
+
});
|
|
279
|
+
`;
|
|
280
|
+
files["webmonterey.json"] = JSON.stringify(
|
|
281
|
+
{
|
|
282
|
+
"//": "Site identity. READ BY THE BUILD - editing this changes output, it is not documentation.",
|
|
283
|
+
client,
|
|
284
|
+
domain,
|
|
285
|
+
repo: `${org}/${n.repo}`,
|
|
286
|
+
worker: n.worker,
|
|
287
|
+
slug: n.slug,
|
|
288
|
+
launched: null,
|
|
289
|
+
"//environment": "What this deployment is FOR. 'staging' redirects EVERY email the site sends to stagingEmail below, so testing a form on a preview cannot reach the client's real contacts. A new site starts here; /webm:launch flips it to 'production'. Anything served from workers.dev is treated as staging regardless, so a branch preview of a live site is covered too.",
|
|
290
|
+
environment: "staging",
|
|
291
|
+
"//stagingEmail": "Where staging email goes instead of its real recipients. REQUIRED while environment is staging - a staging site with nowhere to send refuses to send rather than guessing. webm doctor checks.",
|
|
292
|
+
stagingEmail: options.stagingEmail ?? "",
|
|
293
|
+
"//gtmId": "Google Tag Manager container, GTM-XXXXXXX. PUBLIC by design - it is in the source of every page - so it is tracked here rather than in .env, which is gitignored and therefore absent on Workers Builds: a container id set only in the environment works locally and silently loads nothing in production. PUBLIC_GTM_ID in the environment overrides this, for pointing a branch at a different container. Leave empty and no GTM renders at all.",
|
|
294
|
+
gtmId: "",
|
|
295
|
+
"//locale": "IANA zone and locale for every client-facing date. D1 stores UTC, so nothing reaches a person without passing through these. There is no Pacific/LA.",
|
|
296
|
+
timeZone: "America/Los_Angeles",
|
|
297
|
+
locale: "en-US",
|
|
298
|
+
"//organization": "Feeds the JSON-LD graph. EVERY empty field is omitted from output - never guess one, because wrong opening hours in structured data is worse than none.",
|
|
299
|
+
organization: {
|
|
300
|
+
type: "Organization",
|
|
301
|
+
legalName: "",
|
|
302
|
+
telephone: "",
|
|
303
|
+
email: "",
|
|
304
|
+
streetAddress: "",
|
|
305
|
+
addressLocality: "",
|
|
306
|
+
addressRegion: "",
|
|
307
|
+
postalCode: "",
|
|
308
|
+
addressCountry: "",
|
|
309
|
+
sameAs: []
|
|
310
|
+
},
|
|
311
|
+
"//features": "Technical switches for what is WIRED on this site - not a commercial plan. Nothing here reads a tier. `platform` is reserved and inert until the platform mail relay ships.",
|
|
312
|
+
features: { compliance: true, d1: false, turnstile: false, platform: false },
|
|
313
|
+
"//app": "The web app namespace, reserved on every site. The folder is ALWAYS src/pages/webapp/; `path` is the public URL segment - set `portal`, `members` or `account` for a client whose customers log in, and the framework rewrites it onto the folder. Everything else derives from this field. Every page under the folder must be `prerender = false`.",
|
|
314
|
+
app: { enabled: false, path: "webapp", label: "Portal" }
|
|
315
|
+
},
|
|
316
|
+
null,
|
|
317
|
+
2
|
|
318
|
+
) + "\n";
|
|
319
|
+
files["design.json"] = JSON.stringify(
|
|
320
|
+
{
|
|
321
|
+
$schema: "./node_modules/@cparkerwebm/webmonterey/schema/design.json",
|
|
322
|
+
version: 1,
|
|
323
|
+
brand: {
|
|
324
|
+
name: client === "CHANGEME" ? "" : client,
|
|
325
|
+
voice: "",
|
|
326
|
+
rules: []
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
null,
|
|
330
|
+
2
|
|
331
|
+
) + "\n";
|
|
332
|
+
files["wrangler.jsonc"] = `{
|
|
333
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
334
|
+
|
|
335
|
+
// MUST match the Worker name in the Cloudflare dashboard, or Workers Builds fails.
|
|
336
|
+
"name": "${n.worker}",
|
|
337
|
+
|
|
338
|
+
// The date this Worker was created, set a fortnight behind the scaffold so it cannot be newer
|
|
339
|
+
// than the runtime the installed wrangler bundles - a compatibility_date in the future of that
|
|
340
|
+
// binary refuses to build at all. Do not bump it casually; it pins runtime behavior.
|
|
341
|
+
// Do not let it go stale either: a date a few months behind the installed workerd renders
|
|
342
|
+
// every page as "[object Object]", with no error and a successful build. \`webm doctor\` checks.
|
|
343
|
+
"compatibility_date": "${compatibilityDate}",
|
|
344
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
345
|
+
|
|
346
|
+
// Astro's adapter builds static assets into dist/client, NOT dist.
|
|
347
|
+
"assets": {
|
|
348
|
+
"directory": "./dist/client",
|
|
349
|
+
"not_found_handling": "404-page",
|
|
350
|
+
|
|
351
|
+
/*
|
|
352
|
+
* NOT REDUNDANT. Do not remove an entry because the path "already works" in curl.
|
|
353
|
+
*
|
|
354
|
+
* "404-page" intercepts NAVIGATION requests that match no static asset and serves
|
|
355
|
+
* 404.html, so they never reach the Worker. Every \`prerender = false\` route is such a
|
|
356
|
+
* path. The interception keys off \`Sec-Fetch-Dest: document\`, which a browser sends and
|
|
357
|
+
* curl does not - so the same URL returns 200 to curl and the 404 page to Chrome.
|
|
358
|
+
*
|
|
359
|
+
* >> ADD EVERY ROUTE YOU GIVE \`prerender = false\`, IN BOTH SLASH FORMS. <<
|
|
360
|
+
* \`npx webm doctor\` checks this.
|
|
361
|
+
*/
|
|
362
|
+
"run_worker_first": ["/_actions/*"]
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
"observability": { "enabled": true },
|
|
366
|
+
|
|
367
|
+
// Set BOTH explicitly. Toggling previews in the dashboard without updating this file
|
|
368
|
+
// silently reverts it on the next deploy.
|
|
369
|
+
"workers_dev": true,
|
|
370
|
+
"preview_urls": true
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
files["tsconfig.json"] = JSON.stringify(
|
|
374
|
+
{
|
|
375
|
+
extends: "astro/tsconfigs/strict",
|
|
376
|
+
include: [".astro/types.d.ts", "**/*", "worker-configuration.d.ts"],
|
|
377
|
+
exclude: ["dist", "node_modules"]
|
|
378
|
+
},
|
|
379
|
+
null,
|
|
380
|
+
2
|
|
381
|
+
) + "\n";
|
|
382
|
+
files[".claude/settings.json"] = JSON.stringify(
|
|
383
|
+
{
|
|
384
|
+
"//": `Project settings for ${n.repo}.`,
|
|
385
|
+
"//mcp": "A server declared in .mcp.json is INERT until approved on each machine. Without this line the rules that say consult the Astro and MDN docs before using an API would depend on whoever cloned the repo happening to hit Approve.",
|
|
386
|
+
includeCoAuthoredBy: false,
|
|
387
|
+
enabledMcpjsonServers: MCP_NAMES,
|
|
388
|
+
permissions: {
|
|
389
|
+
deny: [
|
|
390
|
+
"Read(**/.dev.vars)",
|
|
391
|
+
"Read(**/.dev.vars.*)",
|
|
392
|
+
"Read(**/.env)",
|
|
393
|
+
"Read(**/.env.*)",
|
|
394
|
+
"Read(**/*.pem)",
|
|
395
|
+
"Read(**/*.key)",
|
|
396
|
+
"Read(**/.npmrc)",
|
|
397
|
+
"Edit(**/.dev.vars)",
|
|
398
|
+
"Edit(**/.env)",
|
|
399
|
+
"Write(**/.dev.vars)",
|
|
400
|
+
"Write(**/.env)"
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
null,
|
|
405
|
+
2
|
|
406
|
+
) + "\n";
|
|
407
|
+
files[".mcp.json"] = JSON.stringify(mcpConfig(), null, 2) + "\n";
|
|
408
|
+
files["src/actions/index.ts"] = `/*
|
|
409
|
+
* Form handlers.
|
|
410
|
+
*
|
|
411
|
+
* \`src/actions/index.ts\` is a FIXED path - Astro looks nowhere else - so this file has to live
|
|
412
|
+
* in the client repo. What it contains is a re-export, and that is the whole point: the pipeline
|
|
413
|
+
* itself is package-owned, so a fix to validation, Turnstile handling, the D1 write or an email
|
|
414
|
+
* template reaches this site on npm update rather than never.
|
|
415
|
+
*
|
|
416
|
+
* The order inside it is load-bearing: validate, verify Turnstile, write to D1, notify, then
|
|
417
|
+
* autorespond. Storing before sending means a Mailgun failure leaves an enquiry with
|
|
418
|
+
* notified_at NULL rather than losing it.
|
|
419
|
+
*
|
|
420
|
+
* Forms are defined in src/forms/*.json, one per form, and the filename is the form id. There
|
|
421
|
+
* is no registry to keep in step - adding src/forms/quote.json is the whole job.
|
|
422
|
+
*
|
|
423
|
+
* TO CUSTOMIZE: wrap rather than fork. Import the package's server, spread it, and add your own
|
|
424
|
+
* action beside it. Copying the pipeline in here is how a site stops receiving fixes.
|
|
425
|
+
*/
|
|
426
|
+
export { server } from '@cparkerwebm/webmonterey/actions';
|
|
427
|
+
`;
|
|
428
|
+
files["src/components/registry.ts"] = `import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
429
|
+
|
|
430
|
+
/*
|
|
431
|
+
* THE BLOCK REGISTRY - client-owned, because every visible component is. The package ships
|
|
432
|
+
* none.
|
|
433
|
+
*
|
|
434
|
+
* Maps a block \`type\` in page JSON to the component that renders it. Adding a component means
|
|
435
|
+
* THREE things: the folder, an entry here, and its schema joining the union in
|
|
436
|
+
* src/content.config.ts.
|
|
437
|
+
*
|
|
438
|
+
* FORGETTING THIS FILE IS THE MOST COMMON BUG. Nothing errors - the build succeeds and the
|
|
439
|
+
* block renders as nothing. Miss the union instead and valid JSON fails to build.
|
|
440
|
+
*/
|
|
441
|
+
export const blocks: Record<string, AstroComponentFactory> = {};
|
|
442
|
+
|
|
443
|
+
export const registeredTypes = (): string[] => Object.keys(blocks);
|
|
444
|
+
`;
|
|
445
|
+
files["src/content.config.ts"] = `import { webmontereyCollections } from '@cparkerwebm/webmonterey/content';
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
* The block union is what makes a typo in page JSON a BUILD ERROR rather than a blank space.
|
|
449
|
+
* Add each component's schema as you build it:
|
|
450
|
+
*
|
|
451
|
+
* import { schema as content000001 } from './components/content/content-000001/schema.ts';
|
|
452
|
+
* export const collections = webmontereyCollections([content000001]);
|
|
453
|
+
*
|
|
454
|
+
* A union needs at least one member, so this stays commented until the first component exists.
|
|
455
|
+
*/
|
|
456
|
+
export const collections = webmontereyCollections([]);
|
|
457
|
+
`;
|
|
458
|
+
files["src/styles/custom/_index.css"] = `/*
|
|
459
|
+
* The client override seam.
|
|
460
|
+
*
|
|
461
|
+
* webm.components.custom beats webm.components.core at identical specificity, so a rule here
|
|
462
|
+
* lands without !important and stays legible as an override. webm.overrides is the last word.
|
|
463
|
+
*
|
|
464
|
+
* To retheme, change the TOKEN in design.json rather than the rule - one declaration cascades
|
|
465
|
+
* everywhere. Reach for this file when the token system genuinely does not express something.
|
|
466
|
+
*
|
|
467
|
+
* NOTHING IMPORTS THIS FILE FROM THIS REPO. The package's base layout pulls it in through a
|
|
468
|
+
* virtual module, so it ships automatically and there is no entry point here to keep in step.
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
@layer webm.components.custom {
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
@layer webm.overrides {
|
|
475
|
+
}
|
|
476
|
+
`;
|
|
477
|
+
files["src/content/pages/home.json"] = JSON.stringify(
|
|
478
|
+
{
|
|
479
|
+
title: client === "CHANGEME" ? "Home" : client,
|
|
480
|
+
showTitle: true,
|
|
481
|
+
description: "",
|
|
482
|
+
blocks: []
|
|
483
|
+
},
|
|
484
|
+
null,
|
|
485
|
+
2
|
|
486
|
+
) + "\n";
|
|
487
|
+
files["public/site.webmanifest"] = JSON.stringify(
|
|
488
|
+
{
|
|
489
|
+
name: client === "CHANGEME" ? domain : client,
|
|
490
|
+
short_name: client === "CHANGEME" ? domain : client,
|
|
491
|
+
icons: [
|
|
492
|
+
{ src: "/android-chrome-192x192.png", sizes: "192x192", type: "image/png" },
|
|
493
|
+
{
|
|
494
|
+
src: "/android-chrome-512x512.png",
|
|
495
|
+
sizes: "512x512",
|
|
496
|
+
type: "image/png",
|
|
497
|
+
purpose: "any maskable"
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
theme_color: "#006abe",
|
|
501
|
+
background_color: "#ffffff",
|
|
502
|
+
display: "standalone",
|
|
503
|
+
start_url: "/"
|
|
504
|
+
},
|
|
505
|
+
null,
|
|
506
|
+
2
|
|
507
|
+
) + "\n";
|
|
508
|
+
files[".gitignore"] = `node_modules/
|
|
509
|
+
dist/
|
|
510
|
+
.astro/
|
|
511
|
+
.wrangler/
|
|
512
|
+
worker-configuration.d.ts
|
|
513
|
+
.DS_Store
|
|
514
|
+
|
|
515
|
+
# Secrets. Never committed, never uploaded - wrangler secret put is the only path.
|
|
516
|
+
.dev.vars
|
|
517
|
+
.dev.vars.*
|
|
518
|
+
.env
|
|
519
|
+
.env.*
|
|
520
|
+
|
|
521
|
+
# Refused by preinstall; here as belt and braces.
|
|
522
|
+
pnpm-lock.yaml
|
|
523
|
+
yarn.lock
|
|
524
|
+
bun.lock*
|
|
525
|
+
`;
|
|
526
|
+
files[".dev.vars.example"] = `# Copy to .dev.vars for local development. NEVER commit .dev.vars.
|
|
527
|
+
# Anything here must also exist as a real Worker secret - \`wrangler secret put\`.
|
|
528
|
+
# Record each one in the password manager as you create it: wrangler cannot read a secret back.
|
|
529
|
+
|
|
530
|
+
# TURNSTILE_SECRET_KEY=
|
|
531
|
+
# MAILGUN_API_KEY=
|
|
532
|
+
# MAILGUN_DOMAIN=
|
|
533
|
+
`;
|
|
534
|
+
files[".nvmrc"] = "24\n";
|
|
535
|
+
files["README.md"] = `# ${client === "CHANGEME" ? n.repo : client}
|
|
536
|
+
|
|
537
|
+
${domain} \u2014 built on [@cparkerwebm/webmonterey](https://github.com/cparkerwebm/webmonterey).
|
|
538
|
+
|
|
539
|
+
## Commands
|
|
540
|
+
|
|
541
|
+
| Command | Runs |
|
|
542
|
+
| --- | --- |
|
|
543
|
+
| \`npm run dev\` | local dev server |
|
|
544
|
+
| \`npm run preview\` | **a real build on real workerd** \u2014 use this, not dev, for anything touching styles, routes or wrangler.jsonc |
|
|
545
|
+
| \`npm run check\` | types and content schema |
|
|
546
|
+
| \`npx webm doctor\` | the things that fail silently |
|
|
547
|
+
|
|
548
|
+
## Cloudflare
|
|
549
|
+
|
|
550
|
+
| | |
|
|
551
|
+
| --- | --- |
|
|
552
|
+
| Worker | \`${n.worker}\` |
|
|
553
|
+
| D1 | \`${n.d1}\` |
|
|
554
|
+
| R2 media | \`${n.r2Media}\` |
|
|
555
|
+
|
|
556
|
+
The repo is named for the domain; Cloudflare resources use the slug, with no TLD, so a
|
|
557
|
+
preview hostname never embeds a domain Chrome could mistake for a lookalike.
|
|
558
|
+
|
|
559
|
+
## Deploying
|
|
560
|
+
|
|
561
|
+
Push to deploy. A \`wrangler deploy\` from a laptop creates a version no build produced, so
|
|
562
|
+
history stops describing what is live and the next push reverts it.
|
|
563
|
+
`;
|
|
564
|
+
return files;
|
|
565
|
+
}
|
|
566
|
+
var init_scaffold = __esm({
|
|
567
|
+
"src/cli/scaffold.ts"() {
|
|
568
|
+
"use strict";
|
|
569
|
+
init_slug();
|
|
570
|
+
init_mcp();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// src/cli/sync.ts
|
|
575
|
+
var sync_exports = {};
|
|
576
|
+
__export(sync_exports, {
|
|
577
|
+
run: () => run,
|
|
578
|
+
sync: () => sync
|
|
579
|
+
});
|
|
580
|
+
import {
|
|
581
|
+
cpSync,
|
|
582
|
+
existsSync as existsSync2,
|
|
583
|
+
mkdirSync,
|
|
584
|
+
readFileSync as readFileSync2,
|
|
585
|
+
rmSync,
|
|
586
|
+
writeFileSync,
|
|
587
|
+
readdirSync
|
|
588
|
+
} from "node:fs";
|
|
589
|
+
import { basename, join as join2 } from "node:path";
|
|
590
|
+
function syncDir(source, target) {
|
|
591
|
+
if (!existsSync2(source)) return [];
|
|
592
|
+
mkdirSync(target, { recursive: true });
|
|
593
|
+
const written = [];
|
|
594
|
+
for (const entry2 of readdirSync(source, { withFileTypes: true })) {
|
|
595
|
+
if (entry2.name.startsWith(".")) continue;
|
|
596
|
+
if (!entry2.isFile()) continue;
|
|
597
|
+
cpSync(join2(source, entry2.name), join2(target, entry2.name));
|
|
598
|
+
written.push(entry2.name);
|
|
599
|
+
}
|
|
600
|
+
return written.sort();
|
|
601
|
+
}
|
|
602
|
+
function addMissing(source, target) {
|
|
603
|
+
if (!existsSync2(source)) return [];
|
|
604
|
+
mkdirSync(target, { recursive: true });
|
|
605
|
+
const added = [];
|
|
606
|
+
for (const entry2 of readdirSync(source, { withFileTypes: true })) {
|
|
607
|
+
if (entry2.name.startsWith(".") || !entry2.isFile()) continue;
|
|
608
|
+
const dest = join2(target, entry2.name);
|
|
609
|
+
if (existsSync2(dest)) continue;
|
|
610
|
+
cpSync(join2(source, entry2.name), dest);
|
|
611
|
+
added.push(entry2.name);
|
|
612
|
+
}
|
|
613
|
+
return added.sort();
|
|
614
|
+
}
|
|
615
|
+
function listSkills(dir) {
|
|
616
|
+
if (!existsSync2(dir)) return [];
|
|
617
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync2(join2(dir, e.name, "SKILL.md"))).map((e) => e.name).sort();
|
|
618
|
+
}
|
|
619
|
+
function sync(siteRoot) {
|
|
620
|
+
const version = packageVersion();
|
|
621
|
+
const source = join2(PACKAGE_ROOT, "skills");
|
|
622
|
+
const target = join2(siteRoot, ".claude", "skills", NAMESPACE);
|
|
623
|
+
const before = listSkills(join2(target, "skills"));
|
|
624
|
+
const after = listSkills(source);
|
|
625
|
+
rmSync(target, { recursive: true, force: true });
|
|
626
|
+
mkdirSync(join2(target, ".claude-plugin"), { recursive: true });
|
|
627
|
+
const noDotfiles = (src) => !basename(src).startsWith(".");
|
|
628
|
+
if (existsSync2(source)) {
|
|
629
|
+
cpSync(source, join2(target, "skills"), { recursive: true, filter: noDotfiles });
|
|
630
|
+
}
|
|
631
|
+
const plugin = (dir) => {
|
|
632
|
+
const from = join2(PACKAGE_ROOT, dir);
|
|
633
|
+
if (!existsSync2(from)) return [];
|
|
634
|
+
const names = readdirSync(from).filter(noDotfiles).sort();
|
|
635
|
+
if (names.length) cpSync(from, join2(target, dir), { recursive: true, filter: noDotfiles });
|
|
636
|
+
return names;
|
|
637
|
+
};
|
|
638
|
+
const agents = plugin("agents");
|
|
639
|
+
const hooks = plugin("hooks");
|
|
640
|
+
writeFileSync(
|
|
641
|
+
join2(target, ".claude-plugin", "plugin.json"),
|
|
642
|
+
JSON.stringify(
|
|
643
|
+
{
|
|
644
|
+
name: NAMESPACE,
|
|
645
|
+
description: "WebMonterey fleet skills. Materialized by `webm sync` - do not edit here.",
|
|
646
|
+
version
|
|
647
|
+
},
|
|
648
|
+
null,
|
|
649
|
+
2
|
|
650
|
+
) + "\n"
|
|
651
|
+
);
|
|
652
|
+
writeFileSync(
|
|
653
|
+
join2(target, ".webm-sync.json"),
|
|
654
|
+
JSON.stringify({ version, namespace: NAMESPACE, skills: after }, null, 2) + "\n"
|
|
655
|
+
);
|
|
656
|
+
const template = join2(PACKAGE_ROOT, "template");
|
|
657
|
+
return {
|
|
658
|
+
added: after.filter((s) => !before.includes(s)),
|
|
659
|
+
removed: before.filter((s) => !after.includes(s)),
|
|
660
|
+
unchanged: after.filter((s) => before.includes(s)),
|
|
661
|
+
version,
|
|
662
|
+
agents,
|
|
663
|
+
hooks,
|
|
664
|
+
scripts: syncDir(join2(template, "scripts"), join2(siteRoot, "scripts")),
|
|
665
|
+
/*
|
|
666
|
+
* CI, REPLACE-managed like scripts/. A site never edits its own workflow - if a leg is wrong
|
|
667
|
+
* it is wrong on every site, and it gets fixed here. Five sites once had no CI whatsoever,
|
|
668
|
+
* which is how code depending on an unpublished package reached main without a word.
|
|
669
|
+
*/
|
|
670
|
+
workflows: syncDir(join2(template, "workflows"), join2(siteRoot, ".github/workflows")),
|
|
671
|
+
migrations: addMissing(join2(template, "migrations"), join2(siteRoot, "migrations"))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function ensureGitignored(siteRoot) {
|
|
675
|
+
const path = join2(siteRoot, ".gitignore");
|
|
676
|
+
const entry2 = `.claude/skills/${NAMESPACE}/`;
|
|
677
|
+
const current = existsSync2(path) ? readFileSync2(path, "utf8") : "";
|
|
678
|
+
if (current.includes(entry2)) return false;
|
|
679
|
+
const addition = `
|
|
680
|
+
# Materialized by \`webm sync\` on every install. Package-owned; never commit.
|
|
681
|
+
# Client skills go BESIDE it, at .claude/skills/<name>/.
|
|
682
|
+
${entry2}
|
|
683
|
+
`;
|
|
684
|
+
writeFileSync(path, current.replace(/\n*$/, "\n") + addition);
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
function run(argv2) {
|
|
688
|
+
const siteRoot = argv2[0] ?? process.cwd();
|
|
689
|
+
const result = sync(siteRoot);
|
|
690
|
+
const total = result.added.length + result.unchanged.length;
|
|
691
|
+
console.log(`webm sync: ${total} skills at .claude/skills/${NAMESPACE}/ (v${result.version})`);
|
|
692
|
+
for (const s of result.added) console.log(` + /${NAMESPACE}:${s}`);
|
|
693
|
+
for (const s of result.removed) console.log(` - /${NAMESPACE}:${s}`);
|
|
694
|
+
if (result.agents.length) console.log(` agents: ${result.agents.join(", ")}`);
|
|
695
|
+
if (result.hooks.length) console.log(` hooks: ${result.hooks.join(", ")}`);
|
|
696
|
+
if (result.scripts.length) {
|
|
697
|
+
console.log(` scripts/ refreshed: ${result.scripts.join(", ")}`);
|
|
698
|
+
}
|
|
699
|
+
for (const m of result.migrations) {
|
|
700
|
+
console.log(` + migrations/${m} (apply it: npx wrangler d1 migrations apply <DB> --remote)`);
|
|
701
|
+
}
|
|
702
|
+
if (ensureGitignored(siteRoot)) {
|
|
703
|
+
console.log(` gitignored .claude/skills/${NAMESPACE}/`);
|
|
704
|
+
}
|
|
705
|
+
if (result.added.length || result.removed.length) {
|
|
706
|
+
console.log(
|
|
707
|
+
`
|
|
708
|
+
Skill changes are picked up live. If this was the first sync in this repo, restart Claude Code so it can watch the new directory.`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
var NAMESPACE;
|
|
714
|
+
var init_sync = __esm({
|
|
715
|
+
"src/cli/sync.ts"() {
|
|
716
|
+
"use strict";
|
|
717
|
+
init_package_root();
|
|
718
|
+
NAMESPACE = "webm";
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// src/cli/seed.ts
|
|
723
|
+
import { cpSync as cpSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
724
|
+
import { dirname as dirname2, join as join3, relative } from "node:path";
|
|
725
|
+
function walk(dir) {
|
|
726
|
+
if (!existsSync3(dir)) return [];
|
|
727
|
+
return readdirSync2(dir, { withFileTypes: true }).flatMap((e) => {
|
|
728
|
+
if (e.name.startsWith(".") && e.name !== ".editorconfig" && e.name !== ".prettierrc.json" && e.name !== ".prettierignore") {
|
|
729
|
+
return [];
|
|
730
|
+
}
|
|
731
|
+
const full = join3(dir, e.name);
|
|
732
|
+
return e.isDirectory() ? walk(full) : [full];
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function seed(packageRoot, siteRoot) {
|
|
736
|
+
const written = [];
|
|
737
|
+
for (const { from, to } of SEEDS) {
|
|
738
|
+
const source = join3(packageRoot, "template", from);
|
|
739
|
+
for (const file of walk(source)) {
|
|
740
|
+
const rel = relative(source, file);
|
|
741
|
+
const dest = join3(siteRoot, to === "." ? rel : join3(to, rel));
|
|
742
|
+
if (existsSync3(dest)) continue;
|
|
743
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
744
|
+
cpSync2(file, dest);
|
|
745
|
+
written.push(relative(siteRoot, dest));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return written.sort();
|
|
749
|
+
}
|
|
750
|
+
function gitkeepEmptyDirs(siteRoot, dirs) {
|
|
751
|
+
const kept = [];
|
|
752
|
+
for (const dir of dirs) {
|
|
753
|
+
const full = join3(siteRoot, dir);
|
|
754
|
+
mkdirSync2(full, { recursive: true });
|
|
755
|
+
if (readdirSync2(full).length > 0) continue;
|
|
756
|
+
writeFileSync2(join3(full, ".gitkeep"), "");
|
|
757
|
+
kept.push(join3(dir, ".gitkeep"));
|
|
758
|
+
}
|
|
759
|
+
const walkDirs = (d) => {
|
|
760
|
+
for (const e of readdirSync2(d, { withFileTypes: true })) {
|
|
761
|
+
if (!e.isDirectory() || e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
762
|
+
const full = join3(d, e.name);
|
|
763
|
+
if (readdirSync2(full).length === 0) {
|
|
764
|
+
writeFileSync2(join3(full, ".gitkeep"), "");
|
|
765
|
+
kept.push(join3(relative(siteRoot, full), ".gitkeep"));
|
|
766
|
+
} else {
|
|
767
|
+
walkDirs(full);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
walkDirs(siteRoot);
|
|
772
|
+
return [...new Set(kept)].sort();
|
|
773
|
+
}
|
|
774
|
+
var SEEDS, SCAFFOLD_DIRS;
|
|
775
|
+
var init_seed = __esm({
|
|
776
|
+
"src/cli/seed.ts"() {
|
|
777
|
+
"use strict";
|
|
778
|
+
SEEDS = [
|
|
779
|
+
{ from: "site", to: "." },
|
|
780
|
+
{ from: "public", to: "public" },
|
|
781
|
+
{ from: "assets", to: "src/assets/images" }
|
|
782
|
+
];
|
|
783
|
+
SCAFFOLD_DIRS = [
|
|
784
|
+
"migrations",
|
|
785
|
+
"public",
|
|
786
|
+
"src/assets/fonts",
|
|
787
|
+
"src/assets/icons",
|
|
788
|
+
"src/assets/images",
|
|
789
|
+
"src/components/asides",
|
|
790
|
+
"src/components/content",
|
|
791
|
+
"src/components/general",
|
|
792
|
+
"src/components/interfaces",
|
|
793
|
+
"src/components/regions",
|
|
794
|
+
"src/content/pages",
|
|
795
|
+
"src/forms",
|
|
796
|
+
"src/pages",
|
|
797
|
+
"src/pages/webapp",
|
|
798
|
+
"src/scripts",
|
|
799
|
+
"src/styles/custom"
|
|
800
|
+
];
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// src/cli/new.ts
|
|
805
|
+
var new_exports = {};
|
|
806
|
+
__export(new_exports, {
|
|
807
|
+
run: () => run2
|
|
808
|
+
});
|
|
809
|
+
import { execFileSync } from "node:child_process";
|
|
810
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
811
|
+
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
812
|
+
function parseArgs(argv2) {
|
|
813
|
+
const positional = argv2.filter((a) => !a.startsWith("--"));
|
|
814
|
+
const flag = (name) => {
|
|
815
|
+
const hit = argv2.find((a) => a.startsWith(`--${name}=`));
|
|
816
|
+
return hit ? hit.slice(name.length + 3) : void 0;
|
|
817
|
+
};
|
|
818
|
+
return {
|
|
819
|
+
domain: positional[0],
|
|
820
|
+
client: flag("client"),
|
|
821
|
+
org: flag("org") ?? "webmonterey",
|
|
822
|
+
stagingEmail: flag("staging-email"),
|
|
823
|
+
into: flag("into"),
|
|
824
|
+
install: !argv2.includes("--no-install")
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function gitUserEmail() {
|
|
828
|
+
try {
|
|
829
|
+
return execFileSync("git", ["config", "user.email"], { encoding: "utf8" }).trim();
|
|
830
|
+
} catch {
|
|
831
|
+
return "";
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function run2(argv2) {
|
|
835
|
+
const args = parseArgs(argv2);
|
|
836
|
+
if (!args.domain) {
|
|
837
|
+
console.error(
|
|
838
|
+
'webm new <domain> [--client="Name"] [--org=webmonterey] [--staging-email=you@example.com] [--into=path] [--no-install]'
|
|
839
|
+
);
|
|
840
|
+
console.error('\n webm new autire.com --client="Autire Technologies"');
|
|
841
|
+
return 1;
|
|
842
|
+
}
|
|
843
|
+
let domain;
|
|
844
|
+
try {
|
|
845
|
+
domain = normalizeDomain(args.domain);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error(`webm new: ${error instanceof DomainError ? error.message : error}`);
|
|
848
|
+
return 1;
|
|
849
|
+
}
|
|
850
|
+
const names = resourceNames(domain);
|
|
851
|
+
const root = resolve(args.into ?? repoName(domain));
|
|
852
|
+
if (existsSync4(root) && readdirSync3(root).length > 0) {
|
|
853
|
+
console.error(`webm new: ${root} exists and is not empty. Refusing to scaffold over it.`);
|
|
854
|
+
return 1;
|
|
855
|
+
}
|
|
856
|
+
const files = scaffold({
|
|
857
|
+
domain,
|
|
858
|
+
client: args.client,
|
|
859
|
+
org: args.org,
|
|
860
|
+
stagingEmail: args.stagingEmail ?? gitUserEmail(),
|
|
861
|
+
packageVersion: packageVersion(),
|
|
862
|
+
/* Real today, not a constant - see ScaffoldOptions.today for what a stale one does. */
|
|
863
|
+
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
864
|
+
});
|
|
865
|
+
for (const [path, contents] of Object.entries(files)) {
|
|
866
|
+
const full = join4(root, path);
|
|
867
|
+
mkdirSync3(dirname3(full), { recursive: true });
|
|
868
|
+
writeFileSync3(full, contents);
|
|
869
|
+
}
|
|
870
|
+
const seeded = seed(PACKAGE_ROOT, root);
|
|
871
|
+
sync(root);
|
|
872
|
+
const kept = gitkeepEmptyDirs(root, SCAFFOLD_DIRS);
|
|
873
|
+
console.log(`Scaffolded ${Object.keys(files).length} files into ${root}`);
|
|
874
|
+
console.log(` + ${seeded.length} seeded (favicons, headers, CLAUDE.md, a contact form)`);
|
|
875
|
+
console.log(` + ${kept.length} .gitkeep, so the empty directories survive a clone
|
|
876
|
+
`);
|
|
877
|
+
console.log(` repo ${args.org}/${names.repo} (keeps the domain)`);
|
|
878
|
+
console.log(
|
|
879
|
+
` worker ${names.worker} (slug - no TLD, so Chrome does not flag previews)`
|
|
880
|
+
);
|
|
881
|
+
console.log(` d1 ${names.d1}`);
|
|
882
|
+
console.log(` r2 ${names.r2Media}`);
|
|
883
|
+
try {
|
|
884
|
+
execFileSync("git", ["init", "-q", "-b", "main"], { cwd: root });
|
|
885
|
+
} catch {
|
|
886
|
+
console.log("\n (git init failed - initialize the repo yourself)");
|
|
887
|
+
}
|
|
888
|
+
if (args.install) {
|
|
889
|
+
console.log("\nInstalling\u2026");
|
|
890
|
+
try {
|
|
891
|
+
execFileSync("npm", ["install"], { cwd: root, stdio: "inherit" });
|
|
892
|
+
} catch {
|
|
893
|
+
console.error(
|
|
894
|
+
`
|
|
895
|
+
Install failed. The scaffold is written and fine; fix the cause above and run npm install in ${names.repo}.`
|
|
896
|
+
);
|
|
897
|
+
return 1;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
console.log(`
|
|
901
|
+
Next:`);
|
|
902
|
+
console.log(` cd ${names.repo}`);
|
|
903
|
+
console.log(` claude # then /webm:start \u2014 it takes it from here`);
|
|
904
|
+
console.log(`
|
|
905
|
+
Start Claude from the repo root. Project skills-dir plugins load only from the`);
|
|
906
|
+
console.log(`directory you start in; launching from a subdirectory misses them.`);
|
|
907
|
+
return 0;
|
|
908
|
+
}
|
|
909
|
+
var init_new = __esm({
|
|
910
|
+
"src/cli/new.ts"() {
|
|
911
|
+
"use strict";
|
|
912
|
+
init_slug();
|
|
913
|
+
init_scaffold();
|
|
914
|
+
init_sync();
|
|
915
|
+
init_seed();
|
|
916
|
+
init_package_root();
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// src/design/defaults.ts
|
|
921
|
+
var DEFAULTS, TOKEN_NAMES;
|
|
922
|
+
var init_defaults = __esm({
|
|
923
|
+
"src/design/defaults.ts"() {
|
|
924
|
+
"use strict";
|
|
925
|
+
DEFAULTS = [
|
|
926
|
+
{
|
|
927
|
+
title: "base palette",
|
|
928
|
+
note: "Raw neutrals. Everything semantic below points at these, not the other way round.",
|
|
929
|
+
tokens: [
|
|
930
|
+
{ name: "--webm-base-100", value: "#ffffff" },
|
|
931
|
+
{ name: "--webm-base-300", value: "#f1eae8" },
|
|
932
|
+
{ name: "--webm-base-500", value: "#3f3f3f" },
|
|
933
|
+
{ name: "--webm-base-700", value: "#222222" },
|
|
934
|
+
{ name: "--webm-base-900", value: "#000000" }
|
|
935
|
+
]
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
title: "action",
|
|
939
|
+
note: "The brand color. Retheming a client usually starts and ends here.",
|
|
940
|
+
tokens: [
|
|
941
|
+
{ name: "--webm-action", value: "#006abe" },
|
|
942
|
+
{ name: "--webm-action-dark", value: "#003e80" },
|
|
943
|
+
{ name: "--webm-action-light", value: "#6bc7ff" }
|
|
944
|
+
]
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
title: "surface",
|
|
948
|
+
tokens: [
|
|
949
|
+
{ name: "--webm-surface", value: "var(--webm-base-100)" },
|
|
950
|
+
{ name: "--webm-surface-alt", value: "var(--webm-base-300)" },
|
|
951
|
+
{ name: "--webm-surface-accent", value: "var(--webm-action-light)" },
|
|
952
|
+
{ name: "--webm-surface-inverse", value: "var(--webm-base-900)" },
|
|
953
|
+
{ name: "--webm-surface-raised", value: "var(--webm-base-100)" }
|
|
954
|
+
]
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
title: "text",
|
|
958
|
+
tokens: [
|
|
959
|
+
{ name: "--webm-text", value: "var(--webm-base-700)" },
|
|
960
|
+
{ name: "--webm-text-muted", value: "var(--webm-base-500)" },
|
|
961
|
+
{ name: "--webm-text-strong", value: "var(--webm-base-900)" },
|
|
962
|
+
{ name: "--webm-text-inverse", value: "var(--webm-base-100)" },
|
|
963
|
+
{ name: "--webm-text-on-action", value: "var(--webm-base-100)" }
|
|
964
|
+
]
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
title: "link",
|
|
968
|
+
tokens: [
|
|
969
|
+
{ name: "--webm-link", value: "var(--webm-action)" },
|
|
970
|
+
{ name: "--webm-link-hover", value: "var(--webm-action-dark)" },
|
|
971
|
+
{ name: "--webm-link-visited", value: "var(--webm-action-dark)" }
|
|
972
|
+
]
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
title: "border",
|
|
976
|
+
tokens: [
|
|
977
|
+
{ name: "--webm-border-subtle", value: "#dcd3d0" },
|
|
978
|
+
{ name: "--webm-border-interactive", value: "var(--webm-base-500)" },
|
|
979
|
+
{ name: "--webm-border-strong", value: "var(--webm-base-700)" }
|
|
980
|
+
]
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
title: "state",
|
|
984
|
+
tokens: [
|
|
985
|
+
{ name: "--webm-state-success", value: "#0f7b3f" },
|
|
986
|
+
{ name: "--webm-state-warning", value: "#9a6700" },
|
|
987
|
+
{ name: "--webm-state-danger", value: "#b3261e" },
|
|
988
|
+
{ name: "--webm-state-info", value: "var(--webm-action)" }
|
|
989
|
+
]
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
title: "font",
|
|
993
|
+
note: [
|
|
994
|
+
"System stack by default - zero network cost, no layout shift, no dependency.",
|
|
995
|
+
"To use a real typeface, put the woff2 in public/fonts/, declare @font-face in",
|
|
996
|
+
"src/styles/custom/, and set font.sans in design.json."
|
|
997
|
+
].join("\n"),
|
|
998
|
+
tokens: [
|
|
999
|
+
{
|
|
1000
|
+
name: "--webm-font-sans",
|
|
1001
|
+
value: "system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: "--webm-font-mono",
|
|
1005
|
+
value: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
title: "font-size",
|
|
1011
|
+
note: "Fluid scale. Each step interpolates between a mobile and a desktop size.",
|
|
1012
|
+
tokens: [
|
|
1013
|
+
{ name: "--webm-font-size-2xs", value: "clamp(0.6944rem, 0.6859rem + 0.0426vw, 0.72rem)" },
|
|
1014
|
+
{ name: "--webm-font-size-xs", value: "clamp(0.8333rem, 0.8111rem + 0.1111vw, 0.9rem)" },
|
|
1015
|
+
{ name: "--webm-font-size-sm", value: "clamp(1rem, 0.9583rem + 0.2083vw, 1.125rem)" },
|
|
1016
|
+
{ name: "--webm-font-size-md", value: "clamp(1.2rem, 1.1312rem + 0.3438vw, 1.406rem)" },
|
|
1017
|
+
{ name: "--webm-font-size-lg", value: "clamp(1.44rem, 1.3341rem + 0.5297vw, 1.758rem)" },
|
|
1018
|
+
{ name: "--webm-font-size-xl", value: "clamp(1.728rem, 1.5716rem + 0.7821vw, 2.197rem)" },
|
|
1019
|
+
{ name: "--webm-font-size-2xl", value: "clamp(2.074rem, 1.8493rem + 1.1216vw, 2.747rem)" },
|
|
1020
|
+
{ name: "--webm-font-size-3xl", value: "clamp(2.488rem, 2.1734rem + 1.5748vw, 3.433rem)" },
|
|
1021
|
+
{ name: "--webm-font-size-4xl", value: "clamp(2.986rem, 2.5508rem + 2.1759vw, 4.292rem)" }
|
|
1022
|
+
]
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
title: "leading",
|
|
1026
|
+
tokens: [
|
|
1027
|
+
{ name: "--webm-leading-tight", value: "1.1" },
|
|
1028
|
+
{ name: "--webm-leading-snug", value: "1.25" },
|
|
1029
|
+
{ name: "--webm-leading-normal", value: "1.5" },
|
|
1030
|
+
{ name: "--webm-leading-loose", value: "1.7" }
|
|
1031
|
+
]
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
title: "tracking",
|
|
1035
|
+
tokens: [
|
|
1036
|
+
{ name: "--webm-tracking-tight", value: "-0.02em" },
|
|
1037
|
+
{ name: "--webm-tracking-normal", value: "0em" },
|
|
1038
|
+
{ name: "--webm-tracking-wide", value: "0.06em" }
|
|
1039
|
+
]
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
title: "weight",
|
|
1043
|
+
tokens: [
|
|
1044
|
+
{ name: "--webm-weight-regular", value: "400" },
|
|
1045
|
+
{ name: "--webm-weight-medium", value: "500" },
|
|
1046
|
+
{ name: "--webm-weight-semibold", value: "600" },
|
|
1047
|
+
{ name: "--webm-weight-bold", value: "700" }
|
|
1048
|
+
]
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
title: "space",
|
|
1052
|
+
tokens: [
|
|
1053
|
+
{ name: "--webm-space-3xs", value: "0.25rem" },
|
|
1054
|
+
{ name: "--webm-space-2xs", value: "0.5rem" },
|
|
1055
|
+
{ name: "--webm-space-xs", value: "0.75rem" },
|
|
1056
|
+
{ name: "--webm-space-sm", value: "1rem" },
|
|
1057
|
+
{ name: "--webm-space-md", value: "1.5rem" },
|
|
1058
|
+
{ name: "--webm-space-lg", value: "2rem" },
|
|
1059
|
+
{ name: "--webm-space-xl", value: "3rem" },
|
|
1060
|
+
{ name: "--webm-space-2xl", value: "4rem" },
|
|
1061
|
+
{ name: "--webm-space-3xl", value: "6rem" },
|
|
1062
|
+
{ name: "--webm-space-4xl", value: "8rem" }
|
|
1063
|
+
]
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
title: "space, fluid",
|
|
1067
|
+
tokens: [
|
|
1068
|
+
{ name: "--webm-space-fluid-sm", value: "clamp(1rem, 0.8333rem + 0.8333vw, 1.5rem)" },
|
|
1069
|
+
{ name: "--webm-space-fluid-md", value: "clamp(1.5rem, 1.25rem + 1.25vw, 2.25rem)" },
|
|
1070
|
+
{ name: "--webm-space-fluid-lg", value: "clamp(2rem, 1.6667rem + 1.6667vw, 3rem)" },
|
|
1071
|
+
{ name: "--webm-space-fluid-xl", value: "clamp(3rem, 2.5rem + 2.5vw, 4.5rem)" },
|
|
1072
|
+
{ name: "--webm-space-fluid-2xl", value: "clamp(4rem, 3.3333rem + 3.3333vw, 6rem)" },
|
|
1073
|
+
{ name: "--webm-space-fluid-3xl", value: "clamp(6rem, 5rem + 5vw, 9rem)" }
|
|
1074
|
+
]
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
title: "section rhythm",
|
|
1078
|
+
tokens: [
|
|
1079
|
+
{ name: "--webm-section-sm", value: "clamp(2rem, 1.6667rem + 1.6667vw, 3rem)" },
|
|
1080
|
+
{ name: "--webm-section-md", value: "clamp(3rem, 2rem + 5vw, 6rem)" },
|
|
1081
|
+
{ name: "--webm-section-lg", value: "clamp(4rem, 2.6667rem + 6.6667vw, 8rem)" },
|
|
1082
|
+
{ name: "--webm-section-xl", value: "clamp(6rem, 4rem + 10vw, 12rem)" }
|
|
1083
|
+
]
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
title: "gutter",
|
|
1087
|
+
tokens: [{ name: "--webm-gutter", value: "clamp(1rem, 0.3333rem + 3.3333vw, 3rem)" }]
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
title: "width",
|
|
1091
|
+
tokens: [
|
|
1092
|
+
{ name: "--webm-width-max", value: "1920px" },
|
|
1093
|
+
{ name: "--webm-width-wide", value: "1600px" },
|
|
1094
|
+
{ name: "--webm-width-content", value: "1280px" },
|
|
1095
|
+
{ name: "--webm-width-narrow", value: "960px" },
|
|
1096
|
+
{ name: "--webm-width-text", value: "68ch" }
|
|
1097
|
+
]
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
title: "radius",
|
|
1101
|
+
tokens: [
|
|
1102
|
+
{ name: "--webm-radius-none", value: "0" },
|
|
1103
|
+
{ name: "--webm-radius-xs", value: "0.125rem" },
|
|
1104
|
+
{ name: "--webm-radius-sm", value: "0.25rem" },
|
|
1105
|
+
{ name: "--webm-radius-md", value: "0.5rem" },
|
|
1106
|
+
{ name: "--webm-radius-lg", value: "1rem" },
|
|
1107
|
+
{ name: "--webm-radius-xl", value: "1.5rem" },
|
|
1108
|
+
{ name: "--webm-radius-pill", value: "62.4375rem" },
|
|
1109
|
+
{ name: "--webm-radius-circle", value: "50%" }
|
|
1110
|
+
]
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
title: "border-width",
|
|
1114
|
+
tokens: [
|
|
1115
|
+
{ name: "--webm-border-width-0", value: "0" },
|
|
1116
|
+
{ name: "--webm-border-width-1", value: "1px" },
|
|
1117
|
+
{ name: "--webm-border-width-2", value: "2px" },
|
|
1118
|
+
{ name: "--webm-border-width-4", value: "4px" }
|
|
1119
|
+
]
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
title: "shadow",
|
|
1123
|
+
tokens: [
|
|
1124
|
+
{ name: "--webm-shadow-xs", value: "0 1px 2px 0 rgb(34 34 34 / 0.06)" },
|
|
1125
|
+
{
|
|
1126
|
+
name: "--webm-shadow-sm",
|
|
1127
|
+
value: "0 1px 3px 0 rgb(34 34 34 / 0.1), 0 1px 2px -1px rgb(34 34 34 / 0.1)"
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
name: "--webm-shadow-md",
|
|
1131
|
+
value: "0 4px 6px -1px rgb(34 34 34 / 0.1), 0 2px 4px -2px rgb(34 34 34 / 0.1)"
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
name: "--webm-shadow-lg",
|
|
1135
|
+
value: "0 10px 15px -3px rgb(34 34 34 / 0.1), 0 4px 6px -4px rgb(34 34 34 / 0.1)"
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: "--webm-shadow-xl",
|
|
1139
|
+
value: "0 20px 25px -5px rgb(34 34 34 / 0.12), 0 8px 10px -6px rgb(34 34 34 / 0.1)"
|
|
1140
|
+
}
|
|
1141
|
+
]
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
title: "z-index",
|
|
1145
|
+
note: "Named layers only. Never write a raw z-index in component CSS.",
|
|
1146
|
+
tokens: [
|
|
1147
|
+
{ name: "--webm-z-below", value: "-1" },
|
|
1148
|
+
{ name: "--webm-z-base", value: "0" },
|
|
1149
|
+
{ name: "--webm-z-raised", value: "10" },
|
|
1150
|
+
{ name: "--webm-z-sticky", value: "100" },
|
|
1151
|
+
{ name: "--webm-z-header", value: "200" },
|
|
1152
|
+
{ name: "--webm-z-drawer", value: "300" },
|
|
1153
|
+
{ name: "--webm-z-overlay", value: "400" },
|
|
1154
|
+
{ name: "--webm-z-modal", value: "500" },
|
|
1155
|
+
{ name: "--webm-z-toast", value: "600" },
|
|
1156
|
+
{ name: "--webm-z-tooltip", value: "700" },
|
|
1157
|
+
{ name: "--webm-z-skiplink", value: "900" }
|
|
1158
|
+
]
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
title: "duration",
|
|
1162
|
+
tokens: [
|
|
1163
|
+
{ name: "--webm-duration-instant", value: "75ms" },
|
|
1164
|
+
{ name: "--webm-duration-fast", value: "150ms" },
|
|
1165
|
+
{ name: "--webm-duration-normal", value: "250ms" },
|
|
1166
|
+
{ name: "--webm-duration-slow", value: "400ms" },
|
|
1167
|
+
{ name: "--webm-duration-slower", value: "600ms" }
|
|
1168
|
+
]
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
title: "easing",
|
|
1172
|
+
tokens: [
|
|
1173
|
+
{ name: "--webm-ease-linear", value: "linear" },
|
|
1174
|
+
{ name: "--webm-ease-out", value: "cubic-bezier(0.16, 1, 0.3, 1)" },
|
|
1175
|
+
{ name: "--webm-ease-in", value: "cubic-bezier(0.7, 0, 0.84, 0)" },
|
|
1176
|
+
{ name: "--webm-ease-in-out", value: "cubic-bezier(0.65, 0, 0.35, 1)" },
|
|
1177
|
+
{ name: "--webm-ease-spring", value: "cubic-bezier(0.34, 1.56, 0.64, 1)" }
|
|
1178
|
+
]
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
title: "scroll reveal",
|
|
1182
|
+
note: [
|
|
1183
|
+
"Defaults for .webm-reveal. Overridable per element with data-reveal-delay and",
|
|
1184
|
+
"data-reveal-duration, which observe.ts writes as inline properties.",
|
|
1185
|
+
"",
|
|
1186
|
+
"NOT var(--webm-duration-slow), and not a card-sized distance. The motion scale tops out",
|
|
1187
|
+
"at 400ms, tuned for a button or a dropdown - something small moving a short way. A reveal",
|
|
1188
|
+
"is a whole content group arriving, and at 400ms over 1.5rem it reads as a twitch.",
|
|
1189
|
+
"",
|
|
1190
|
+
"7.5rem over 1500ms was arrived at on a real client homepage, not picked. Raising duration",
|
|
1191
|
+
"alone did nothing: --webm-ease-out front-loads the curve, so extra time only stretches the",
|
|
1192
|
+
"settle. Raising distance alone lurched. Both had to grow together."
|
|
1193
|
+
].join("\n"),
|
|
1194
|
+
tokens: [
|
|
1195
|
+
{ name: "--webm-reveal-duration", value: "1500ms" },
|
|
1196
|
+
{ name: "--webm-reveal-delay", value: "0ms" },
|
|
1197
|
+
{ name: "--webm-reveal-distance", value: "7.5rem" },
|
|
1198
|
+
{ name: "--webm-reveal-ease", value: "var(--webm-ease-out)" },
|
|
1199
|
+
{ name: "--webm-reveal-blur", value: "8px" },
|
|
1200
|
+
{ name: "--webm-reveal-stagger-step", value: "80ms" }
|
|
1201
|
+
]
|
|
1202
|
+
},
|
|
1203
|
+
{
|
|
1204
|
+
title: "focus",
|
|
1205
|
+
tokens: [
|
|
1206
|
+
{ name: "--webm-focus-width", value: "3px" },
|
|
1207
|
+
{ name: "--webm-focus-offset", value: "2px" },
|
|
1208
|
+
{ name: "--webm-focus-color", value: "var(--webm-action)" }
|
|
1209
|
+
]
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
title: "composites",
|
|
1213
|
+
tokens: [
|
|
1214
|
+
{ name: "--webm-focus-ring", value: "var(--webm-focus-width) solid var(--webm-focus-color)" }
|
|
1215
|
+
]
|
|
1216
|
+
}
|
|
1217
|
+
];
|
|
1218
|
+
TOKEN_NAMES = new Set(
|
|
1219
|
+
DEFAULTS.flatMap((g) => g.tokens.map((t) => t.name))
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// src/design/compile.ts
|
|
1225
|
+
var MAPPING;
|
|
1226
|
+
var init_compile = __esm({
|
|
1227
|
+
"src/design/compile.ts"() {
|
|
1228
|
+
"use strict";
|
|
1229
|
+
init_defaults();
|
|
1230
|
+
MAPPING = [
|
|
1231
|
+
[["color", "base", "100"], "--webm-base-100"],
|
|
1232
|
+
[["color", "base", "300"], "--webm-base-300"],
|
|
1233
|
+
[["color", "base", "500"], "--webm-base-500"],
|
|
1234
|
+
[["color", "base", "700"], "--webm-base-700"],
|
|
1235
|
+
[["color", "base", "900"], "--webm-base-900"],
|
|
1236
|
+
[["color", "action", "base"], "--webm-action"],
|
|
1237
|
+
[["color", "action", "dark"], "--webm-action-dark"],
|
|
1238
|
+
[["color", "action", "light"], "--webm-action-light"],
|
|
1239
|
+
[["color", "border", "subtle"], "--webm-border-subtle"],
|
|
1240
|
+
[["color", "state", "success"], "--webm-state-success"],
|
|
1241
|
+
[["color", "state", "warning"], "--webm-state-warning"],
|
|
1242
|
+
[["color", "state", "danger"], "--webm-state-danger"],
|
|
1243
|
+
[["color", "state", "info"], "--webm-state-info"],
|
|
1244
|
+
[["font", "sans"], "--webm-font-sans"],
|
|
1245
|
+
[["font", "mono"], "--webm-font-mono"],
|
|
1246
|
+
[["radius", "none"], "--webm-radius-none"],
|
|
1247
|
+
[["radius", "xs"], "--webm-radius-xs"],
|
|
1248
|
+
[["radius", "sm"], "--webm-radius-sm"],
|
|
1249
|
+
[["radius", "md"], "--webm-radius-md"],
|
|
1250
|
+
[["radius", "lg"], "--webm-radius-lg"],
|
|
1251
|
+
[["radius", "xl"], "--webm-radius-xl"],
|
|
1252
|
+
[["radius", "pill"], "--webm-radius-pill"],
|
|
1253
|
+
[["radius", "circle"], "--webm-radius-circle"]
|
|
1254
|
+
];
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// src/cli/design-extract.ts
|
|
1259
|
+
var design_extract_exports = {};
|
|
1260
|
+
__export(design_extract_exports, {
|
|
1261
|
+
extract: () => extract,
|
|
1262
|
+
parseTokens: () => parseTokens,
|
|
1263
|
+
run: () => run3
|
|
1264
|
+
});
|
|
1265
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "node:fs";
|
|
1266
|
+
function parseTokens(css) {
|
|
1267
|
+
const source = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1268
|
+
const base = /* @__PURE__ */ new Map();
|
|
1269
|
+
const conditional = /* @__PURE__ */ new Map();
|
|
1270
|
+
const CONDITIONAL = /^@(media|supports|container)\b/;
|
|
1271
|
+
const atRules = [];
|
|
1272
|
+
const atRuleDepth = [];
|
|
1273
|
+
let depth = 0;
|
|
1274
|
+
let buffer = "";
|
|
1275
|
+
for (let i = 0; i < source.length; i++) {
|
|
1276
|
+
const char = source[i];
|
|
1277
|
+
if (char === "{") {
|
|
1278
|
+
const prelude = buffer.trim();
|
|
1279
|
+
if (CONDITIONAL.test(prelude)) {
|
|
1280
|
+
atRules.push(prelude);
|
|
1281
|
+
atRuleDepth.push(depth);
|
|
1282
|
+
}
|
|
1283
|
+
depth++;
|
|
1284
|
+
buffer = "";
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1287
|
+
if (char === "}") {
|
|
1288
|
+
depth--;
|
|
1289
|
+
if (atRuleDepth.length && atRuleDepth.at(-1) === depth) {
|
|
1290
|
+
atRules.pop();
|
|
1291
|
+
atRuleDepth.pop();
|
|
1292
|
+
}
|
|
1293
|
+
buffer = "";
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (char === ";") {
|
|
1297
|
+
const declaration = buffer.trim();
|
|
1298
|
+
const match = /^(--webm-[\w-]+)\s*:\s*([\s\S]+)$/.exec(declaration);
|
|
1299
|
+
if (match) {
|
|
1300
|
+
const [, name, value] = match;
|
|
1301
|
+
if (atRules.length === 0) {
|
|
1302
|
+
base.set(name, value.trim());
|
|
1303
|
+
} else {
|
|
1304
|
+
conditional.set(name, [...conditional.get(name) ?? [], atRules.join(" / ")]);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
buffer = "";
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
buffer += char;
|
|
1311
|
+
}
|
|
1312
|
+
return { base, conditional };
|
|
1313
|
+
}
|
|
1314
|
+
function setPath(target, path, value) {
|
|
1315
|
+
let node = target;
|
|
1316
|
+
for (const key of path.slice(0, -1)) {
|
|
1317
|
+
node[key] ??= {};
|
|
1318
|
+
node = node[key];
|
|
1319
|
+
}
|
|
1320
|
+
node[path.at(-1)] = value;
|
|
1321
|
+
}
|
|
1322
|
+
function extract(css) {
|
|
1323
|
+
const { base: found, conditional } = parseTokens(css);
|
|
1324
|
+
const design = { version: 1 };
|
|
1325
|
+
const mapped = [];
|
|
1326
|
+
const overridden = [];
|
|
1327
|
+
const added = [];
|
|
1328
|
+
let unchanged = 0;
|
|
1329
|
+
for (const [name, value] of found) {
|
|
1330
|
+
const isKnown = DEFAULT_VALUES.has(name);
|
|
1331
|
+
if (isKnown && DEFAULT_VALUES.get(name) === value) {
|
|
1332
|
+
unchanged++;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
const path = REVERSE.get(name);
|
|
1336
|
+
if (path) {
|
|
1337
|
+
setPath(design, path, value);
|
|
1338
|
+
mapped.push(name);
|
|
1339
|
+
} else {
|
|
1340
|
+
design.overrides ??= {};
|
|
1341
|
+
design.overrides[name] = value;
|
|
1342
|
+
(isKnown ? overridden : added).push(name);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return { design, mapped, overridden, added, unchanged, conditional };
|
|
1346
|
+
}
|
|
1347
|
+
function run3(argv2) {
|
|
1348
|
+
const input = argv2[0] ?? "src/styles/tokens.css";
|
|
1349
|
+
const output = argv2[1] ?? "design.json";
|
|
1350
|
+
if (!existsSync5(input)) {
|
|
1351
|
+
console.error(`webm design:extract: no such file: ${input}`);
|
|
1352
|
+
console.error(` usage: webm design:extract [tokens.css] [design.json]`);
|
|
1353
|
+
return 1;
|
|
1354
|
+
}
|
|
1355
|
+
if (existsSync5(output)) {
|
|
1356
|
+
console.error(`webm design:extract: ${output} already exists. Move it aside first.`);
|
|
1357
|
+
return 1;
|
|
1358
|
+
}
|
|
1359
|
+
const result = extract(readFileSync4(input, "utf8"));
|
|
1360
|
+
const json = JSON.stringify(
|
|
1361
|
+
{ $schema: "./node_modules/@cparkerwebm/webmonterey/schema/design.json", ...result.design },
|
|
1362
|
+
null,
|
|
1363
|
+
2
|
|
1364
|
+
);
|
|
1365
|
+
writeFileSync4(output, json + "\n");
|
|
1366
|
+
console.log(`Read ${input}`);
|
|
1367
|
+
console.log(` ${result.unchanged} tokens match the defaults and were omitted`);
|
|
1368
|
+
if (result.mapped.length) {
|
|
1369
|
+
console.log(` ${result.mapped.length} mapped to structured fields:`);
|
|
1370
|
+
for (const n of result.mapped) console.log(` ${n}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (result.overridden.length) {
|
|
1373
|
+
console.log(` ${result.overridden.length} kept as overrides (no structured field exists):`);
|
|
1374
|
+
for (const n of result.overridden) console.log(` ${n}`);
|
|
1375
|
+
}
|
|
1376
|
+
if (result.added.length) {
|
|
1377
|
+
console.log(` ${result.added.length} are site tokens, not part of the default set:`);
|
|
1378
|
+
for (const n of result.added) console.log(` ${n}`);
|
|
1379
|
+
}
|
|
1380
|
+
console.log(`
|
|
1381
|
+
Wrote ${output}`);
|
|
1382
|
+
if (result.conditional.size) {
|
|
1383
|
+
console.log(
|
|
1384
|
+
`
|
|
1385
|
+
${result.conditional.size} tokens are ALSO set inside an at-rule. design.json holds one
|
|
1386
|
+
flat :root block, so those conditional values are NOT in it and must stay in CSS:
|
|
1387
|
+
`
|
|
1388
|
+
);
|
|
1389
|
+
for (const [name, rules] of result.conditional) {
|
|
1390
|
+
console.log(` ${name} ${[...new Set(rules)].join(", ")}`);
|
|
1391
|
+
}
|
|
1392
|
+
console.log(
|
|
1393
|
+
`
|
|
1394
|
+
Move those blocks to src/styles/custom/_index.css. Leaving them behind silently
|
|
1395
|
+
drops reduced-motion handling and every responsive token override.`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
console.log(
|
|
1399
|
+
`
|
|
1400
|
+
Next: delete ${input}, and check the compiled output matches what the site rendered before.`
|
|
1401
|
+
);
|
|
1402
|
+
return 0;
|
|
1403
|
+
}
|
|
1404
|
+
var DEFAULT_VALUES, REVERSE;
|
|
1405
|
+
var init_design_extract = __esm({
|
|
1406
|
+
"src/cli/design-extract.ts"() {
|
|
1407
|
+
"use strict";
|
|
1408
|
+
init_defaults();
|
|
1409
|
+
init_compile();
|
|
1410
|
+
DEFAULT_VALUES = new Map(DEFAULTS.flatMap((g) => g.tokens.map((t) => [t.name, t.value])));
|
|
1411
|
+
REVERSE = new Map(MAPPING.map(([path, token]) => [token, path]));
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// src/cli/compare.ts
|
|
1416
|
+
var compare_exports = {};
|
|
1417
|
+
__export(compare_exports, {
|
|
1418
|
+
compare: () => compare,
|
|
1419
|
+
cssDeclarations: () => cssDeclarations,
|
|
1420
|
+
headTags: () => headTags,
|
|
1421
|
+
jsonLd: () => jsonLd,
|
|
1422
|
+
run: () => run4,
|
|
1423
|
+
visibleText: () => visibleText
|
|
1424
|
+
});
|
|
1425
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync as readdirSync4, statSync } from "node:fs";
|
|
1426
|
+
import { join as join5, relative as relative2 } from "node:path";
|
|
1427
|
+
function walk2(dir, match) {
|
|
1428
|
+
if (!existsSync6(dir)) return [];
|
|
1429
|
+
return readdirSync4(dir, { withFileTypes: true }).flatMap((e) => {
|
|
1430
|
+
const full = join5(dir, e.name);
|
|
1431
|
+
return e.isDirectory() ? walk2(full, match) : match(e.name) ? [full] : [];
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
function visibleText(html) {
|
|
1435
|
+
return html.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<[^>]+>/g, " ").replace(/&[a-z]+;|&#\d+;/gi, " ").replace(/\s+/g, " ").trim();
|
|
1436
|
+
}
|
|
1437
|
+
function headTags(html) {
|
|
1438
|
+
const head = html.slice(0, html.indexOf("</head>") + 1);
|
|
1439
|
+
return [...head.matchAll(/<(meta|link)\b[^>]*>/gi)].map(
|
|
1440
|
+
(m) => m[0].replace(/\s+/g, " ").replace(/\.[A-Za-z0-9_-]{8,}\.(css|js|woff2?|png|jpg|webp|svg)/g, ".$1").replace(/(\/_server-islands\/[A-Za-z0-9_-]+)\?[^"']*/g, "$1")
|
|
1441
|
+
).sort();
|
|
1442
|
+
}
|
|
1443
|
+
function jsonLd(html) {
|
|
1444
|
+
const blocks = [
|
|
1445
|
+
...html.matchAll(/<script[^>]+application\/ld\+json[^>]*>([\s\S]*?)<\/script>/gi)
|
|
1446
|
+
];
|
|
1447
|
+
try {
|
|
1448
|
+
return JSON.stringify(
|
|
1449
|
+
blocks.map((b) => JSON.parse(b[1])),
|
|
1450
|
+
null,
|
|
1451
|
+
1
|
|
1452
|
+
);
|
|
1453
|
+
} catch {
|
|
1454
|
+
return blocks.map((b) => b[1]).join("\n");
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
function cssDeclarations(dist) {
|
|
1458
|
+
const inline = walk2(dist, (f) => f.endsWith(".html")).flatMap((f) => [...readFileSync5(f, "utf8").matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)]).map((m) => m[1]);
|
|
1459
|
+
const css = [
|
|
1460
|
+
...walk2(join5(dist, "_astro"), (f) => f.endsWith(".css")).map((f) => readFileSync5(f, "utf8")),
|
|
1461
|
+
...inline
|
|
1462
|
+
].join("\n").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\[data-astro-cid-[a-z0-9]+\]/gi, "");
|
|
1463
|
+
return new Set(
|
|
1464
|
+
css.split(/[{};]/).map((d) => d.trim()).filter((d) => d.includes(":") && d.length < 300)
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
function compare(oldDist, newDist) {
|
|
1468
|
+
const diffs = [];
|
|
1469
|
+
const pages = walk2(oldDist, (f) => f.endsWith(".html")).map((f) => relative2(oldDist, f));
|
|
1470
|
+
for (const page of pages) {
|
|
1471
|
+
const a = readFileSync5(join5(oldDist, page), "utf8");
|
|
1472
|
+
const bPath = join5(newDist, page);
|
|
1473
|
+
if (!existsSync6(bPath)) {
|
|
1474
|
+
diffs.push({ layer: "text", page, missing: ["THE WHOLE PAGE"], added: [] });
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
const b = readFileSync5(bPath, "utf8");
|
|
1478
|
+
const at = words(visibleText(a));
|
|
1479
|
+
const bt = words(visibleText(b));
|
|
1480
|
+
if (setDiff(at, bt).length || setDiff(bt, at).length) {
|
|
1481
|
+
diffs.push({ layer: "text", page, missing: setDiff(at, bt), added: setDiff(bt, at) });
|
|
1482
|
+
}
|
|
1483
|
+
const ah = new Set(headTags(a));
|
|
1484
|
+
const bh = new Set(headTags(b));
|
|
1485
|
+
if (setDiff(ah, bh).length || setDiff(bh, ah).length) {
|
|
1486
|
+
diffs.push({ layer: "head", page, missing: setDiff(ah, bh), added: setDiff(bh, ah) });
|
|
1487
|
+
}
|
|
1488
|
+
if (jsonLd(a) !== jsonLd(b)) {
|
|
1489
|
+
diffs.push({ layer: "jsonld", page, missing: ["graph differs"], added: [] });
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const ac = cssDeclarations(oldDist);
|
|
1493
|
+
const bc = cssDeclarations(newDist);
|
|
1494
|
+
const lost = setDiff(ac, bc);
|
|
1495
|
+
const gained = setDiff(bc, ac);
|
|
1496
|
+
if (lost.length) {
|
|
1497
|
+
diffs.push({ layer: "css", page: "(bundle)", missing: lost, added: gained });
|
|
1498
|
+
} else if (gained.length) {
|
|
1499
|
+
diffs.push({ layer: "css", page: "(bundle)", missing: [], added: gained, info: true });
|
|
1500
|
+
}
|
|
1501
|
+
return diffs;
|
|
1502
|
+
}
|
|
1503
|
+
function run4(argv2) {
|
|
1504
|
+
const [oldDist, newDist] = argv2;
|
|
1505
|
+
if (!oldDist || !newDist) {
|
|
1506
|
+
console.error("webm compare <old-dist/client> <new-dist/client>");
|
|
1507
|
+
console.error("\n Build both sites first. Compares text, head tags, JSON-LD and CSS.");
|
|
1508
|
+
return 1;
|
|
1509
|
+
}
|
|
1510
|
+
for (const d of [oldDist, newDist]) {
|
|
1511
|
+
if (!existsSync6(d) || !statSync(d).isDirectory()) {
|
|
1512
|
+
console.error(`webm compare: no such directory: ${d}`);
|
|
1513
|
+
return 1;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const diffs = compare(oldDist, newDist);
|
|
1517
|
+
const pages = walk2(oldDist, (f) => f.endsWith(".html")).length;
|
|
1518
|
+
const regressions = diffs.filter((d) => !d.info);
|
|
1519
|
+
if (!diffs.length) {
|
|
1520
|
+
console.log(`webm compare: ${pages} pages, no differences in text, head, JSON-LD or CSS.`);
|
|
1521
|
+
return 0;
|
|
1522
|
+
}
|
|
1523
|
+
for (const d of diffs) {
|
|
1524
|
+
console.log(`
|
|
1525
|
+
${d.layer.toUpperCase()}${d.info ? " (not a regression)" : ""} ${d.page}`);
|
|
1526
|
+
for (const m of d.missing.slice(0, 25)) console.log(` - ${m.slice(0, 160)}`);
|
|
1527
|
+
if (d.missing.length > 25) console.log(` \u2026 ${d.missing.length - 25} more missing`);
|
|
1528
|
+
for (const a of d.added.slice(0, 10)) console.log(` + ${a.slice(0, 160)}`);
|
|
1529
|
+
if (d.added.length > 10) console.log(` \u2026 ${d.added.length - 10} more added`);
|
|
1530
|
+
}
|
|
1531
|
+
if (!regressions.length) {
|
|
1532
|
+
console.log(
|
|
1533
|
+
`
|
|
1534
|
+
webm compare: ${pages} pages, no regressions in text, head, JSON-LD or CSS. The additions above are what the new build serves and the old did not.`
|
|
1535
|
+
);
|
|
1536
|
+
return 0;
|
|
1537
|
+
}
|
|
1538
|
+
console.log(
|
|
1539
|
+
`
|
|
1540
|
+
${regressions.length} differences across ${pages} pages. A "-" line is something the old site served and the new one does not.`
|
|
1541
|
+
);
|
|
1542
|
+
return 1;
|
|
1543
|
+
}
|
|
1544
|
+
var setDiff, words;
|
|
1545
|
+
var init_compare = __esm({
|
|
1546
|
+
"src/cli/compare.ts"() {
|
|
1547
|
+
"use strict";
|
|
1548
|
+
setDiff = (a, b) => [...a].filter((x) => !b.has(x));
|
|
1549
|
+
words = (s) => new Set(s.split(" ").filter(Boolean));
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// src/includes/webmonterey/config.ts
|
|
1554
|
+
function isConfigured(value) {
|
|
1555
|
+
return typeof value === "string" && value.length > 0 && value !== PLACEHOLDER;
|
|
1556
|
+
}
|
|
1557
|
+
function isValidTimeZone(zone) {
|
|
1558
|
+
try {
|
|
1559
|
+
new Intl.DateTimeFormat("en-US", { timeZone: zone });
|
|
1560
|
+
return true;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function appEnabled(config) {
|
|
1566
|
+
return config.app?.enabled === true;
|
|
1567
|
+
}
|
|
1568
|
+
function resolveAppPath(config) {
|
|
1569
|
+
const raw = (config.app?.path ?? APP_DIR).replace(/^\/+|\/+$/g, "");
|
|
1570
|
+
return raw || APP_DIR;
|
|
1571
|
+
}
|
|
1572
|
+
function workerFirstPaths(config) {
|
|
1573
|
+
const paths = ["/_actions/*"];
|
|
1574
|
+
if (appEnabled(config)) {
|
|
1575
|
+
const app = resolveAppPath(config);
|
|
1576
|
+
paths.push(`/${app}/*`, `/${app}`, `/${app}/`);
|
|
1577
|
+
}
|
|
1578
|
+
return paths;
|
|
1579
|
+
}
|
|
1580
|
+
var APP_DIR, PLACEHOLDER;
|
|
1581
|
+
var init_config = __esm({
|
|
1582
|
+
"src/includes/webmonterey/config.ts"() {
|
|
1583
|
+
"use strict";
|
|
1584
|
+
APP_DIR = "webapp";
|
|
1585
|
+
PLACEHOLDER = "CHANGEME";
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
// src/cli/checks.ts
|
|
1590
|
+
function stripComments(source) {
|
|
1591
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1").replace(/<!--[\s\S]*?-->/g, " ");
|
|
1592
|
+
}
|
|
1593
|
+
function onDemandRoutes(pages) {
|
|
1594
|
+
const out = [];
|
|
1595
|
+
for (const [file, source] of pages) {
|
|
1596
|
+
if (!/export\s+const\s+prerender\s*=\s*false/.test(source)) continue;
|
|
1597
|
+
const route = "/" + file.replace(/^.*src\/pages\//, "").replace(/\.(astro|ts|js|md)$/, "").replace(/\/index$/, "").replace(/^index$/, "");
|
|
1598
|
+
out.push(route === "/" ? "/" : route);
|
|
1599
|
+
}
|
|
1600
|
+
return out;
|
|
1601
|
+
}
|
|
1602
|
+
function actionsCalled(sources) {
|
|
1603
|
+
const out = /* @__PURE__ */ new Map();
|
|
1604
|
+
for (const [file, raw] of sources) {
|
|
1605
|
+
if (!/from\s+['"]astro:actions['"]/.test(raw)) continue;
|
|
1606
|
+
const src = stripComments(raw);
|
|
1607
|
+
for (const m of src.matchAll(/\bactions\.([a-zA-Z][a-zA-Z0-9]*)/g)) {
|
|
1608
|
+
const name = m[1];
|
|
1609
|
+
out.set(name, [...out.get(name) ?? [], file]);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return out;
|
|
1613
|
+
}
|
|
1614
|
+
function actionsExported(actions) {
|
|
1615
|
+
const names = /* @__PURE__ */ new Set();
|
|
1616
|
+
for (const [, raw] of actions) {
|
|
1617
|
+
const src = stripComments(raw);
|
|
1618
|
+
for (const m of src.matchAll(/([a-zA-Z][a-zA-Z0-9]*)\s*:\s*defineAction\b/g)) names.add(m[1]);
|
|
1619
|
+
for (const m of src.matchAll(/export\s+const\s+([a-zA-Z][a-zA-Z0-9]*)\s*=\s*defineAction\b/g))
|
|
1620
|
+
names.add(m[1]);
|
|
1621
|
+
const server = src.match(/export\s+const\s+server\s*=\s*\{([\s\S]*?)\n\}/);
|
|
1622
|
+
if (server) {
|
|
1623
|
+
for (const m of server[1].matchAll(/^\s*([a-zA-Z][a-zA-Z0-9]*)\s*(?:,|:)/gm)) names.add(m[1]);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
return [...names];
|
|
1627
|
+
}
|
|
1628
|
+
function tablesCreated(migrations) {
|
|
1629
|
+
const names = /* @__PURE__ */ new Set();
|
|
1630
|
+
for (const [, raw] of migrations) {
|
|
1631
|
+
for (const m of stripComments(raw).matchAll(
|
|
1632
|
+
/CREATE\s+(?:TABLE|VIEW)(?:\s+IF\s+NOT\s+EXISTS)?\s+[`"\[]?([a-zA-Z_][a-zA-Z0-9_]*)/gi
|
|
1633
|
+
))
|
|
1634
|
+
names.add(m[1].toLowerCase());
|
|
1635
|
+
}
|
|
1636
|
+
return [...names];
|
|
1637
|
+
}
|
|
1638
|
+
function tablesReferenced(sources) {
|
|
1639
|
+
const out = /* @__PURE__ */ new Map();
|
|
1640
|
+
for (const group of sources) {
|
|
1641
|
+
for (const [file, raw] of group) {
|
|
1642
|
+
for (const m of stripComments(raw).matchAll(
|
|
1643
|
+
/\b(?:FROM|INSERT\s+INTO|UPDATE|DELETE\s+FROM|JOIN)\s+[`"\[]?([a-z_][a-zA-Z0-9_]*)/g
|
|
1644
|
+
)) {
|
|
1645
|
+
const name = m[1].toLowerCase();
|
|
1646
|
+
if (name === "sqlite_master" || name.startsWith("sqlite_")) continue;
|
|
1647
|
+
out.set(name, [...out.get(name) ?? [], file]);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return out;
|
|
1652
|
+
}
|
|
1653
|
+
var pass, fail, warn, PACKAGE_ACTIONS, CHECKS;
|
|
1654
|
+
var init_checks = __esm({
|
|
1655
|
+
"src/cli/checks.ts"() {
|
|
1656
|
+
"use strict";
|
|
1657
|
+
init_config();
|
|
1658
|
+
init_mcp();
|
|
1659
|
+
pass = { status: "pass" };
|
|
1660
|
+
fail = (detail) => ({ status: "fail", detail });
|
|
1661
|
+
warn = (detail) => ({ status: "warn", detail });
|
|
1662
|
+
PACKAGE_ACTIONS = ["submitForm"];
|
|
1663
|
+
CHECKS = [
|
|
1664
|
+
{
|
|
1665
|
+
id: "actions-exist",
|
|
1666
|
+
title: "Every action a page calls is one the server exports",
|
|
1667
|
+
silentAs: "the button posts to an action that is not there - no build error, and nothing said at runtime until somebody submits",
|
|
1668
|
+
run(ctx) {
|
|
1669
|
+
const called = actionsCalled(new Map([...ctx.pages, ...ctx.components, ...ctx.includes]));
|
|
1670
|
+
if (called.size === 0) return pass;
|
|
1671
|
+
const available = /* @__PURE__ */ new Set([...PACKAGE_ACTIONS, ...actionsExported(ctx.actions)]);
|
|
1672
|
+
const missing = [...called].filter(([name]) => !available.has(name));
|
|
1673
|
+
if (missing.length === 0) return pass;
|
|
1674
|
+
return fail(
|
|
1675
|
+
missing.map(([name, files]) => `actions.${name} (${files[0]}) is not exported by src/actions/`).join("; ")
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
id: "migrations-cover-tables",
|
|
1681
|
+
title: "Every table the code reads has a migration that creates it",
|
|
1682
|
+
silentAs: "production keeps working against tables that already exist there while local dev has none - and the repo can no longer rebuild its own database",
|
|
1683
|
+
run(ctx) {
|
|
1684
|
+
if (!ctx.site.features?.d1) return pass;
|
|
1685
|
+
if (ctx.migrations.size === 0) return pass;
|
|
1686
|
+
const created = new Set(tablesCreated(ctx.migrations));
|
|
1687
|
+
const referenced = tablesReferenced([ctx.includes, ctx.pages, ctx.components, ctx.actions]);
|
|
1688
|
+
const missing = [...referenced].filter(([t]) => !created.has(t));
|
|
1689
|
+
if (missing.length === 0) return pass;
|
|
1690
|
+
return fail(
|
|
1691
|
+
missing.map(([t, files]) => `${t} (used in ${files[0]}) has no CREATE in migrations/`).join("; ")
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
id: "d1-binding",
|
|
1697
|
+
title: "features.d1 has a database bound to write to",
|
|
1698
|
+
silentAs: "the form validates, thanks the visitor and stores nothing, because the binding it writes through does not exist",
|
|
1699
|
+
run(ctx) {
|
|
1700
|
+
if (!ctx.site.features?.d1) return pass;
|
|
1701
|
+
const bindings = ctx.wrangler?.d1_databases ?? [];
|
|
1702
|
+
if (bindings.some((b) => b.binding === "DB" && b.database_id)) return pass;
|
|
1703
|
+
return fail(
|
|
1704
|
+
bindings.length === 0 ? "features.d1 is true and wrangler has no d1_databases block" : "features.d1 is true but no d1_databases entry binds DB with a database_id"
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
id: "run-worker-first",
|
|
1710
|
+
title: "Every on-demand route is in run_worker_first, in both slash forms",
|
|
1711
|
+
silentAs: "200 to curl, a 404 page in Chrome - the router branches on Sec-Fetch-Dest",
|
|
1712
|
+
run(ctx) {
|
|
1713
|
+
const listed = ctx.wrangler?.assets?.run_worker_first ?? [];
|
|
1714
|
+
const missing = [];
|
|
1715
|
+
const appPath = appEnabled(ctx.site) ? resolveAppPath(ctx.site) : APP_DIR;
|
|
1716
|
+
const publicRoute = (route) => route === `/${APP_DIR}` || route.startsWith(`/${APP_DIR}/`) ? `/${appPath}${route.slice(APP_DIR.length + 1)}` : route;
|
|
1717
|
+
for (const route of onDemandRoutes(ctx.pages).map(publicRoute)) {
|
|
1718
|
+
const covered = (p) => listed.some(
|
|
1719
|
+
(entry2) => entry2 === p || entry2.endsWith("/*") && p.startsWith(entry2.slice(0, -1))
|
|
1720
|
+
);
|
|
1721
|
+
for (const form of [route, `${route}/`]) if (!covered(form)) missing.push(form);
|
|
1722
|
+
}
|
|
1723
|
+
return missing.length ? fail(`not listed: ${[...new Set(missing)].join(", ")}`) : pass;
|
|
1724
|
+
}
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
id: "app-namespace",
|
|
1728
|
+
title: "The web app namespace is wired for the path it is served at",
|
|
1729
|
+
silentAs: "the app 404s in a browser and works in curl, a marketing page shadows the portal, or a portal page prerenders and never sees a binding",
|
|
1730
|
+
run(ctx) {
|
|
1731
|
+
const appPages = [...ctx.pages.keys()].filter((f) => f.includes(`src/pages/${APP_DIR}/`));
|
|
1732
|
+
if (!appEnabled(ctx.site)) {
|
|
1733
|
+
return appPages.length ? warn(
|
|
1734
|
+
`src/pages/${APP_DIR}/ has ${appPages.length} route(s) but app.enabled is false in webmonterey.json, so nothing derives the noindex, sitemap exclusion or run_worker_first entries for them. Switch it on, or move the routes.`
|
|
1735
|
+
) : pass;
|
|
1736
|
+
}
|
|
1737
|
+
const path = resolveAppPath(ctx.site);
|
|
1738
|
+
const problems = [];
|
|
1739
|
+
const listed = new Set(ctx.wrangler?.assets?.run_worker_first ?? []);
|
|
1740
|
+
const needed = workerFirstPaths(ctx.site).filter((p) => p !== "/_actions/*");
|
|
1741
|
+
const absent = needed.filter((p) => !listed.has(p));
|
|
1742
|
+
if (absent.length) problems.push(`run_worker_first is missing ${absent.join(", ")}`);
|
|
1743
|
+
if (ctx.contentPages.includes(path)) {
|
|
1744
|
+
problems.push(`src/content/pages/${path}.json collides with app.path "${path}"`);
|
|
1745
|
+
}
|
|
1746
|
+
const prerendered = appPages.filter(
|
|
1747
|
+
(f) => !/export\s+const\s+prerender\s*=\s*false/.test(ctx.pages.get(f) ?? "")
|
|
1748
|
+
);
|
|
1749
|
+
if (prerendered.length) {
|
|
1750
|
+
problems.push(`not \`prerender = false\`: ${prerendered.join(", ")}`);
|
|
1751
|
+
}
|
|
1752
|
+
return problems.length ? fail(problems.join("; ")) : pass;
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
id: "changeme",
|
|
1757
|
+
title: "No CHANGEME left in webmonterey.json",
|
|
1758
|
+
silentAs: "a placeholder in a credit link, an email subject, or structured data",
|
|
1759
|
+
run(ctx) {
|
|
1760
|
+
const stale = Object.entries(ctx.site).filter(([, v]) => v === PLACEHOLDER).map(([k]) => k);
|
|
1761
|
+
return stale.length ? fail(`still placeholder: ${stale.join(", ")}`) : pass;
|
|
1762
|
+
}
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
id: "timezone",
|
|
1766
|
+
title: "timeZone is a real IANA zone",
|
|
1767
|
+
silentAs: "Intl.DateTimeFormat throws at runtime, in a Worker, not on your laptop",
|
|
1768
|
+
run(ctx) {
|
|
1769
|
+
const zone = ctx.site.timeZone;
|
|
1770
|
+
if (!zone) return warn("unset - defaults to America/Los_Angeles");
|
|
1771
|
+
return isValidTimeZone(zone) ? pass : fail(
|
|
1772
|
+
`"${zone}" is not a zone Intl knows. There is no Pacific/LA; the Pacific/* zones are ocean locations.`
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
id: "skills-synced",
|
|
1778
|
+
title: "Fleet skills are materialized and match the installed version",
|
|
1779
|
+
silentAs: "no /webm: skills in the session, and no error saying why",
|
|
1780
|
+
run(ctx) {
|
|
1781
|
+
if (!ctx.sync) {
|
|
1782
|
+
return fail(
|
|
1783
|
+
".claude/skills/webm/ is missing. `npm install --ignore-scripts` skips the postinstall sync silently - run `npx webm sync`."
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return ctx.sync.version === ctx.version ? pass : warn(`synced v${ctx.sync.version}, installed v${ctx.version} - run \`npx webm sync\``);
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
/*
|
|
1791
|
+
* THE DOCS SERVERS, AND WHY A CHECK RATHER THAN TRUST.
|
|
1792
|
+
*
|
|
1793
|
+
* Astro ships majors faster than any training corpus turns over and the web platform moves
|
|
1794
|
+
* continuously, so a session working from recall writes code against an API that changed. The
|
|
1795
|
+
* servers are the fix; this check is what notices when a site has quietly lost them - a
|
|
1796
|
+
* merge that dropped .mcp.json, a settings file rewritten by hand, a site scaffolded before
|
|
1797
|
+
* the server existed. None of those announce themselves. The site keeps building, and the
|
|
1798
|
+
* only symptom is worse code, months later, with nothing to point at.
|
|
1799
|
+
*
|
|
1800
|
+
* DECLARED IS NOT ENOUGH. A server missing from `enabledMcpjsonServers` prompts for approval
|
|
1801
|
+
* on every machine, so the rule that depends on it holds only for whoever happened to hit
|
|
1802
|
+
* Approve. That is reported separately because the fix is different.
|
|
1803
|
+
*/
|
|
1804
|
+
id: "mcp-docs",
|
|
1805
|
+
title: "The four documentation servers are wired and pre-approved",
|
|
1806
|
+
silentAs: "code written from training-data recall instead of current docs",
|
|
1807
|
+
run(ctx) {
|
|
1808
|
+
if (!ctx.mcp.declared) return fail("no .mcp.json - run `npx webm upgrade` to restore it");
|
|
1809
|
+
const { undeclared, unapproved, wrongUrl } = mcpGaps(ctx.mcp.declared, ctx.mcp.enabled);
|
|
1810
|
+
const problems = [
|
|
1811
|
+
undeclared.length && `missing from .mcp.json: ${undeclared.join(", ")}`,
|
|
1812
|
+
unapproved.length && `declared but not in enabledMcpjsonServers, so inert until someone approves them by hand: ${unapproved.join(", ")}`,
|
|
1813
|
+
wrongUrl.length && wrongUrl.map(
|
|
1814
|
+
(n) => `${n} points at ${ctx.mcp.declared?.[n]?.url}, expected ${MCP_SERVERS[n].url}`
|
|
1815
|
+
).join("; ")
|
|
1816
|
+
].filter(Boolean);
|
|
1817
|
+
return problems.length ? fail(`${problems.join(". ")}. \`npx webm upgrade\` adds what is missing, in place.`) : pass;
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
id: "staging-email",
|
|
1822
|
+
title: "A staging site has somewhere to send its mail",
|
|
1823
|
+
silentAs: "every form submission on the preview throws instead of arriving anywhere",
|
|
1824
|
+
run(ctx) {
|
|
1825
|
+
if (ctx.site.environment !== "staging") return pass;
|
|
1826
|
+
return isConfigured(ctx.site.stagingEmail) ? pass : fail(
|
|
1827
|
+
'environment is "staging" but stagingEmail is empty. Every message this site tries to send will throw. Set "stagingEmail" in webmonterey.json to the inbox that should receive test mail.'
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
id: "select-element",
|
|
1833
|
+
title: "No querySelector<HTMLSelectElement>",
|
|
1834
|
+
silentAs: "ts(2344), which reads like a typo rather than a type conflict",
|
|
1835
|
+
run(ctx) {
|
|
1836
|
+
const hits = [];
|
|
1837
|
+
for (const [file, src] of [...ctx.pages, ...ctx.components]) {
|
|
1838
|
+
if (/querySelector<\s*HTMLSelectElement/.test(stripComments(src))) hits.push(file);
|
|
1839
|
+
}
|
|
1840
|
+
return hits.length ? fail(
|
|
1841
|
+
`${hits.join(", ")} - HTMLRewriter's Element interface merges with lib.dom's, and only HTMLSelectElement shadows remove(). Cast instead: \`as HTMLSelectElement | null\`.`
|
|
1842
|
+
) : pass;
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
id: "image-on-demand",
|
|
1847
|
+
title: "No <Image> or getImage on an on-demand route",
|
|
1848
|
+
silentAs: "a dead /_image URL, in production only - astro dev serves it happily",
|
|
1849
|
+
run(ctx) {
|
|
1850
|
+
const routes = new Set(onDemandRoutes(ctx.pages));
|
|
1851
|
+
const hits = [];
|
|
1852
|
+
for (const [file, src] of ctx.pages) {
|
|
1853
|
+
if (!/export\s+const\s+prerender\s*=\s*false/.test(src)) continue;
|
|
1854
|
+
const code = stripComments(src);
|
|
1855
|
+
if (/<Image\b|getImage\s*\(/.test(code) && !/Astro\.isPrerendered/.test(code))
|
|
1856
|
+
hits.push(file);
|
|
1857
|
+
}
|
|
1858
|
+
return hits.length ? fail(
|
|
1859
|
+
`${hits.join(", ")} - imageService: 'compile' ships no runtime endpoint. Branch on Astro.isPrerendered and fall back to a plain <img>. (${routes.size} on-demand routes)`
|
|
1860
|
+
) : pass;
|
|
1861
|
+
}
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
id: "compatibility-date",
|
|
1865
|
+
title: "compatibility_date is not stale",
|
|
1866
|
+
silentAs: 'EVERY page renders as the literal text "[object Object]" - build succeeds, no error',
|
|
1867
|
+
run(ctx) {
|
|
1868
|
+
const date = ctx.wrangler?.compatibility_date;
|
|
1869
|
+
if (!date) return ctx.wrangler ? warn("wrangler.jsonc sets no compatibility_date") : pass;
|
|
1870
|
+
const age = (Date.parse(ctx.today) - Date.parse(date)) / 864e5;
|
|
1871
|
+
if (Number.isNaN(age)) return warn(`compatibility_date is not a date: ${date}`);
|
|
1872
|
+
if (age < 0) {
|
|
1873
|
+
return fail(
|
|
1874
|
+
`compatibility_date is ${date}, which is in the FUTURE. The runtime bundled with your wrangler refuses a date it does not know, so the site will not build - miniflare reports ERR_FUTURE_COMPATIBILITY_DATE. Set it to a date at least a fortnight behind today, which is what \`webm new\` now scaffolds.`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
if (age > 180) {
|
|
1878
|
+
return fail(
|
|
1879
|
+
`compatibility_date is ${date}, ${Math.round(age)} days old. Run \`npm run preview\` and open a page: if it says "[object Object]", this is why. Bump it to today, redeploy, and check the site still behaves - the date pins runtime behavior.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
if (age > 90) {
|
|
1883
|
+
return warn(
|
|
1884
|
+
`compatibility_date is ${date}, ${Math.round(age)} days old. Not broken yet. Bump it on the next deploy, and verify on a preview URL rather than in production.`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
return pass;
|
|
1888
|
+
}
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
id: "cron-without-handler",
|
|
1892
|
+
title: "A Cron Trigger has a scheduled() handler to run",
|
|
1893
|
+
silentAs: "the cron fires on schedule and does nothing at all, forever",
|
|
1894
|
+
run(ctx) {
|
|
1895
|
+
const crons = ctx.wrangler?.triggers?.crons ?? [];
|
|
1896
|
+
if (crons.length === 0) return pass;
|
|
1897
|
+
const main = ctx.wrangler?.main;
|
|
1898
|
+
if (!main) {
|
|
1899
|
+
return fail(
|
|
1900
|
+
`wrangler.jsonc declares ${crons.length} Cron Trigger(s) (${crons.join(", ")}) but sets no "main". The adapter's generated Worker exports only fetch, so Cloudflare invokes a scheduled handler that does not exist and nothing reports it. Point "main" at a source entrypoint that re-exports @astrojs/cloudflare/handler and adds scheduled().`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
if (/dist\//.test(main)) {
|
|
1904
|
+
return fail(
|
|
1905
|
+
`"main" is ${main}, which points into the build output. It must name a SOURCE entrypoint - the adapter builds it. The Pages-era ./dist/_worker.js/index.js is the value not to use.`
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
if (ctx.workerEntry === null) {
|
|
1909
|
+
return warn(`"main" is ${main} but that file was not found from the site root.`);
|
|
1910
|
+
}
|
|
1911
|
+
const source = stripComments(ctx.workerEntry);
|
|
1912
|
+
if (!/\bscheduled\b\s*[(:]/.test(source)) {
|
|
1913
|
+
return fail(
|
|
1914
|
+
`${main} does not export a scheduled() handler, so the ${crons.length} declared cron(s) have nothing to run.`
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
if (!/@astrojs\/cloudflare\/handler/.test(source) && !/\bdefineWorker\b/.test(source)) {
|
|
1918
|
+
return fail(
|
|
1919
|
+
`${main} neither calls defineWorker nor imports handle from @astrojs/cloudflare/handler. Replacing the generated entrypoint without one of them means every page, action and API route on the site stops being served - the cron would work and nothing else would.`
|
|
1920
|
+
);
|
|
1921
|
+
}
|
|
1922
|
+
return pass;
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
id: "block-types-registered",
|
|
1927
|
+
title: "Every block type used in content is in the registry",
|
|
1928
|
+
silentAs: "the block renders as NOTHING - no error, no warning, no gap in the page",
|
|
1929
|
+
run(ctx) {
|
|
1930
|
+
if (ctx.registry === null) return pass;
|
|
1931
|
+
const registered = new Set(
|
|
1932
|
+
[...ctx.registry.matchAll(/['"]([A-Za-z][\w.-]*)['"]\s*:/g)].map((m) => m[1])
|
|
1933
|
+
);
|
|
1934
|
+
if (registered.size === 0 && ctx.content.size === 0) return pass;
|
|
1935
|
+
const missing = /* @__PURE__ */ new Map();
|
|
1936
|
+
for (const [file, raw] of ctx.content) {
|
|
1937
|
+
let parsed;
|
|
1938
|
+
try {
|
|
1939
|
+
parsed = JSON.parse(raw);
|
|
1940
|
+
} catch {
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
for (const block of parsed.blocks ?? []) {
|
|
1944
|
+
const type = block?.type;
|
|
1945
|
+
if (typeof type !== "string" || registered.has(type)) continue;
|
|
1946
|
+
missing.set(type, [...missing.get(type) ?? [], file]);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (missing.size === 0) return pass;
|
|
1950
|
+
const lines = [...missing].map(([type, files]) => `${type} (${files.join(", ")})`);
|
|
1951
|
+
return fail(
|
|
1952
|
+
`not in src/components/registry.ts: ${lines.join("; ")}. Those blocks render as nothing. Either register the component or remove the block.`
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
id: "agency-credit",
|
|
1958
|
+
title: "Something renders the agency credit",
|
|
1959
|
+
silentAs: 'the site ships with no "Powered by WebMonterey" and nobody notices for months',
|
|
1960
|
+
run(ctx) {
|
|
1961
|
+
if (ctx.components.size === 0) return pass;
|
|
1962
|
+
if (ctx.site.domain === "webmonterey.com") return pass;
|
|
1963
|
+
for (const src of ctx.components.values()) {
|
|
1964
|
+
if (/webmonterey\/credits/.test(stripComments(src))) return pass;
|
|
1965
|
+
}
|
|
1966
|
+
return warn(
|
|
1967
|
+
"no component imports @cparkerwebm/webmonterey/webmonterey/credits/Credit.astro. The footer component is where it goes."
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
{
|
|
1972
|
+
id: "placeholder-branding",
|
|
1973
|
+
title: "No placeholder mark is being served as the client's",
|
|
1974
|
+
silentAs: "the client's site shows WebMonterey's favicon, and nobody looks at a favicon",
|
|
1975
|
+
run(ctx) {
|
|
1976
|
+
if (ctx.placeholders.length === 0) return pass;
|
|
1977
|
+
const detail = `still the package's placeholder, byte for byte: ${ctx.placeholders.join(", ")}. Replace with the client's own artwork before launch.`;
|
|
1978
|
+
return isConfigured(ctx.site.launched) ? fail(detail) : warn(detail);
|
|
1979
|
+
}
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
id: "environment",
|
|
1983
|
+
title: "The declared environment matches where the site actually is",
|
|
1984
|
+
silentAs: "a launched site whose client email is still being diverted to the agency's inbox",
|
|
1985
|
+
run(ctx) {
|
|
1986
|
+
const declared = ctx.site.environment;
|
|
1987
|
+
if (declared !== void 0 && declared !== "production" && declared !== "staging") {
|
|
1988
|
+
return fail(
|
|
1989
|
+
`environment is "${String(declared)}"; it must be "production" or "staging". An unrecognised value is treated as production, so this site is mailing real people.`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
if (declared === "staging" && isConfigured(ctx.site.launched)) {
|
|
1993
|
+
return fail(
|
|
1994
|
+
`this site launched on ${ctx.site.launched} but is still declared staging, so every email it sends is being redirected away from its real recipients. Set "environment": "production" in webmonterey.json.`
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
if (declared !== "staging" && !isConfigured(ctx.site.launched)) {
|
|
1998
|
+
return warn(
|
|
1999
|
+
`this site has no launch date but is treated as production, so testing a form will email the client's real contacts. Set "environment": "staging" in webmonterey.json until /webm:launch.`
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
return pass;
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
{
|
|
2006
|
+
id: "seeded-files",
|
|
2007
|
+
title: "The files Astro copies verbatim are present",
|
|
2008
|
+
silentAs: "a missing favicon or an absent _headers - no build error either way",
|
|
2009
|
+
run(ctx) {
|
|
2010
|
+
const gone = Object.entries(ctx.present).filter(([, there]) => !there).map(([file]) => file);
|
|
2011
|
+
return gone.length ? warn(`missing: ${gone.join(", ")}. \`webm sync\` restores package-owned ones.`) : pass;
|
|
2012
|
+
}
|
|
2013
|
+
},
|
|
2014
|
+
{
|
|
2015
|
+
id: "literal-values",
|
|
2016
|
+
title: "Component CSS contains no literal colors or sizes",
|
|
2017
|
+
silentAs: "drift away from the token system, one component at a time",
|
|
2018
|
+
run(ctx) {
|
|
2019
|
+
const hits = [];
|
|
2020
|
+
for (const [file, src] of ctx.components) {
|
|
2021
|
+
const styles = [...src.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join("\n");
|
|
2022
|
+
if (!styles) continue;
|
|
2023
|
+
const withoutVars = stripComments(styles).replace(/--webm-[\w-]+\s*:[^;]+;/g, "");
|
|
2024
|
+
if (/#[0-9a-fA-F]{3,8}\b|\brgb a?\(|\bhsl a?\(/.test(withoutVars)) hits.push(file);
|
|
2025
|
+
}
|
|
2026
|
+
return hits.length ? warn(`literal colors in: ${hits.join(", ")}`) : pass;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
];
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
// src/integration/config.ts
|
|
2034
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync5 } from "node:fs";
|
|
2035
|
+
import { join as join6 } from "node:path";
|
|
2036
|
+
function readJson(path, what, required) {
|
|
2037
|
+
if (!existsSync7(path)) {
|
|
2038
|
+
if (required) {
|
|
2039
|
+
throw new ConfigError(`${what} not found at ${path}. Every site needs one.`);
|
|
2040
|
+
}
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
try {
|
|
2044
|
+
return JSON.parse(readFileSync6(path, "utf8"));
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
throw new ConfigError(
|
|
2047
|
+
`${what} at ${path} is not valid JSON: ${error instanceof Error ? error.message : error}`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function loadSiteFiles(root) {
|
|
2052
|
+
const sitePath = join6(root, "webmonterey.json");
|
|
2053
|
+
const designPath = join6(root, "design.json");
|
|
2054
|
+
const site = readJson(sitePath, "webmonterey.json", true);
|
|
2055
|
+
const design = readJson(designPath, "design.json", false);
|
|
2056
|
+
return {
|
|
2057
|
+
site,
|
|
2058
|
+
design: design ?? {},
|
|
2059
|
+
paths: { site: sitePath, design: design ? designPath : null }
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
var ConfigError;
|
|
2063
|
+
var init_config2 = __esm({
|
|
2064
|
+
"src/integration/config.ts"() {
|
|
2065
|
+
"use strict";
|
|
2066
|
+
ConfigError = class extends Error {
|
|
2067
|
+
constructor(message) {
|
|
2068
|
+
super(message);
|
|
2069
|
+
this.name = "ConfigError";
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
// src/cli/doctor.ts
|
|
2076
|
+
var doctor_exports = {};
|
|
2077
|
+
__export(doctor_exports, {
|
|
2078
|
+
buildContext: () => buildContext,
|
|
2079
|
+
run: () => run5
|
|
2080
|
+
});
|
|
2081
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync6 } from "node:fs";
|
|
2082
|
+
import { join as join7, relative as relative3 } from "node:path";
|
|
2083
|
+
function parseJsonc(source) {
|
|
2084
|
+
const stripped = source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, comment) => comment ? "" : m).replace(/,(\s*[}\]])/g, "$1");
|
|
2085
|
+
return JSON.parse(stripped);
|
|
2086
|
+
}
|
|
2087
|
+
function readTree(root, dir, exts) {
|
|
2088
|
+
const out = /* @__PURE__ */ new Map();
|
|
2089
|
+
const walk3 = (d) => {
|
|
2090
|
+
if (!existsSync8(d)) return;
|
|
2091
|
+
for (const entry2 of readdirSync6(d, { withFileTypes: true })) {
|
|
2092
|
+
const full = join7(d, entry2.name);
|
|
2093
|
+
if (entry2.isDirectory()) walk3(full);
|
|
2094
|
+
else if (exts.some((e) => entry2.name.endsWith(e))) {
|
|
2095
|
+
out.set(relative3(root, full), readFileSync7(full, "utf8"));
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
walk3(join7(root, dir));
|
|
2100
|
+
return out;
|
|
2101
|
+
}
|
|
2102
|
+
function placeholderFiles(siteRoot) {
|
|
2103
|
+
const seeded = join7(PACKAGE_ROOT, "template", "public");
|
|
2104
|
+
if (!existsSync8(seeded)) return [];
|
|
2105
|
+
return readdirSync6(seeded).filter((file) => {
|
|
2106
|
+
const theirs = join7(siteRoot, "public", file);
|
|
2107
|
+
if (!existsSync8(theirs)) return false;
|
|
2108
|
+
try {
|
|
2109
|
+
return readFileSync7(theirs, "base64") === readFileSync7(join7(seeded, file), "base64");
|
|
2110
|
+
} catch {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
}).map((file) => `public/${file}`);
|
|
2114
|
+
}
|
|
2115
|
+
function readWorkerEntry(siteRoot, wranglerPath) {
|
|
2116
|
+
if (!wranglerPath) return null;
|
|
2117
|
+
const config = parseJsonc(readFileSync7(wranglerPath, "utf8"));
|
|
2118
|
+
if (!config.main) return null;
|
|
2119
|
+
const entry2 = join7(siteRoot, config.main);
|
|
2120
|
+
return existsSync8(entry2) ? readFileSync7(entry2, "utf8") : null;
|
|
2121
|
+
}
|
|
2122
|
+
function contentPageNames(root) {
|
|
2123
|
+
const dir = join7(root, "src/content/pages");
|
|
2124
|
+
if (!existsSync8(dir)) return [];
|
|
2125
|
+
return readdirSync6(dir).filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
2126
|
+
}
|
|
2127
|
+
function readMcp(siteRoot) {
|
|
2128
|
+
const read = (rel, pick) => {
|
|
2129
|
+
const path = join7(siteRoot, rel);
|
|
2130
|
+
if (!existsSync8(path)) return null;
|
|
2131
|
+
try {
|
|
2132
|
+
return pick(JSON.parse(readFileSync7(path, "utf8")));
|
|
2133
|
+
} catch {
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
return {
|
|
2138
|
+
declared: read(".mcp.json", (p) => p.mcpServers ?? null),
|
|
2139
|
+
enabled: read(".claude/settings.json", (p) => p.enabledMcpjsonServers ?? null)
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function buildContext(siteRoot) {
|
|
2143
|
+
const { site } = loadSiteFiles(siteRoot);
|
|
2144
|
+
const wranglerPath = ["wrangler.jsonc", "wrangler.json"].map((f) => join7(siteRoot, f)).find(existsSync8);
|
|
2145
|
+
const syncPath = join7(siteRoot, ".claude/skills/webm/.webm-sync.json");
|
|
2146
|
+
return {
|
|
2147
|
+
site,
|
|
2148
|
+
wrangler: wranglerPath ? parseJsonc(readFileSync7(wranglerPath, "utf8")) : null,
|
|
2149
|
+
pages: readTree(siteRoot, "src/pages", [".astro", ".ts"]),
|
|
2150
|
+
components: readTree(siteRoot, "src/components", [".astro", ".ts"]),
|
|
2151
|
+
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2152
|
+
workerEntry: readWorkerEntry(siteRoot, wranglerPath),
|
|
2153
|
+
placeholders: placeholderFiles(siteRoot),
|
|
2154
|
+
contentPages: contentPageNames(siteRoot),
|
|
2155
|
+
content: readTree(siteRoot, "src/content/pages", [".json"]),
|
|
2156
|
+
/*
|
|
2157
|
+
* Everything else in the repo that can hold a call the compiler will not resolve for you:
|
|
2158
|
+
* actions, the site's own includes and email templates, and the SQL that has to have a
|
|
2159
|
+
* migration behind it. Read here rather than in each check so a check stays a pure function
|
|
2160
|
+
* of its context and the tests can hand it a literal.
|
|
2161
|
+
*/
|
|
2162
|
+
actions: readTree(siteRoot, "src/actions", [".ts"]),
|
|
2163
|
+
includes: readTree(siteRoot, "src/includes", [".ts", ".astro"]),
|
|
2164
|
+
emails: readTree(siteRoot, "src/emails", [".ts"]),
|
|
2165
|
+
migrations: readTree(siteRoot, "migrations", [".sql"]),
|
|
2166
|
+
registry: existsSync8(join7(siteRoot, "src/components/registry.ts")) ? readFileSync7(join7(siteRoot, "src/components/registry.ts"), "utf8") : null,
|
|
2167
|
+
present: Object.fromEntries(
|
|
2168
|
+
[
|
|
2169
|
+
"public/_headers",
|
|
2170
|
+
"public/favicon.ico",
|
|
2171
|
+
"public/site.webmanifest",
|
|
2172
|
+
"scripts/check-node.mjs"
|
|
2173
|
+
].map((f) => [f, existsSync8(join7(siteRoot, f))])
|
|
2174
|
+
),
|
|
2175
|
+
sync: existsSync8(syncPath) ? JSON.parse(readFileSync7(syncPath, "utf8")) : null,
|
|
2176
|
+
mcp: readMcp(siteRoot),
|
|
2177
|
+
version: packageVersion()
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
function run5(argv2) {
|
|
2181
|
+
const siteRoot = argv2[0] ?? process.cwd();
|
|
2182
|
+
if (!existsSync8(join7(siteRoot, "webmonterey.json"))) {
|
|
2183
|
+
console.error(`webm doctor: no webmonterey.json in ${siteRoot}. Not a WebMonterey site.`);
|
|
2184
|
+
return 1;
|
|
2185
|
+
}
|
|
2186
|
+
const ctx = buildContext(siteRoot);
|
|
2187
|
+
let failed = 0;
|
|
2188
|
+
let warned = 0;
|
|
2189
|
+
for (const check of CHECKS) {
|
|
2190
|
+
const result = check.run(ctx);
|
|
2191
|
+
const mark = result.status === "pass" ? " ok " : result.status === "warn" ? "warn " : "FAIL ";
|
|
2192
|
+
console.log(`${mark} ${check.title}`);
|
|
2193
|
+
if (result.detail) {
|
|
2194
|
+
console.log(` ${result.detail}`);
|
|
2195
|
+
if (result.status === "fail") console.log(` fails as: ${check.silentAs}`);
|
|
2196
|
+
}
|
|
2197
|
+
if (result.status === "fail") failed++;
|
|
2198
|
+
if (result.status === "warn") warned++;
|
|
2199
|
+
}
|
|
2200
|
+
console.log(
|
|
2201
|
+
`
|
|
2202
|
+
${CHECKS.length - failed - warned} passed, ${warned} warning${warned === 1 ? "" : "s"}, ${failed} failed`
|
|
2203
|
+
);
|
|
2204
|
+
return failed > 0 ? 1 : 0;
|
|
2205
|
+
}
|
|
2206
|
+
var init_doctor = __esm({
|
|
2207
|
+
"src/cli/doctor.ts"() {
|
|
2208
|
+
"use strict";
|
|
2209
|
+
init_checks();
|
|
2210
|
+
init_config2();
|
|
2211
|
+
init_package_root();
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
|
|
2215
|
+
// src/cli/codemods.ts
|
|
2216
|
+
function compareVersions(a, b) {
|
|
2217
|
+
const pa = a.split(".").map(Number);
|
|
2218
|
+
const pb = b.split(".").map(Number);
|
|
2219
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
2220
|
+
return 0;
|
|
2221
|
+
}
|
|
2222
|
+
function codemodsBetween(from, to, list = CODEMODS) {
|
|
2223
|
+
return list.filter((c) => compareVersions(c.version, from) > 0 && compareVersions(c.version, to) <= 0).sort((a, b) => compareVersions(a.version, b.version));
|
|
2224
|
+
}
|
|
2225
|
+
var CODEMODS;
|
|
2226
|
+
var init_codemods = __esm({
|
|
2227
|
+
"src/cli/codemods.ts"() {
|
|
2228
|
+
"use strict";
|
|
2229
|
+
CODEMODS = [];
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
// src/cli/upgrade.ts
|
|
2234
|
+
var upgrade_exports = {};
|
|
2235
|
+
__export(upgrade_exports, {
|
|
2236
|
+
run: () => run6
|
|
2237
|
+
});
|
|
2238
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2239
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
2240
|
+
import { join as join8 } from "node:path";
|
|
2241
|
+
function git(siteRoot, args) {
|
|
2242
|
+
return execFileSync2("git", args, { cwd: siteRoot, encoding: "utf8" }).trim();
|
|
2243
|
+
}
|
|
2244
|
+
function installedVersion(siteRoot) {
|
|
2245
|
+
const path = join8(siteRoot, "node_modules", PACKAGE, "package.json");
|
|
2246
|
+
if (!existsSync9(path)) return null;
|
|
2247
|
+
return JSON.parse(readFileSync8(path, "utf8")).version;
|
|
2248
|
+
}
|
|
2249
|
+
function run6(argv2) {
|
|
2250
|
+
const siteRoot = process.cwd();
|
|
2251
|
+
const target = argv2.find((a) => !a.startsWith("-")) ?? "latest";
|
|
2252
|
+
const dryRun = argv2.includes("--dry-run");
|
|
2253
|
+
if (!existsSync9(join8(siteRoot, "webmonterey.json"))) {
|
|
2254
|
+
console.error(`webm upgrade: no webmonterey.json here. Not a WebMonterey site.`);
|
|
2255
|
+
return 1;
|
|
2256
|
+
}
|
|
2257
|
+
if (git(siteRoot, ["status", "--porcelain"])) {
|
|
2258
|
+
console.error("webm upgrade: working tree is not clean. Commit or stash first.");
|
|
2259
|
+
return 1;
|
|
2260
|
+
}
|
|
2261
|
+
const from = installedVersion(siteRoot);
|
|
2262
|
+
console.log(`Installed: ${from ?? "nothing"} -> ${target}`);
|
|
2263
|
+
if (dryRun) {
|
|
2264
|
+
console.log("--dry-run: stopping before any change.");
|
|
2265
|
+
return 0;
|
|
2266
|
+
}
|
|
2267
|
+
const branch = `upgrade/${PACKAGE.split("/")[1]}-${target}`;
|
|
2268
|
+
if (git(siteRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) === "main") {
|
|
2269
|
+
git(siteRoot, ["checkout", "-b", branch]);
|
|
2270
|
+
console.log(`Branched to ${branch}. Never upgrade on main.`);
|
|
2271
|
+
}
|
|
2272
|
+
execFileSync2("npm", ["install", `${PACKAGE}@${target}`], { cwd: siteRoot, stdio: "inherit" });
|
|
2273
|
+
const to = installedVersion(siteRoot);
|
|
2274
|
+
if (!to) {
|
|
2275
|
+
console.error("webm upgrade: install did not produce a version. Check the npm output above.");
|
|
2276
|
+
return 1;
|
|
2277
|
+
}
|
|
2278
|
+
const mods = codemodsBetween(from ?? "0.0.0", to);
|
|
2279
|
+
if (mods.length) {
|
|
2280
|
+
console.log(`
|
|
2281
|
+
Running ${mods.length} codemod${mods.length === 1 ? "" : "s"}:`);
|
|
2282
|
+
for (const mod of mods) {
|
|
2283
|
+
const changes = mod.run(siteRoot);
|
|
2284
|
+
console.log(` ${mod.version} ${mod.title}`);
|
|
2285
|
+
for (const c of changes) console.log(` ${c}`);
|
|
2286
|
+
if (!changes.length) console.log(` nothing to change`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
const synced = sync(siteRoot);
|
|
2290
|
+
console.log(`
|
|
2291
|
+
Skills re-materialized (v${synced.version})`);
|
|
2292
|
+
for (const s of synced.added) console.log(` + /webm:${s}`);
|
|
2293
|
+
for (const s of synced.removed) console.log(` - /webm:${s}`);
|
|
2294
|
+
console.log(`
|
|
2295
|
+
Now, in order:`);
|
|
2296
|
+
console.log(` npx webm doctor`);
|
|
2297
|
+
console.log(` npm run check && npm run build`);
|
|
2298
|
+
console.log(` git push -u origin HEAD # review the preview URL before merging`);
|
|
2299
|
+
if (synced.added.length || synced.removed.length) {
|
|
2300
|
+
console.log(`
|
|
2301
|
+
Skill changes are live already. Run /reload-plugins if anything outside`);
|
|
2302
|
+
console.log(`skills/ changed - agents, hooks and .mcp.json are not picked up live.`);
|
|
2303
|
+
}
|
|
2304
|
+
return 0;
|
|
2305
|
+
}
|
|
2306
|
+
var PACKAGE;
|
|
2307
|
+
var init_upgrade = __esm({
|
|
2308
|
+
"src/cli/upgrade.ts"() {
|
|
2309
|
+
"use strict";
|
|
2310
|
+
init_codemods();
|
|
2311
|
+
init_sync();
|
|
2312
|
+
init_package_root();
|
|
2313
|
+
PACKAGE = packageName();
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
// bin/webm.mjs
|
|
2318
|
+
init_package_root();
|
|
2319
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
2320
|
+
import { join as join9 } from "node:path";
|
|
2321
|
+
var pkg = JSON.parse(readFileSync9(join9(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
2322
|
+
var COMMANDS = {
|
|
2323
|
+
new: {
|
|
2324
|
+
blurb: "Scaffold a new client site from a domain",
|
|
2325
|
+
load: () => Promise.resolve().then(() => (init_new(), new_exports))
|
|
2326
|
+
},
|
|
2327
|
+
"design:extract": {
|
|
2328
|
+
blurb: "Turn a hand-edited tokens.css into design.json",
|
|
2329
|
+
load: () => Promise.resolve().then(() => (init_design_extract(), design_extract_exports))
|
|
2330
|
+
},
|
|
2331
|
+
compare: {
|
|
2332
|
+
blurb: "Diff two built sites - text, head tags, JSON-LD and CSS. For rebuilds.",
|
|
2333
|
+
load: () => Promise.resolve().then(() => (init_compare(), compare_exports))
|
|
2334
|
+
},
|
|
2335
|
+
sync: {
|
|
2336
|
+
blurb: "Materialize the fleet skills into .claude/skills/webm/",
|
|
2337
|
+
load: () => Promise.resolve().then(() => (init_sync(), sync_exports))
|
|
2338
|
+
},
|
|
2339
|
+
doctor: {
|
|
2340
|
+
blurb: "Check this site against the traps that fail silently",
|
|
2341
|
+
load: () => Promise.resolve().then(() => (init_doctor(), doctor_exports))
|
|
2342
|
+
},
|
|
2343
|
+
upgrade: {
|
|
2344
|
+
blurb: "Move this site to a newer framework version",
|
|
2345
|
+
load: () => Promise.resolve().then(() => (init_upgrade(), upgrade_exports))
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
function usage() {
|
|
2349
|
+
const width = Math.max(...Object.keys(COMMANDS).map((k) => k.length));
|
|
2350
|
+
console.log(`webm ${pkg.version} - ${pkg.description}
|
|
2351
|
+
`);
|
|
2352
|
+
console.log("Usage: webm <command> [options]\n");
|
|
2353
|
+
console.log("Commands:");
|
|
2354
|
+
for (const [name, c] of Object.entries(COMMANDS)) {
|
|
2355
|
+
console.log(` ${name.padEnd(width)} ${c.blurb}`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
var [command, ...argv] = process.argv.slice(2);
|
|
2359
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
2360
|
+
usage();
|
|
2361
|
+
process.exit(0);
|
|
2362
|
+
}
|
|
2363
|
+
if (command === "--version" || command === "-v") {
|
|
2364
|
+
console.log(pkg.version);
|
|
2365
|
+
process.exit(0);
|
|
2366
|
+
}
|
|
2367
|
+
var entry = COMMANDS[command];
|
|
2368
|
+
if (!entry) {
|
|
2369
|
+
console.error(`webm: unknown command "${command}"
|
|
2370
|
+
`);
|
|
2371
|
+
usage();
|
|
2372
|
+
process.exit(1);
|
|
2373
|
+
}
|
|
2374
|
+
try {
|
|
2375
|
+
const mod = await entry.load();
|
|
2376
|
+
process.exit(await mod.run(argv) ?? 0);
|
|
2377
|
+
} catch (error) {
|
|
2378
|
+
console.error(`webm ${command}: ${error instanceof Error ? error.message : error}`);
|
|
2379
|
+
if (process.env.WEBM_DEBUG) console.error(error);
|
|
2380
|
+
process.exit(1);
|
|
2381
|
+
}
|