@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/src/cli/new.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* `webm new <domain>` - from nothing to a site that builds.
|
|
3
|
+
*
|
|
4
|
+
* THE BOOTSTRAP GAP THIS CLOSES: `start` has to run before the package is installed, so it
|
|
5
|
+
* cannot come from the package's own skills. `npx @cparkerwebm/webmonterey new example.com` runs
|
|
6
|
+
* with no repo and no prior setup, and hands off to /webm:start once the skills exist.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately does NOT touch Cloudflare or GitHub. Creating a Worker, a D1 database or a repo
|
|
9
|
+
* are outward-facing actions with real consequences, and they belong to a person with the
|
|
10
|
+
* dashboard open - or to the start skill, which knows what order they go in. This writes files.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { DomainError, normalizeDomain, repoName, resourceNames } from './slug.ts';
|
|
16
|
+
import { scaffold } from './scaffold.ts';
|
|
17
|
+
import { sync } from './sync.ts';
|
|
18
|
+
import { seed, gitkeepEmptyDirs, SCAFFOLD_DIRS } from './seed.ts';
|
|
19
|
+
|
|
20
|
+
import { PACKAGE_ROOT, packageVersion } from './package-root.ts';
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv: string[]) {
|
|
23
|
+
const positional = argv.filter((a) => !a.startsWith('--'));
|
|
24
|
+
const flag = (name: string) => {
|
|
25
|
+
const hit = argv.find((a) => a.startsWith(`--${name}=`));
|
|
26
|
+
return hit ? hit.slice(name.length + 3) : undefined;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
domain: positional[0],
|
|
30
|
+
client: flag('client'),
|
|
31
|
+
org: flag('org') ?? 'webmonterey',
|
|
32
|
+
stagingEmail: flag('staging-email'),
|
|
33
|
+
into: flag('into'),
|
|
34
|
+
install: !argv.includes('--no-install'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*
|
|
39
|
+
* WHERE A STAGING SITE'S MAIL GOES, defaulting to whoever is scaffolding it.
|
|
40
|
+
*
|
|
41
|
+
* The package carries no inbox of its own: a default address baked into a public package means
|
|
42
|
+
* a stranger's staging site mails the package author. `git config user.email` is the person at
|
|
43
|
+
* the keyboard, which is the right default for a site they are about to test. The flag overrides
|
|
44
|
+
* it; doctor fails a staging site that ends up with none.
|
|
45
|
+
*/
|
|
46
|
+
function gitUserEmail(): string {
|
|
47
|
+
try {
|
|
48
|
+
return execFileSync('git', ['config', 'user.email'], { encoding: 'utf8' }).trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function run(argv: string[]): number {
|
|
55
|
+
const args = parseArgs(argv);
|
|
56
|
+
|
|
57
|
+
if (!args.domain) {
|
|
58
|
+
console.error(
|
|
59
|
+
'webm new <domain> [--client="Name"] [--org=webmonterey] [--staging-email=you@example.com] [--into=path] [--no-install]',
|
|
60
|
+
);
|
|
61
|
+
console.error('\n webm new autire.com --client="Autire Technologies"');
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let domain: string;
|
|
66
|
+
try {
|
|
67
|
+
domain = normalizeDomain(args.domain);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error(`webm new: ${error instanceof DomainError ? error.message : error}`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const names = resourceNames(domain);
|
|
74
|
+
const root = resolve(args.into ?? repoName(domain));
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Refuse a directory that already has anything in it. Scaffolding over an existing site would
|
|
78
|
+
* overwrite webmonterey.json and design.json - the two files nobody can reconstruct.
|
|
79
|
+
*/
|
|
80
|
+
if (existsSync(root) && readdirSync(root).length > 0) {
|
|
81
|
+
console.error(`webm new: ${root} exists and is not empty. Refusing to scaffold over it.`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const files = scaffold({
|
|
86
|
+
domain,
|
|
87
|
+
client: args.client,
|
|
88
|
+
org: args.org,
|
|
89
|
+
stagingEmail: args.stagingEmail ?? gitUserEmail(),
|
|
90
|
+
packageVersion: packageVersion(),
|
|
91
|
+
/* Real today, not a constant - see ScaffoldOptions.today for what a stale one does. */
|
|
92
|
+
today: new Date().toISOString().slice(0, 10),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for (const [path, contents] of Object.entries(files)) {
|
|
96
|
+
const full = join(root, path);
|
|
97
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
98
|
+
writeFileSync(full, contents);
|
|
99
|
+
}
|
|
100
|
+
/*
|
|
101
|
+
* Seed AFTER the scaffold, and never over it. Both can produce a given path; the scaffold's
|
|
102
|
+
* version is the one with this client's domain in it.
|
|
103
|
+
*/
|
|
104
|
+
const seeded = seed(PACKAGE_ROOT, root);
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
* scripts/ and migrations/ come from `sync`, so they arrive on every install rather than only
|
|
108
|
+
* at scaffold time. Run it before the .gitkeep walk so migrations/ is not marked empty.
|
|
109
|
+
*/
|
|
110
|
+
sync(root);
|
|
111
|
+
|
|
112
|
+
const kept = gitkeepEmptyDirs(root, SCAFFOLD_DIRS);
|
|
113
|
+
|
|
114
|
+
console.log(`Scaffolded ${Object.keys(files).length} files into ${root}`);
|
|
115
|
+
console.log(` + ${seeded.length} seeded (favicons, headers, CLAUDE.md, a contact form)`);
|
|
116
|
+
console.log(` + ${kept.length} .gitkeep, so the empty directories survive a clone\n`);
|
|
117
|
+
console.log(` repo ${args.org}/${names.repo} (keeps the domain)`);
|
|
118
|
+
console.log(
|
|
119
|
+
` worker ${names.worker} (slug - no TLD, so Chrome does not flag previews)`,
|
|
120
|
+
);
|
|
121
|
+
console.log(` d1 ${names.d1}`);
|
|
122
|
+
console.log(` r2 ${names.r2Media}`);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: root });
|
|
126
|
+
} catch {
|
|
127
|
+
console.log('\n (git init failed - initialize the repo yourself)');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (args.install) {
|
|
131
|
+
console.log('\nInstalling…');
|
|
132
|
+
try {
|
|
133
|
+
execFileSync('npm', ['install'], { cwd: root, stdio: 'inherit' });
|
|
134
|
+
} catch {
|
|
135
|
+
console.error(
|
|
136
|
+
`\nInstall failed. The scaffold is written and fine; fix the cause above and run npm install in ${names.repo}.`,
|
|
137
|
+
);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`\nNext:`);
|
|
143
|
+
console.log(` cd ${names.repo}`);
|
|
144
|
+
console.log(` claude # then /webm:start — it takes it from here`);
|
|
145
|
+
console.log(`\nStart Claude from the repo root. Project skills-dir plugins load only from the`);
|
|
146
|
+
console.log(`directory you start in; launching from a subdirectory misses them.`);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Finding the package's own root, from wherever this code ended up.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM BUNDLING CREATES: src/cli/*.ts resolve the root as `../../` from their own
|
|
5
|
+
* location, and bin/webm.mjs as `../`. Bundle them all into dist/webm.mjs and every one of those
|
|
6
|
+
* is now relative to dist/ - so `../../` overshoots the repo entirely and reads a package.json
|
|
7
|
+
* belonging to whatever sits above it. That failed as `ENOENT: /Users/cparkerwebm/Workspace/
|
|
8
|
+
* package.json`, which names a directory that has nothing to do with this package.
|
|
9
|
+
*
|
|
10
|
+
* Walking up until the manifest identifies itself works from source and from a bundle, and does
|
|
11
|
+
* not care how deep either sits.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* The manifest identifies itself by its bin, not by a hardcoded name. The package has been
|
|
19
|
+
* renamed once already; a name literal here is one more place a rename has to find.
|
|
20
|
+
*/
|
|
21
|
+
function isOurs(manifest: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8')) as { bin?: Record<string, string> };
|
|
24
|
+
return typeof pkg.bin?.webm === 'string';
|
|
25
|
+
} catch {
|
|
26
|
+
return false; /* a malformed package.json above us is not ours; keep walking */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findRoot(): string {
|
|
31
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
for (let i = 0; i < 8; i++) {
|
|
33
|
+
const manifest = join(dir, 'package.json');
|
|
34
|
+
if (existsSync(manifest) && isOurs(manifest)) return dir;
|
|
35
|
+
const up = dirname(dir);
|
|
36
|
+
if (up === dir) break;
|
|
37
|
+
dir = up;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(
|
|
40
|
+
`webm: cannot locate the package root from ${fileURLToPath(import.meta.url)}. ` +
|
|
41
|
+
`This usually means the CLI was moved out of the installed package.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const PACKAGE_ROOT = findRoot();
|
|
46
|
+
|
|
47
|
+
function manifest(): { name: string; version: string } {
|
|
48
|
+
return JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function packageVersion(): string {
|
|
52
|
+
return manifest().version;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The published name, read from the manifest so a rename is one edit. */
|
|
56
|
+
export function packageName(): string {
|
|
57
|
+
return manifest().name;
|
|
58
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { scaffold } from './scaffold.ts';
|
|
4
|
+
import { MCP_NAMES, mcpConfig } from './mcp.ts';
|
|
5
|
+
|
|
6
|
+
const files = (over = {}) =>
|
|
7
|
+
scaffold({
|
|
8
|
+
domain: 'autire.com',
|
|
9
|
+
client: 'Autire Technologies',
|
|
10
|
+
packageVersion: '1.0.0',
|
|
11
|
+
today: '2026-08-26',
|
|
12
|
+
...over,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const json = (f: Record<string, string>, path: string) => JSON.parse(f[path]!);
|
|
16
|
+
|
|
17
|
+
test('three names, three jobs: repo, slug, Cloudflare', () => {
|
|
18
|
+
const f = files();
|
|
19
|
+
const site = json(f, 'webmonterey.json');
|
|
20
|
+
// Repo carries the full domain with UNDERSCORES; Cloudflare carries the slug, no TLD.
|
|
21
|
+
assert.equal(site.repo, 'webmonterey/autire_com');
|
|
22
|
+
assert.equal(site.worker, 'webm-autire');
|
|
23
|
+
assert.equal(site.slug, 'autire');
|
|
24
|
+
assert.match(f['wrangler.jsonc']!, /"name": "webm-autire"/);
|
|
25
|
+
assert.match(f['README.md']!, /webm-autire-db/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('MCP is declared AND pre-approved - a declaration alone is inert', () => {
|
|
29
|
+
const f = files();
|
|
30
|
+
// Both halves come from src/cli/mcp.ts, so this asserts against that list rather than a literal
|
|
31
|
+
// that has to be edited every time a server is added - which is how it fell one behind before.
|
|
32
|
+
assert.deepEqual(json(f, '.mcp.json'), mcpConfig());
|
|
33
|
+
assert.deepEqual(json(f, '.claude/settings.json').enabledMcpjsonServers, MCP_NAMES);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('Mailgun and Stripe are never scaffolded into a client repo', () => {
|
|
37
|
+
// A session there could send mail as any client, or issue refunds.
|
|
38
|
+
const all = Object.values(files()).join('\n').toLowerCase();
|
|
39
|
+
assert.ok(!all.includes('mailgun-mcp'), 'mailgun MCP must not appear');
|
|
40
|
+
assert.ok(!/stripe.*mcp|mcp.*stripe/.test(all), 'stripe MCP must not appear');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('the registry and the block union both start empty - the package ships no components', () => {
|
|
44
|
+
const f = files();
|
|
45
|
+
assert.match(
|
|
46
|
+
f['src/components/registry.ts']!,
|
|
47
|
+
/blocks: Record<string, AstroComponentFactory> = \{\}/,
|
|
48
|
+
);
|
|
49
|
+
assert.match(f['src/content.config.ts']!, /webmontereyCollections\(\[\]\)/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('postinstall runs webm sync, or a repo has no fleet skills and no error saying why', () => {
|
|
53
|
+
assert.equal(json(files(), 'package.json').scripts.postinstall, 'webm sync');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('no .npmrc is scaffolded - the package is on the public registry', () => {
|
|
57
|
+
const f = files();
|
|
58
|
+
assert.equal(f['.npmrc'], undefined);
|
|
59
|
+
const all = Object.values(f).join('\n');
|
|
60
|
+
assert.ok(!all.includes('NODE_AUTH_TOKEN'), 'no registry credential anywhere in the scaffold');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('stagingEmail is the address passed in, and empty rather than invented when none is', () => {
|
|
64
|
+
// The package carries no inbox. A default address in a public package would mean a stranger's
|
|
65
|
+
// staging site mails the author; `webm new` supplies the git user's email instead.
|
|
66
|
+
assert.equal(
|
|
67
|
+
json(files({ stagingEmail: 'me@example.com' }), 'webmonterey.json').stagingEmail,
|
|
68
|
+
'me@example.com',
|
|
69
|
+
);
|
|
70
|
+
assert.equal(json(files(), 'webmonterey.json').stagingEmail, '');
|
|
71
|
+
assert.ok(!Object.values(files()).join('\n').includes('webmonterey.com'), 'no agency inbox');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('run_worker_first starts with the actions endpoint and carries its warning', () => {
|
|
75
|
+
const w = files()['wrangler.jsonc']!;
|
|
76
|
+
assert.match(w, /"run_worker_first": \["\/_actions\/\*"\]/);
|
|
77
|
+
assert.match(w, /BOTH SLASH FORMS/);
|
|
78
|
+
assert.match(w, /Sec-Fetch-Dest/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('secrets are gitignored and the example names the password-manager habit', () => {
|
|
82
|
+
const f = files();
|
|
83
|
+
assert.match(f['.gitignore']!, /^\.dev\.vars$/m);
|
|
84
|
+
assert.match(f['.dev.vars.example']!, /password manager/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('an unnamed client gets CHANGEME, which go-live refuses to launch with', () => {
|
|
88
|
+
const site = json(files({ client: undefined }), 'webmonterey.json');
|
|
89
|
+
assert.equal(site.client, 'CHANGEME');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('timeZone defaults to a real IANA zone', () => {
|
|
93
|
+
assert.equal(json(files(), 'webmonterey.json').timeZone, 'America/Los_Angeles');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('features are technical switches, and carry no plan or tier', () => {
|
|
97
|
+
const site = json(files(), 'webmonterey.json');
|
|
98
|
+
assert.deepEqual(Object.keys(site.features).sort(), [
|
|
99
|
+
'compliance',
|
|
100
|
+
'd1',
|
|
101
|
+
'platform',
|
|
102
|
+
'turnstile',
|
|
103
|
+
]);
|
|
104
|
+
// The app namespace is reserved on every site from day one - off, folder fixed, path public.
|
|
105
|
+
assert.deepEqual(site.app, { enabled: false, path: 'webapp', label: 'Portal' });
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Check the DATA, not the `//`-prefixed keys - those are inline documentation and one of them
|
|
109
|
+
* says "Nothing here reads a tier", which a blunt substring check flags as the very thing it
|
|
110
|
+
* forbids.
|
|
111
|
+
*/
|
|
112
|
+
const data = Object.fromEntries(Object.entries(site).filter(([k]) => !k.startsWith('//')));
|
|
113
|
+
const raw = JSON.stringify(data).toLowerCase();
|
|
114
|
+
for (const word of ['basic', 'standard', 'premium', 'plan_label', 'tier', 'billable', 'credit']) {
|
|
115
|
+
assert.ok(!raw.includes(word), `webmonterey.json must not know about "${word}"`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('there is no orphan stylesheet entry point in the site', () => {
|
|
120
|
+
/*
|
|
121
|
+
* The scaffold used to write src/styles/index.css importing the package's stylesheet and then
|
|
122
|
+
* custom/. Nothing imported it. A client could write a rule in custom/_index.css, see the
|
|
123
|
+
* import chain in their own repo, and never see the rule on the page - the file was not in the
|
|
124
|
+
* module graph at all.
|
|
125
|
+
*
|
|
126
|
+
* base.astro now pulls custom/ in through virtual:webm/custom, so there is nothing here to
|
|
127
|
+
* keep in step, and a file that looks like an entry point but is not is worse than none.
|
|
128
|
+
*/
|
|
129
|
+
const f = files();
|
|
130
|
+
assert.equal(f['src/styles/index.css'], undefined, 'no orphan entry point');
|
|
131
|
+
assert.ok(f['src/styles/custom/_index.css'], 'the override seam itself still ships');
|
|
132
|
+
assert.match(
|
|
133
|
+
f['src/styles/custom/_index.css']!,
|
|
134
|
+
/NOTHING IMPORTS THIS FILE FROM THIS REPO/,
|
|
135
|
+
'and it says so, so nobody goes looking for the import',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('astro.config names the adapter, or the build dies on the first on-demand route', () => {
|
|
140
|
+
/*
|
|
141
|
+
* An adapter set through updateConfig from inside the integration does not run its own hooks.
|
|
142
|
+
* A fully static site never notices, which is how it went unnoticed for a whole build-out.
|
|
143
|
+
*/
|
|
144
|
+
const config = files()['astro.config.mjs']!;
|
|
145
|
+
assert.match(config, /import webmonterey, \{ adapter \} from '@cparkerwebm\/webmonterey'/);
|
|
146
|
+
assert.match(config, /adapter:\s*adapter\(\)/);
|
|
147
|
+
assert.match(config, /integrations:\s*\[webmonterey\(\)\]/);
|
|
148
|
+
/*
|
|
149
|
+
* Check the IMPORTS, not the whole file - the config's own comment explains that the site takes
|
|
150
|
+
* no direct dependency on @astrojs/cloudflare, and a substring check flags the explanation as
|
|
151
|
+
* the thing it forbids.
|
|
152
|
+
*/
|
|
153
|
+
const imports = config.split('\n').filter((l) => l.trimStart().startsWith('import '));
|
|
154
|
+
assert.ok(
|
|
155
|
+
!imports.some((l) => l.includes('@astrojs/')),
|
|
156
|
+
`the site must not import an Astro integration directly: ${imports.join(' | ')}`,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('the scaffolded site wires the form pipeline by re-export, not by copy', () => {
|
|
161
|
+
// The re-export is the propagation seam: a fix to validation, Turnstile, the D1 write or an
|
|
162
|
+
// email template reaches the site on npm update. A copied pipeline never receives one.
|
|
163
|
+
const actions = files()['src/actions/index.ts']!;
|
|
164
|
+
assert.match(actions, /export \{ server \} from '@cparkerwebm\/webmonterey\/actions'/);
|
|
165
|
+
assert.match(actions, /wrap rather than fork/i);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('compatibility_date is the date passed in, never a constant baked into the package', () => {
|
|
169
|
+
/*
|
|
170
|
+
* The bug this exists to prevent, and it is the nastiest one found in the whole build-out: a
|
|
171
|
+
* compatibility_date a few months behind the installed workerd renders EVERY page as the
|
|
172
|
+
* string "[object Object]". 15 bytes, no warning, exit code 0, and `astro build` says
|
|
173
|
+
* "Complete!". It was hardcoded to 2026-01-01 with a comment claiming the caller overwrote it.
|
|
174
|
+
*
|
|
175
|
+
* It is now derived from the argument rather than equal to it - see the fortnight margin in
|
|
176
|
+
* scaffold.ts, which exists because a date NEWER than the installed runtime does not build at
|
|
177
|
+
* all. Both directions are traps and they pull in opposite ways.
|
|
178
|
+
*/
|
|
179
|
+
const config = scaffold({ domain: 'a.com', packageVersion: '1.0.0', today: '2026-08-26' })[
|
|
180
|
+
'wrangler.jsonc'
|
|
181
|
+
]!;
|
|
182
|
+
assert.match(config, /"compatibility_date":\s*"2026-08-12"/, 'derived from it, a fortnight back');
|
|
183
|
+
|
|
184
|
+
const other = scaffold({ domain: 'a.com', packageVersion: '1.0.0', today: '2027-03-04' })[
|
|
185
|
+
'wrangler.jsonc'
|
|
186
|
+
]!;
|
|
187
|
+
assert.match(other, /"compatibility_date":\s*"2027-02-18"/, 'and it still tracks the argument');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('a malformed date is refused rather than written into wrangler.jsonc', () => {
|
|
191
|
+
assert.throws(
|
|
192
|
+
() => scaffold({ domain: 'a.com', packageVersion: '1.0.0', today: 'today' }),
|
|
193
|
+
/YYYY-MM-DD/,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('the scaffolded compatibility_date is never in the future of the installed runtime', () => {
|
|
198
|
+
/*
|
|
199
|
+
* A date newer than the runtime the installed wrangler bundles is refused outright - miniflare
|
|
200
|
+
* throws ERR_FUTURE_COMPATIBILITY_DATE and the site does not build AT ALL, from the moment it
|
|
201
|
+
* is scaffolded. Writing today's date walks into that for anyone whose wrangler is a few weeks
|
|
202
|
+
* old, which is most people most of the time. Two client sites hit it independently.
|
|
203
|
+
*
|
|
204
|
+
* Measured: on wrangler 4.126 the ceiling is today, so today's date happens to work on a
|
|
205
|
+
* machine that just installed. The margin is for every machine that did not.
|
|
206
|
+
*/
|
|
207
|
+
const written = (today: string) =>
|
|
208
|
+
scaffold({ domain: 'a.com', packageVersion: '1.0.0', today })['wrangler.jsonc']!.match(
|
|
209
|
+
/"compatibility_date":\s*"([\d-]+)"/,
|
|
210
|
+
)![1]!;
|
|
211
|
+
|
|
212
|
+
for (const today of ['2026-08-27', '2026-01-01', '2026-03-01', '2027-12-31']) {
|
|
213
|
+
const gap = (Date.parse(today) - Date.parse(written(today))) / 86_400_000;
|
|
214
|
+
assert.equal(
|
|
215
|
+
gap,
|
|
216
|
+
14,
|
|
217
|
+
`${today} should scaffold a date a fortnight back, got ${written(today)}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('the margin crosses a month and a year boundary correctly', () => {
|
|
223
|
+
// Naive string arithmetic gets 2026-01-05 minus 14 days wrong; this is why it goes through Date.
|
|
224
|
+
const at = (today: string) =>
|
|
225
|
+
scaffold({ domain: 'a.com', packageVersion: '1.0.0', today })['wrangler.jsonc']!.match(
|
|
226
|
+
/"compatibility_date":\s*"([\d-]+)"/,
|
|
227
|
+
)![1]!;
|
|
228
|
+
assert.equal(at('2026-01-05'), '2025-12-22');
|
|
229
|
+
assert.equal(at('2026-03-05'), '2026-02-19', 'and February');
|
|
230
|
+
});
|