@codepress/design-kit 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -19,6 +19,47 @@ It runs on Node 20 or later. Playwright is a peer dependency, so the capture
19
19
  drives the Playwright and Chromium your project already has. The `kit`
20
20
  commands need neither.
21
21
 
22
+ ## Set up in your repository
23
+
24
+ `design-kit init` sets a kit up as one reviewable change. Run it in the
25
+ repository's root, or in a monorepo with `--root <package>` (the config goes
26
+ there; the workflow, skill and instructions line go at the repository root):
27
+
28
+ ```bash
29
+ npx @codepress/design-kit init --dry-run # what it found and would write
30
+ npx @codepress/design-kit init
31
+ ```
32
+
33
+ It looks for Storybook (a `storybook` or `@storybook/*` dependency, or a
34
+ `.storybook` folder, in the project or its workspace packages) and its
35
+ version, the framework (Next.js, Vite, Remix, Create React App, Astro or
36
+ other), the package manager and lockfile, the dev or start script and the
37
+ app's local URL, an existing Playwright, and whether the app switches to dark
38
+ with a class or a `data-theme` attribute. Then it writes:
39
+
40
+ - `design-kit.config.json`: the `stories` section when Storybook is found, a
41
+ `pages` section with `TODO` selectors to fill in, and the `theme`.
42
+ - `.github/workflows/design-kit.yml`: a daily build (and `workflow_dispatch`)
43
+ on `ubuntu-latest`. It skips when there are no commits since the newest
44
+ `design-kit` artifact's commit; otherwise it installs dependencies, this
45
+ package at a pinned version and Chromium, builds Storybook and captures the
46
+ stories, captures the pages once no `TODO` is left in the config, and
47
+ uploads the `design-kit` artifact for 30 days. A run with nothing to
48
+ capture yet (no stories, pages still `TODO`) ends with a warning and
49
+ uploads nothing. The page capture gets
50
+ `DESIGN_KIT_USERNAME` and `DESIGN_KIT_PASSWORD` from repository secrets for
51
+ a sign-in module (`pages.auth.setup`).
52
+ - `.claude/skills/design-kit/SKILL.md`: how agents fetch the newest kit,
53
+ compose mockups from it and caption them, and `.agents/skills/design-kit` as
54
+ a symlink to it (a copy when the repository keeps real folders there).
55
+ - One line in `AGENTS.md`, or `CLAUDE.md` when only that exists (a new
56
+ `AGENTS.md` when neither does), pointing at the skill.
57
+
58
+ It prints what it found and what is still to decide: the page selectors, the
59
+ start command, sign-in, a missing Storybook. It never overwrites an existing
60
+ file without `--force` (a repository with a `design-kit.config.json` exits 1,
61
+ "already set up") and never writes a secret.
62
+
22
63
  ## Configuration
23
64
 
24
65
  The commands read `design-kit.config.json` from the working directory or the
@@ -109,8 +150,9 @@ export returns (`async ({ browser, baseUrl, config }) => state`).
109
150
  for apps that render a different frame at phone width; `layoutSlots[<that
110
151
  value>]` then replaces `slots`.
111
152
  - `forbidStrings`: values that must never reach the kit, such as a test
112
- account's token. A page whose output contains one, as written, escaped for
113
- HTML or JSON, or (for a token) any long segment of it, is not written.
153
+ account's token. When any page's output contains one, as written, escaped
154
+ for HTML or JSON, or (for a token) any long segment of it, the capture
155
+ fails (exit 1) and writes no page, so a CI job uploads nothing.
114
156
  - `fontsPath`, `copyFonts`: the fonts the pages load are copied into the
115
157
  kit's `fonts/` folder (`copyFonts: false` keeps them out); `fontsPath` says
116
158
  which URL path holds them when not every font file is one.
@@ -122,6 +164,7 @@ value>]` then replaces `slots`.
122
164
  ## Commands
123
165
 
124
166
  ```bash
167
+ design-kit init [--root dir] [--dry-run] [--force]
125
168
  design-kit capture stories [--config f] [--out dir] [--storybook-dir dir]
126
169
  [--only id,... | --only-file f] [--base previous-kit]
127
170
  design-kit capture pages [--config f] [--out dir] [--base-url url] [--only name,...]
@@ -136,7 +179,8 @@ byte. The two captures share one kit folder and keep each other's files: run
136
179
  both with the same `--out`.
137
180
 
138
181
  Exit codes: 0 done, 1 a capture could not run (no Storybook build, the app
139
- did not start), 2 a config or usage error. A story or page that fails is a
182
+ did not start, a forbidden string) or `init` found the repository already set
183
+ up, 2 a config or usage error. A story or page that fails is a
140
184
  `gaps.json` row and does not fail the run.
141
185
 
142
186
  Operational knobs: `DESIGN_KIT_CONCURRENCY` (browser pages at once, default
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@codepress/design-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Capture your Storybook and real app pages as a static design kit, and compose self-contained mockups from it",
5
5
  "type": "module",
6
6
  "bin": {
7
- "design-kit": "./bin/design-kit.mjs"
7
+ "design-kit": "bin/design-kit.mjs"
8
8
  },
9
9
  "main": "./src/index.mjs",
10
10
  "types": "./src/index.d.ts",
@@ -13,7 +13,10 @@
13
13
  "types": "./src/index.d.ts",
14
14
  "default": "./src/index.mjs"
15
15
  },
16
- "./kit": "./src/kit.mjs",
16
+ "./kit": {
17
+ "types": "./src/kit.d.ts",
18
+ "default": "./src/kit.mjs"
19
+ },
17
20
  "./schema.json": "./schema/design-kit.config.schema.json",
18
21
  "./package.json": "./package.json"
19
22
  },
@@ -376,8 +376,16 @@ function themeClassesOf(theme) {
376
376
  * browser}: set `interrupted` to stop between pages; `browser` is set once
377
377
  * launched so a signal handler can close it).
378
378
  *
379
- * Returns 0, or 130 when interrupted (nothing is written then).
379
+ * Returns 0, or 130 when interrupted (nothing is written then). Throws,
380
+ * writing nothing, when any page's markup carries a forbidStrings value.
380
381
  */
382
+ /** A page's markup carries a forbidStrings value. */
383
+ class ForbiddenStringError extends Error {
384
+ constructor() {
385
+ super("the page's markup carries a forbidStrings value");
386
+ }
387
+ }
388
+
381
389
  export async function capturePages(config, options = {}) {
382
390
  const settings = config.pages;
383
391
  if (!settings) {
@@ -442,6 +450,7 @@ export async function capturePages(config, options = {}) {
442
450
  );
443
451
  const timings = {};
444
452
  const gaps = [];
453
+ const leaks = [];
445
454
  const rows = [];
446
455
  const files = new Map();
447
456
  const fontUrls = new Map();
@@ -547,10 +556,7 @@ export async function capturePages(config, options = {}) {
547
556
  ].join("\n"),
548
557
  needles,
549
558
  );
550
- if (found)
551
- throw new Error(
552
- "the page's markup carries a forbidStrings value; not written",
553
- );
559
+ if (found) throw new ForbiddenStringError();
554
560
 
555
561
  // CSS assets on the app origin (other than fonts) are inlined.
556
562
  const inlined = new Map();
@@ -610,6 +616,11 @@ export async function capturePages(config, options = {}) {
610
616
  `captured ${id} (${capture.layout}, slots ${capture.slots.join(",")}, ${Math.round(Buffer.byteLength(html) / 1024)} KB, ${((performance.now() - pageStart) / 1000).toFixed(1)}s)`,
611
617
  );
612
618
  } catch (error) {
619
+ if (error instanceof ForbiddenStringError) {
620
+ leaks.push(id);
621
+ log(`${id}: ${error.message}`);
622
+ continue;
623
+ }
613
624
  const reason = String(error?.message ?? error)
614
625
  .split("\n")
615
626
  .slice(0, 3)
@@ -630,6 +641,12 @@ export async function capturePages(config, options = {}) {
630
641
  fs.rmSync(shotsDir, { recursive: true, force: true });
631
642
  }
632
643
 
644
+ // A leaked value fails the whole capture: no page of this run is written,
645
+ // so a CI job stops before it uploads anything.
646
+ if (leaks.length)
647
+ throw new Error(
648
+ `${leaks.join(", ")} carried a forbidStrings value; nothing was written`,
649
+ );
633
650
  writePagesKit(out, {
634
651
  rows,
635
652
  files,
package/src/cli.mjs CHANGED
@@ -13,6 +13,9 @@ const VERSION = JSON.parse(
13
13
 
14
14
  const USAGE = `usage: design-kit <command> [options]
15
15
 
16
+ init set a kit up in this repository: the config, a daily
17
+ GitHub Actions build, and a skill for its agents
18
+ [--root <dir>] [--dry-run] [--force]
16
19
  capture stories capture every story of a Storybook 7/8 static build
17
20
  [--config <file>] [--out <dir>] [--storybook-dir <dir>]
18
21
  [--only <id,...> | --only-file <file>] [--base <previous kit>]
@@ -27,7 +30,8 @@ const USAGE = `usage: design-kit <command> [options]
27
30
 
28
31
  The config is the nearest design-kit.config.json at or above the working
29
32
  directory unless --config names one. Exit codes: 0 done, 1 a capture could
30
- not run, 2 a config or usage error.`;
33
+ not run (or init found the repository already set up), 2 a config or usage
34
+ error.`;
31
35
 
32
36
  function parse(argv, spec) {
33
37
  const options = {};
@@ -153,6 +157,19 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
153
157
  return 0;
154
158
  }
155
159
  if (command === "kit") return await kitCommand(argv.slice(1), env);
160
+ if (command === "init") {
161
+ const options = parse(argv.slice(1), {
162
+ root: "value",
163
+ "dry-run": "flag",
164
+ force: "flag",
165
+ });
166
+ const { init } = await import("./init.mjs");
167
+ return init({
168
+ root: options.root ?? ".",
169
+ dryRun: Boolean(options["dry-run"]),
170
+ force: Boolean(options.force),
171
+ });
172
+ }
156
173
  if (command === "validate") {
157
174
  const options = parse(argv.slice(1), { config: "value" });
158
175
  const config = loadConfig(options.config);