@erclx/canon 4.101.0 → 4.103.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/SKILL.md +5 -6
- package/claude/skills/canon-cli/SKILL.md +4 -1
- package/claude/skills/canon-rollout/SKILL.md +7 -6
- package/claude/skills/draft-readme/REQUIREMENT.md +2 -0
- package/claude/skills/draft-readme/SKILL.md +9 -1
- package/claude/skills/git-ship/SKILL.md +3 -7
- package/claude/skills/memory-capture/SKILL.md +0 -6
- package/claude/skills/memory-review/REQUIREMENT.md +1 -1
- package/claude/skills/memory-review/SKILL.md +2 -3
- package/claude/skills/plan-groundwork/SKILL.md +3 -2
- package/claude/skills/target-check/REQUIREMENT.md +42 -0
- package/claude/skills/target-check/SKILL.md +91 -0
- package/claude/skills/target-check/references/domains.md +88 -0
- package/claude/skills/ui-checklist/SKILL.md +17 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/install-and-sync.md +7 -1
- package/docs/agents/scripting.md +1 -1
- package/docs/target-projects.md +3 -1
- package/docs/workflow/ai-workflow.md +16 -13
- package/governance/rules/ci/700-ci-workflow.md +11 -0
- package/package.json +1 -1
- package/scripts/core/regen-web-favicon.ts +9 -40
- package/scripts/core/regen-web-previews.ts +1 -0
- package/src/claude/cases/setup.ts +8 -0
- package/src/commands/design.ts +8 -2
- package/src/commands/tooling.ts +92 -19
- package/src/design/css.ts +5 -2
- package/src/design/favicon.ts +33 -0
- package/src/design/render.ts +46 -6
- package/src/design/tokens.ts +1 -1
- package/standards/groundwork.md +8 -2
- package/standards/readme.md +22 -3
- package/tooling/astro/manifest.toml +1 -0
- package/tooling/base/manifest.toml +2 -2
- package/tooling/base/reference.md +1 -1
- package/tooling/nextjs/manifest.toml +1 -0
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +110 -0
- package/tooling/web/configs/e2e/screenshot.ts +69 -15
- package/tooling/web/configs/scripts/lib/preview-server.sh +31 -0
- package/tooling/web/configs/scripts/readme-screenshot.sh +26 -0
- package/tooling/web/configs/scripts/screenshot.sh +1 -24
- package/tooling/web/manifest.toml +1 -0
- package/tooling/web/reference.md +6 -1
|
@@ -24,6 +24,7 @@ packages = [
|
|
|
24
24
|
|
|
25
25
|
[scripts.override]
|
|
26
26
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
27
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4321) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
27
28
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro dev"
|
|
28
29
|
"preview" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && astro preview"
|
|
29
30
|
|
|
@@ -18,9 +18,9 @@ packages = [
|
|
|
18
18
|
|
|
19
19
|
[scripts]
|
|
20
20
|
"check:spell" = "cspell '**' '.*/**' '.*' --no-progress --color --show-context"
|
|
21
|
-
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2
|
|
21
|
+
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && find scripts -name '*.sh' -exec shfmt --diff --indent 2 {} +"
|
|
22
22
|
"check:shell" = "find scripts -name '*.sh' -exec shellcheck --severity=warning {} +"
|
|
23
|
-
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2
|
|
23
|
+
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && find scripts -name '*.sh' -exec shfmt --write --indent 2 {} +"
|
|
24
24
|
"prepare" = "husky"
|
|
25
25
|
"check" = "./scripts/verify.sh"
|
|
26
26
|
|
|
@@ -35,7 +35,7 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
35
35
|
- Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
|
|
36
36
|
- Formatting: Prettier for what it parses, shfmt for shell. Two formatters because Prettier has no shell parser.
|
|
37
37
|
- Spelling: cspell over the whole tree, with project vocabulary split into a project-terms dictionary and a tech-stack one.
|
|
38
|
-
- Shell: shfmt formats and shellcheck lints at warning severity.
|
|
38
|
+
- Shell: shfmt formats and shellcheck lints at warning severity. Both take their files from `find scripts -name '*.sh'`, since `scripts/` can mix shell with TypeScript and shellcheck has no directory mode. A shell script with a shebang and no `.sh` extension is covered by neither, so give it the extension.
|
|
39
39
|
- Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
|
|
40
40
|
- Dev dependencies: `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
|
|
41
41
|
|
|
@@ -17,6 +17,7 @@ packages = [
|
|
|
17
17
|
[scripts.override]
|
|
18
18
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && next dev --port $((3000 + WORKTREE_PORT_OFFSET))"
|
|
19
19
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
20
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
20
21
|
|
|
21
22
|
[gitignore]
|
|
22
23
|
"# Next" = [".next/"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
name: README Screenshot
|
|
2
|
+
# canon-no-seed: a stack-specific workflow with no root counterpart by design, since this repository reports its own capture in pr-visual-checks.yml and never commits it.
|
|
3
|
+
|
|
4
|
+
# No `workflow_dispatch`: the job commits onto a pull request's head branch,
|
|
5
|
+
# and a manual run has no pull request to commit onto.
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
paths:
|
|
9
|
+
# Project-specific. Replace the placeholder with the paths whose change
|
|
10
|
+
# moves the README frame, such as the component and styles it renders.
|
|
11
|
+
# The three entries below the placeholder keep the job firing on this
|
|
12
|
+
# file's own installation, which is what lets the first step refuse an
|
|
13
|
+
# unfilled template instead of the job never running.
|
|
14
|
+
- 'REPLACE_WITH_PROJECT_PATHS'
|
|
15
|
+
- 'scripts/readme-screenshot.sh'
|
|
16
|
+
- 'scripts/lib/preview-server.sh'
|
|
17
|
+
- '.github/workflows/readme-screenshot.yml'
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
group: readme-screenshot-${{ github.ref }}
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
capture:
|
|
25
|
+
name: 🖼️ Refresh README Screenshot
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
# The default GITHUB_TOKEN cannot push to a fork's branch, so a PR from
|
|
28
|
+
# outside this repository is skipped rather than left to fail on the push
|
|
29
|
+
# step below.
|
|
30
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
31
|
+
permissions:
|
|
32
|
+
contents: write
|
|
33
|
+
steps:
|
|
34
|
+
- name: Checkout
|
|
35
|
+
uses: actions/checkout@v4
|
|
36
|
+
with:
|
|
37
|
+
ref: ${{ github.head_ref }}
|
|
38
|
+
|
|
39
|
+
- name: Refuse An Unfilled Template
|
|
40
|
+
# A path list left at its placeholder never fires on the pages it was
|
|
41
|
+
# installed to watch, and that failure leaves no run, no diff, and no
|
|
42
|
+
# complaint. Failing here is what tells the project it has not
|
|
43
|
+
# finished installing.
|
|
44
|
+
run: |
|
|
45
|
+
if grep -q "^ *- 'REPLACE_WITH_PROJECT_PATHS'" .github/workflows/readme-screenshot.yml; then
|
|
46
|
+
echo "::error file=.github/workflows/readme-screenshot.yml::The paths list still holds REPLACE_WITH_PROJECT_PATHS. Replace it with the paths that move the README frame."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
- name: Setup Node
|
|
51
|
+
uses: actions/setup-node@v4
|
|
52
|
+
with:
|
|
53
|
+
node-version: '22'
|
|
54
|
+
|
|
55
|
+
- name: Setup Bun
|
|
56
|
+
uses: oven-sh/setup-bun@v2
|
|
57
|
+
with:
|
|
58
|
+
bun-version: latest
|
|
59
|
+
|
|
60
|
+
- name: Install Dependencies
|
|
61
|
+
run: bun install --frozen-lockfile
|
|
62
|
+
|
|
63
|
+
- name: Cache Playwright Browser
|
|
64
|
+
id: playwright-cache
|
|
65
|
+
uses: actions/cache@v4
|
|
66
|
+
with:
|
|
67
|
+
path: ~/.cache/ms-playwright
|
|
68
|
+
key: playwright-${{ runner.os }}-chromium-${{ hashFiles('**/package.json') }}
|
|
69
|
+
|
|
70
|
+
- name: Install Playwright System Dependencies
|
|
71
|
+
# Unconditional: the cache holds browser binaries and nothing apt
|
|
72
|
+
# wrote, so gating this on a cache hit leaves a warm run launching
|
|
73
|
+
# chromium without the libraries it needs.
|
|
74
|
+
run: bunx playwright install-deps chromium
|
|
75
|
+
|
|
76
|
+
- name: Install Playwright Browser
|
|
77
|
+
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
78
|
+
run: bunx playwright install chromium
|
|
79
|
+
|
|
80
|
+
- name: Capture README Screenshot
|
|
81
|
+
run: bun run readme-screenshot
|
|
82
|
+
|
|
83
|
+
- name: Check For Changes
|
|
84
|
+
id: diff
|
|
85
|
+
run: |
|
|
86
|
+
if git status --porcelain assets/evidence/readme | grep -q .; then
|
|
87
|
+
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
88
|
+
else
|
|
89
|
+
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
- name: Commit And Push
|
|
93
|
+
# Self-terminating: this push retriggers this same workflow on the PR,
|
|
94
|
+
# which captures again, finds no diff against what was just pushed,
|
|
95
|
+
# and stops here without pushing a second time.
|
|
96
|
+
if: steps.diff.outputs.changed == 'true'
|
|
97
|
+
# The hooks are off because this commit carries nothing they can read.
|
|
98
|
+
# `pre-push` runs the full verify, whose format step shells out to
|
|
99
|
+
# `shfmt`, which the runner does not install, so a push through them
|
|
100
|
+
# exits 127. The jobs in verify.yml already run those checks against
|
|
101
|
+
# the source, and what this step pushes is PNG bytes a capture
|
|
102
|
+
# produced.
|
|
103
|
+
env:
|
|
104
|
+
HUSKY: 0
|
|
105
|
+
run: |
|
|
106
|
+
git config user.name "github-actions[bot]"
|
|
107
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
108
|
+
git add assets/evidence/readme
|
|
109
|
+
git commit -m "chore(readme): refresh README screenshot"
|
|
110
|
+
git push origin "HEAD:${{ github.head_ref }}"
|
|
@@ -9,12 +9,33 @@ interface CaptureSection {
|
|
|
9
9
|
selector: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface Viewport {
|
|
13
|
+
readonly name: string
|
|
14
|
+
readonly width: number
|
|
15
|
+
readonly height: number
|
|
16
|
+
/** Whether a case flagged `evidence: true` also writes its frames at this width. */
|
|
17
|
+
readonly evidence: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The breakpoint buckets every case is crossed with, named by width. 320 is
|
|
22
|
+
* the base a page has to hold without wrapping or clipping. Committing every
|
|
23
|
+
* width multiplies the tracked frames by the width count, so only the base and
|
|
24
|
+
* the desktop width carry `evidence`.
|
|
25
|
+
*/
|
|
26
|
+
const VIEWPORTS: readonly Viewport[] = [
|
|
27
|
+
{ name: '320', width: 320, height: 800, evidence: true },
|
|
28
|
+
{ name: '768', width: 768, height: 800, evidence: false },
|
|
29
|
+
{ name: '1280', width: 1280, height: 800, evidence: true },
|
|
30
|
+
{ name: '1536', width: 1536, height: 800, evidence: false },
|
|
31
|
+
]
|
|
32
|
+
|
|
12
33
|
interface CaptureCase {
|
|
13
34
|
section: string
|
|
14
35
|
theme: string
|
|
15
36
|
route: string
|
|
16
|
-
width
|
|
17
|
-
|
|
37
|
+
/** Names of viewports this case skips, for a route that reflows rather than breaks at that width. */
|
|
38
|
+
excludeViewports?: readonly string[]
|
|
18
39
|
evidence?: boolean
|
|
19
40
|
sections?: CaptureSection[]
|
|
20
41
|
setup?: (page: Page) => Promise<void>
|
|
@@ -50,8 +71,6 @@ const CASES: CaptureCase[] = [
|
|
|
50
71
|
section: 'home',
|
|
51
72
|
theme: 'default',
|
|
52
73
|
route: '/',
|
|
53
|
-
width: 1280,
|
|
54
|
-
height: 800,
|
|
55
74
|
evidence: true,
|
|
56
75
|
sections: HOME_SECTIONS,
|
|
57
76
|
},
|
|
@@ -59,13 +78,28 @@ const CASES: CaptureCase[] = [
|
|
|
59
78
|
section: 'home',
|
|
60
79
|
theme: 'dark',
|
|
61
80
|
route: '/',
|
|
62
|
-
width: 1280,
|
|
63
|
-
height: 800,
|
|
64
81
|
sections: HOME_SECTIONS,
|
|
65
82
|
setup: (page) => page.emulateMedia({ colorScheme: 'dark' }),
|
|
66
83
|
},
|
|
67
84
|
]
|
|
68
85
|
|
|
86
|
+
/** One case at one viewport, which is the unit a filter selects and a context is built for. */
|
|
87
|
+
interface CaptureRun {
|
|
88
|
+
readonly captureCase: CaptureCase
|
|
89
|
+
readonly viewport: Viewport
|
|
90
|
+
readonly label: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ALL_RUNS: readonly CaptureRun[] = CASES.flatMap((captureCase) =>
|
|
94
|
+
VIEWPORTS.filter(
|
|
95
|
+
(viewport) => !captureCase.excludeViewports?.includes(viewport.name),
|
|
96
|
+
).map((viewport) => ({
|
|
97
|
+
captureCase,
|
|
98
|
+
viewport,
|
|
99
|
+
label: `${captureCase.section}/${viewport.name}--${captureCase.theme}`,
|
|
100
|
+
})),
|
|
101
|
+
)
|
|
102
|
+
|
|
69
103
|
const args = process.argv.slice(2)
|
|
70
104
|
const checkConsoleClean = args.includes('--check-console-clean')
|
|
71
105
|
const requireBaseUrl = args.includes('--require-base-url')
|
|
@@ -87,6 +121,28 @@ try {
|
|
|
87
121
|
|
|
88
122
|
const OUT_DIR = path.join('screenshots', hostname)
|
|
89
123
|
|
|
124
|
+
const SCREENSHOT_FILTER = process.env.SCREENSHOT_FILTER?.trim() ?? ''
|
|
125
|
+
|
|
126
|
+
const filterTerms = SCREENSHOT_FILTER.split(',')
|
|
127
|
+
.map((term) => term.trim())
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
|
|
130
|
+
const RUNS =
|
|
131
|
+
filterTerms.length > 0
|
|
132
|
+
? ALL_RUNS.filter((run) =>
|
|
133
|
+
filterTerms.some((term) => run.label.includes(term)),
|
|
134
|
+
)
|
|
135
|
+
: ALL_RUNS
|
|
136
|
+
|
|
137
|
+
if (filterTerms.length > 0 && RUNS.length === 0) {
|
|
138
|
+
console.error(
|
|
139
|
+
`No cases matched SCREENSHOT_FILTER=${SCREENSHOT_FILTER}. Available labels:\n${ALL_RUNS.map(
|
|
140
|
+
(run) => ` ${run.label}`,
|
|
141
|
+
).join('\n')}`,
|
|
142
|
+
)
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
|
|
90
146
|
/** Every wait on a lazy load is bounded explicitly. */
|
|
91
147
|
const SETTLE_TIMEOUT = 15_000
|
|
92
148
|
|
|
@@ -198,12 +254,12 @@ const browser = await chromium.launch()
|
|
|
198
254
|
const consoleErrors: string[] = []
|
|
199
255
|
let ranCases = 0
|
|
200
256
|
|
|
201
|
-
for (const captureCase of
|
|
257
|
+
for (const { captureCase, viewport, label } of RUNS) {
|
|
202
258
|
if (captureCase.evidence && requireBaseUrl) continue
|
|
203
259
|
|
|
204
260
|
ranCases++
|
|
205
261
|
const context = await browser.newContext({
|
|
206
|
-
viewport: { width:
|
|
262
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
207
263
|
// A page honoring the preference lands its own motion at the end state,
|
|
208
264
|
// which is what a still frame has to show. Playwright reads it as a
|
|
209
265
|
// context option, so it reaches no page written beside `viewport` above.
|
|
@@ -214,9 +270,7 @@ for (const captureCase of CASES) {
|
|
|
214
270
|
if (checkConsoleClean) {
|
|
215
271
|
page.on('console', (msg) => {
|
|
216
272
|
if (msg.type() === 'error') {
|
|
217
|
-
consoleErrors.push(
|
|
218
|
-
`${captureCase.section}/${captureCase.theme}: ${msg.text()}`,
|
|
219
|
-
)
|
|
273
|
+
consoleErrors.push(`${label}: ${msg.text()}`)
|
|
220
274
|
}
|
|
221
275
|
})
|
|
222
276
|
}
|
|
@@ -226,21 +280,21 @@ for (const captureCase of CASES) {
|
|
|
226
280
|
await page.goto(`${BASE_URL}${captureCase.route}`)
|
|
227
281
|
await page.waitForLoadState('networkidle')
|
|
228
282
|
|
|
229
|
-
const label = `${captureCase.section}/${captureCase.theme}`
|
|
230
283
|
const sections = await settle(page, label, captureCase.sections ?? [])
|
|
231
284
|
|
|
232
285
|
const sweepDir = path.join(OUT_DIR, captureCase.section)
|
|
233
286
|
const evidenceDir = path.join('evidence', captureCase.section)
|
|
234
|
-
const file = `${captureCase.theme}.png`
|
|
287
|
+
const file = `${viewport.name}--${captureCase.theme}.png`
|
|
288
|
+
const writesEvidence = captureCase.evidence && viewport.evidence
|
|
235
289
|
|
|
236
290
|
await shoot(page, sweepDir, file)
|
|
237
|
-
if (
|
|
291
|
+
if (writesEvidence) await shoot(page, evidenceDir, file)
|
|
238
292
|
|
|
239
293
|
if (sections.length > 0) await markFloatingChrome(page)
|
|
240
294
|
|
|
241
295
|
for (const { name, selector } of sections) {
|
|
242
296
|
await shoot(page, path.join(sweepDir, name), file, selector)
|
|
243
|
-
if (
|
|
297
|
+
if (writesEvidence) {
|
|
244
298
|
await shoot(page, path.join(evidenceDir, name), file, selector)
|
|
245
299
|
}
|
|
246
300
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sourced, not executed. Builds the site, serves it in the background on
|
|
3
|
+
# PREVIEW_PORT, and blocks until it answers or exits with an error. Shared by
|
|
4
|
+
# scripts/screenshot.sh and scripts/readme-screenshot.sh so the two don't
|
|
5
|
+
# carry two copies of the same startup boilerplate.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
: "${PREVIEW_PORT:=4173}"
|
|
9
|
+
|
|
10
|
+
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
11
|
+
echo "Port $PREVIEW_PORT already in use. Stop the listening process or set PREVIEW_PORT." >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
bun run build
|
|
16
|
+
bun run preview >/dev/null 2>&1 &
|
|
17
|
+
PREVIEW_PID=$!
|
|
18
|
+
# shellcheck disable=SC2154 # pid is bound by the for loop inside the single-quoted trap body, which shellcheck does not track there
|
|
19
|
+
trap 'kill "$PREVIEW_PID" 2>/dev/null || true; if command -v lsof >/dev/null 2>&1; then for pid in $(lsof -ti tcp:"$PREVIEW_PORT" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done; else echo "lsof not found on PATH; cannot confirm the preview port is clear of a detached grandchild." >&2; fi; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
20
|
+
|
|
21
|
+
for _ in $(seq 1 40); do
|
|
22
|
+
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
23
|
+
break
|
|
24
|
+
fi
|
|
25
|
+
sleep 0.25
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
if ! curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
29
|
+
echo "Preview server not responding on port $PREVIEW_PORT after 10s." >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Project-specific. Both are placeholders the project fills in, and the script
|
|
5
|
+
# refuses to run until it does, since a shipped default would capture the wrong
|
|
6
|
+
# part of the page in every project but the one it was written for.
|
|
7
|
+
README_FILTER='' # case label to capture, such as header/1280
|
|
8
|
+
README_EVIDENCE_DIR='assets/evidence/readme'
|
|
9
|
+
|
|
10
|
+
if [[ -z "$README_FILTER" ]]; then
|
|
11
|
+
echo "README_FILTER is unset in scripts/readme-screenshot.sh. Name the case label the README frame is captured from, such as header/1280." >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/preview-server.sh"
|
|
16
|
+
|
|
17
|
+
SCREENSHOT_BASE_URL="http://localhost:$PREVIEW_PORT" SCREENSHOT_FILTER="$README_FILTER" bun e2e/screenshot.ts
|
|
18
|
+
|
|
19
|
+
# The filter's section and viewport name the folder and the file prefix the
|
|
20
|
+
# capture wrote under screenshots/<hostname>/.
|
|
21
|
+
frame_dir="screenshots/localhost/$(dirname "$README_FILTER")"
|
|
22
|
+
frame_prefix="$(basename "$README_FILTER")"
|
|
23
|
+
|
|
24
|
+
mkdir -p "$README_EVIDENCE_DIR"
|
|
25
|
+
cp "$frame_dir/$frame_prefix--default.png" "$README_EVIDENCE_DIR/light.png"
|
|
26
|
+
cp "$frame_dir/$frame_prefix--dark.png" "$README_EVIDENCE_DIR/dark.png"
|
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
7
|
-
echo "Port $PREVIEW_PORT already in use. Stop the listening process or set PREVIEW_PORT." >&2
|
|
8
|
-
exit 1
|
|
9
|
-
fi
|
|
10
|
-
|
|
11
|
-
bun run build
|
|
12
|
-
bun run preview >/dev/null 2>&1 &
|
|
13
|
-
PREVIEW_PID=$!
|
|
14
|
-
# shellcheck disable=SC2154 # pid is bound by the for loop inside the single-quoted trap body, which shellcheck does not track there
|
|
15
|
-
trap 'kill "$PREVIEW_PID" 2>/dev/null || true; if command -v lsof >/dev/null 2>&1; then for pid in $(lsof -ti tcp:"$PREVIEW_PORT" 2>/dev/null); do kill "$pid" 2>/dev/null || true; done; else echo "lsof not found on PATH; cannot confirm the preview port is clear of a detached grandchild." >&2; fi; wait "$PREVIEW_PID" 2>/dev/null || true' EXIT
|
|
16
|
-
|
|
17
|
-
for _ in $(seq 1 40); do
|
|
18
|
-
if curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
19
|
-
break
|
|
20
|
-
fi
|
|
21
|
-
sleep 0.25
|
|
22
|
-
done
|
|
23
|
-
|
|
24
|
-
if ! curl -sSf "http://localhost:$PREVIEW_PORT/" >/dev/null 2>&1; then
|
|
25
|
-
echo "Preview server not responding on port $PREVIEW_PORT after 10s." >&2
|
|
26
|
-
exit 1
|
|
27
|
-
fi
|
|
4
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/preview-server.sh"
|
|
28
5
|
|
|
29
6
|
SCREENSHOT_BASE_URL="http://localhost:$PREVIEW_PORT" bun e2e/screenshot.ts
|
|
@@ -52,6 +52,7 @@ packages = [
|
|
|
52
52
|
[scripts.override]
|
|
53
53
|
"lint" = "eslint . --max-warnings 0"
|
|
54
54
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4173) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
55
|
+
"readme-screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 4173) && export PREVIEW_PORT && bash scripts/readme-screenshot.sh"
|
|
55
56
|
|
|
56
57
|
[gitignore]
|
|
57
58
|
"# Build" = ["dist/"]
|
package/tooling/web/reference.md
CHANGED
|
@@ -12,7 +12,11 @@ Golden config files live in `tooling/web/configs/` and are copied into the targe
|
|
|
12
12
|
|
|
13
13
|
- `eslint.config.js`: flat config with `@eslint/js`, `typescript-eslint`, React hooks, import sort, check-file, vitest rules scoped to test files, `eslint-config-prettier` last.
|
|
14
14
|
- `src/test/setup.ts`: `@testing-library/jest-dom` import, `cleanup` after each test.
|
|
15
|
-
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, a route, and
|
|
15
|
+
- `e2e/screenshot.ts`: capture template. A single `CASES` record at the top carries one entry per output file, each naming a section, a theme, and a route, and the loop below crosses every case with the `VIEWPORTS` array and writes `screenshots/<hostname>/<section>/<width>--<theme>.png`, keyed on `SCREENSHOT_BASE_URL`'s hostname so a local and a deployed run land in different folders. `VIEWPORTS` holds four breakpoint buckets named by width, 320, 768, 1280 and 1536, with 320 as the base a page has to hold without wrapping or clipping. A case flagged `evidence: true` additionally writes `evidence/<section>/<width>--<theme>.png`, with no hostname segment, at the viewports flagged `evidence` alone, which are 320 and 1280. Committing all four multiplies the tracked frames by the width count, so the flag is what keeps the tracked set at twice the sections rather than four times. A case may name `excludeViewports` to skip a width, for a route whose long-form prose reflows at 320 instead of breaking. `SCREENSHOT_FILTER` narrows a run to the cases whose label, `<section>/<width>--<theme>`, contains any comma-separated term, exits 1 when nothing matches and lists the labels that exist, and leaves the rest of the sweep alone. `--require-base-url` skips a flagged case entirely, so a production smoke run neither writes to the committed path nor counts an evidence route in its console-clean check, and exits 1 when that leaves zero cases run, so flagging every case cannot silence the production check without saying so. Per-project cases extend the one record. A route's widths and themes sit together under its section folder, so the filename carries both. A case may also name `sections`, one `{ name, selector }` per addressable part of the route, and each writes its own frame at `screenshots/<hostname>/<section>/<name>/<width>--<theme>.png` beside the whole-page one, plus `evidence/<section>/<name>/<width>--<theme>.png` where the case is flagged and the width carries `evidence`. The field is optional, so a case that names none captures the page alone as before, and a selector matching nothing is reported and skipped rather than raised, since `CASES` is a template a project rewrites and a target that syncs this file before editing its own list would otherwise capture nothing at all. Testing that every named section resolves belongs to the project that owns the selectors, beside its own page. Every case settles before anything is shot: the context is created with `reducedMotion: 'reduce'`, each named section is scrolled into view so an `IntersectionObserver` fires and a frame marked `loading="lazy"` loads, the page returns to the top, and fonts, images and same-origin frames are waited out on a bounded condition rather than on a second `networkidle`, which is a lifecycle event of the navigation and does not re-arm for a load a scroll started. A section frame drops the page's floating chrome, being whatever computes to `position: sticky` or `fixed`, because a section taller than the viewport is scrolled into view to be cut and a sticky bar otherwise rides that scroll into the middle of the frame. The whole-page frame is shot first and keeps its chrome. `--check-console-clean` collects `console`-level error messages per case and exits 1 with the list if any fired, turning the capture into a smoke check. `--require-base-url` exits 1 before launching a browser when `SCREENSHOT_BASE_URL` is unset, guarding a script meant to run against a real deployment from silently capturing `localhost`.
|
|
16
|
+
- `scripts/screenshot.sh`: sources `scripts/lib/preview-server.sh`, then runs the capture against the served preview.
|
|
17
|
+
- `scripts/lib/preview-server.sh`: sourced, never executed. Builds, serves on `PREVIEW_PORT`, blocks until the port answers, and traps the whole process group on exit, including the detached grandchild `lsof` finds. Both capture scripts source it so the startup lives once.
|
|
18
|
+
- `scripts/readme-screenshot.sh`: captures one case under `SCREENSHOT_FILTER` and copies its light and dark frames to `assets/evidence/readme/`. `README_FILTER` ships empty with `header/1280` as its example, and the script exits 1 until a project fills it, since a shipped default would capture the wrong part of the page in every project but one. Its copy reads from `screenshots/<hostname>/`, which is where the harness writes.
|
|
19
|
+
- `.github/workflows/readme-screenshot.yml`: refreshes that frame on a pull request and commits it back to the branch. The path list ships as `REPLACE_WITH_PROJECT_PATHS`, and the job's first step after checkout exits 1 while it stays, since a job that never fires leaves no run and no diff to notice. The workflow's own path is in the list so the installing pull request reaches that step. When a commit-back is right and when a report is, `governance/rules/ci/700-ci-workflow.md` states the test.
|
|
16
20
|
- `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for ESLint, Tailwind, Playwright, Vitest.
|
|
17
21
|
- `.github/workflows/verify.yml`: `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests` jobs.
|
|
18
22
|
- `scripts/verify.sh`: extends base verify with typecheck, lint, unit tests, and build in the full order.
|
|
@@ -106,6 +110,7 @@ Append rows:
|
|
|
106
110
|
| `bun run test:e2e` | Run Playwright E2E tests. |
|
|
107
111
|
| `bun run test:e2e:changed` | Run Playwright E2E tests for specs the import graph reaches from the current diff. |
|
|
108
112
|
| `bun run screenshot` | Build, preview, then capture screenshots. |
|
|
113
|
+
| `bun run readme-screenshot` | Build, preview, then capture the README frame into `assets/evidence/readme/`. |
|
|
109
114
|
| `bun run smoke:prod` | Capture against `SCREENSHOT_BASE_URL`, requiring it set and failing on any console error. |
|
|
110
115
|
|
|
111
116
|
`canon tooling verify <stack>` is the only automated caller of `bun run screenshot`, running it for any stack whose `package.json` declares the script and asserting that PNG files land under `screenshots/`. It counts them with a recursive find carrying no depth limit, so the section folders the seed writes satisfy the assertion without a change to it. Do not flatten the layout to protect that check. No ship chain captures a screenshot, so the output path the seed writes is a contract that one verifier reads rather than a default a ship step depends on.
|