@codepress/design-kit 0.0.0-stage → 0.1.1
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/LICENSE +11 -0
- package/README.md +228 -2
- package/bin/design-kit.mjs +4 -0
- package/package.json +69 -4
- package/schema/design-kit.config.schema.json +335 -0
- package/src/capture-pages.mjs +725 -0
- package/src/capture-stories.mjs +1226 -0
- package/src/cli.mjs +175 -0
- package/src/config.mjs +276 -0
- package/src/glob.mjs +46 -0
- package/src/index.d.ts +137 -0
- package/src/index.mjs +17 -0
- package/src/kit.d.ts +81 -0
- package/src/kit.mjs +1119 -0
- package/src/pages.mjs +286 -0
- package/src/playwright.mjs +46 -0
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `design-kit` command line: capture a design kit from a Storybook build
|
|
3
|
+
* and a running app, and compose mockups from it. Run `design-kit --help`.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
import { ConfigError, loadConfig } from "./config.mjs";
|
|
9
|
+
|
|
10
|
+
const VERSION = JSON.parse(
|
|
11
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
12
|
+
).version;
|
|
13
|
+
|
|
14
|
+
const USAGE = `usage: design-kit <command> [options]
|
|
15
|
+
|
|
16
|
+
capture stories capture every story of a Storybook 7/8 static build
|
|
17
|
+
[--config <file>] [--out <dir>] [--storybook-dir <dir>]
|
|
18
|
+
[--only <id,...> | --only-file <file>] [--base <previous kit>]
|
|
19
|
+
capture pages capture the configured pages of a running app
|
|
20
|
+
[--config <file>] [--out <dir>] [--base-url <url>] [--only <name,...>]
|
|
21
|
+
[--ready <name>=<selector>] [--screenshots <dir>] [--timeout <ms>]
|
|
22
|
+
kit <info|find|show|compose> ...
|
|
23
|
+
look up and compose from a kit (\`design-kit kit --help\`)
|
|
24
|
+
[--kit <dir>] default: DESIGN_KIT_DIR, the config's "out", ./design-kit
|
|
25
|
+
validate check design-kit.config.json against the v1 schema
|
|
26
|
+
[--config <file>]
|
|
27
|
+
|
|
28
|
+
The config is the nearest design-kit.config.json at or above the working
|
|
29
|
+
directory unless --config names one. Exit codes: 0 done, 1 a capture could
|
|
30
|
+
not run, 2 a config or usage error.`;
|
|
31
|
+
|
|
32
|
+
function parse(argv, spec) {
|
|
33
|
+
const options = {};
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const arg = argv[i];
|
|
36
|
+
if (arg === "--") continue;
|
|
37
|
+
const name = arg.replace(/^--/, "");
|
|
38
|
+
if (!arg.startsWith("--") || !(name in spec)) {
|
|
39
|
+
throw new ConfigError(`unknown option ${arg}\n\n${USAGE}`);
|
|
40
|
+
}
|
|
41
|
+
if (spec[name] === "flag") {
|
|
42
|
+
options[name] = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const value = argv[i + 1];
|
|
46
|
+
if (value === undefined) throw new ConfigError(`${arg} needs a value`);
|
|
47
|
+
i += 1;
|
|
48
|
+
if (spec[name] === "list") (options[name] ??= []).push(value);
|
|
49
|
+
else options[name] = value;
|
|
50
|
+
}
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function captureStories(argv) {
|
|
55
|
+
const options = parse(argv, {
|
|
56
|
+
config: "value",
|
|
57
|
+
out: "value",
|
|
58
|
+
"storybook-dir": "value",
|
|
59
|
+
only: "value",
|
|
60
|
+
"only-file": "value",
|
|
61
|
+
base: "value",
|
|
62
|
+
});
|
|
63
|
+
const config = loadConfig(options.config);
|
|
64
|
+
const { captureStories: run, parseOnly } =
|
|
65
|
+
await import("./capture-stories.mjs");
|
|
66
|
+
const only =
|
|
67
|
+
options.only !== undefined
|
|
68
|
+
? parseOnly(options.only)
|
|
69
|
+
: options["only-file"] !== undefined
|
|
70
|
+
? parseOnly(readFileSync(options["only-file"], "utf8"))
|
|
71
|
+
: undefined;
|
|
72
|
+
await run(config, {
|
|
73
|
+
out: options.out,
|
|
74
|
+
storybookDir: options["storybook-dir"],
|
|
75
|
+
only,
|
|
76
|
+
base: options.base ? resolve(options.base) : undefined,
|
|
77
|
+
});
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function capturePagesCommand(argv) {
|
|
82
|
+
const options = parse(argv, {
|
|
83
|
+
config: "value",
|
|
84
|
+
out: "value",
|
|
85
|
+
"base-url": "value",
|
|
86
|
+
only: "value",
|
|
87
|
+
ready: "list",
|
|
88
|
+
screenshots: "value",
|
|
89
|
+
timeout: "value",
|
|
90
|
+
});
|
|
91
|
+
const config = loadConfig(options.config);
|
|
92
|
+
const ready = {};
|
|
93
|
+
for (const raw of options.ready ?? []) {
|
|
94
|
+
const at = raw.indexOf("=");
|
|
95
|
+
if (at < 1)
|
|
96
|
+
throw new ConfigError(`--ready takes <page>=<selector>, got ${raw}`);
|
|
97
|
+
ready[raw.slice(0, at)] = raw.slice(at + 1);
|
|
98
|
+
}
|
|
99
|
+
const timeoutMs =
|
|
100
|
+
options.timeout === undefined ? undefined : Number(options.timeout);
|
|
101
|
+
if (timeoutMs !== undefined && !(Number.isFinite(timeoutMs) && timeoutMs > 0))
|
|
102
|
+
throw new ConfigError("--timeout takes a positive number of milliseconds");
|
|
103
|
+
const { capturePages } = await import("./capture-pages.mjs");
|
|
104
|
+
const session = { interrupted: false, browser: null };
|
|
105
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
106
|
+
process.on(signal, () => {
|
|
107
|
+
session.interrupted = true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return capturePages(config, {
|
|
111
|
+
out: options.out,
|
|
112
|
+
baseUrl: options["base-url"],
|
|
113
|
+
only: options.only?.split(",").filter(Boolean),
|
|
114
|
+
ready,
|
|
115
|
+
screenshots: options.screenshots ? resolve(options.screenshots) : undefined,
|
|
116
|
+
timeoutMs,
|
|
117
|
+
session,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function kitCommand(argv, env) {
|
|
122
|
+
const { run } = await import("./kit.mjs");
|
|
123
|
+
let defaultDir = resolve("design-kit");
|
|
124
|
+
if (!argv.includes("--kit") && !env.DESIGN_KIT_DIR) {
|
|
125
|
+
try {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
if (config.out) defaultDir = config.out;
|
|
128
|
+
} catch {
|
|
129
|
+
// No usable config: the kit folder comes from --kit or ./design-kit.
|
|
130
|
+
}
|
|
131
|
+
if (!existsSync(defaultDir)) defaultDir = process.cwd();
|
|
132
|
+
}
|
|
133
|
+
return run(argv, undefined, env, defaultDir);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
137
|
+
const [command, sub, ...rest] = argv;
|
|
138
|
+
try {
|
|
139
|
+
if (!command) {
|
|
140
|
+
console.log(USAGE);
|
|
141
|
+
return 2;
|
|
142
|
+
}
|
|
143
|
+
// `kit --help` is the kit tool's own usage; anywhere else, this one.
|
|
144
|
+
if (
|
|
145
|
+
command !== "kit" &&
|
|
146
|
+
argv.some((arg) => arg === "--help" || arg === "-h")
|
|
147
|
+
) {
|
|
148
|
+
console.log(USAGE);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
if (command === "--version") {
|
|
152
|
+
console.log(VERSION);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
if (command === "kit") return await kitCommand(argv.slice(1), env);
|
|
156
|
+
if (command === "validate") {
|
|
157
|
+
const options = parse(argv.slice(1), { config: "value" });
|
|
158
|
+
const config = loadConfig(options.config);
|
|
159
|
+
console.log(`${config.file}: valid (version 1)`);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
if (command === "capture" && sub === "stories")
|
|
163
|
+
return await captureStories(rest);
|
|
164
|
+
if (command === "capture" && sub === "pages")
|
|
165
|
+
return await capturePagesCommand(rest);
|
|
166
|
+
throw new ConfigError(`unknown command "${argv.join(" ")}"\n\n${USAGE}`);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error instanceof ConfigError) {
|
|
169
|
+
console.error(`design-kit: ${error.message}`);
|
|
170
|
+
return 2;
|
|
171
|
+
}
|
|
172
|
+
console.error(`design-kit: ${error?.message ?? error}`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-kit.config.json: finding it, validating it against the v1 schema
|
|
3
|
+
* shipped in this package (schema/design-kit.config.schema.json), and
|
|
4
|
+
* resolving it into the shape the captures use. A config error names the
|
|
5
|
+
* field and exits 2.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
export const CONFIG_NAME = "design-kit.config.json";
|
|
12
|
+
export const SCHEMA_PATH = fileURLToPath(
|
|
13
|
+
new URL("../schema/design-kit.config.schema.json", import.meta.url),
|
|
14
|
+
);
|
|
15
|
+
export const DEFAULT_FIXED_TIME = "2026-01-01T12:00:00.000Z";
|
|
16
|
+
export const DEFAULT_PAGE_WIDTHS = [
|
|
17
|
+
{ width: 1280, height: 800 },
|
|
18
|
+
{ width: 390, height: 844 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/** A problem with the config or the command line: exit 2. */
|
|
22
|
+
export class ConfigError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.exitCode = 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let schemaCache = null;
|
|
30
|
+
export function configSchema() {
|
|
31
|
+
schemaCache ??= JSON.parse(readFileSync(SCHEMA_PATH, "utf8"));
|
|
32
|
+
return schemaCache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── validation (the subset of JSON Schema the shipped schema uses) ──────────
|
|
36
|
+
|
|
37
|
+
function typeOf(value) {
|
|
38
|
+
if (Array.isArray(value)) return "array";
|
|
39
|
+
if (value === null) return "null";
|
|
40
|
+
if (typeof value === "number")
|
|
41
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
42
|
+
return typeof value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fieldName(path) {
|
|
46
|
+
return path.length ? path.join("").replace(/^\./, "") : "(the config)";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every place `value` breaks `schema`, as "<field>: <problem>" strings, in
|
|
51
|
+
* document order.
|
|
52
|
+
*/
|
|
53
|
+
export function validate(value, schema = configSchema(), root = schema) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
const walk = (node, rule, path) => {
|
|
56
|
+
if (rule.$ref) {
|
|
57
|
+
const name = rule.$ref.replace(/^#\/\$defs\//, "");
|
|
58
|
+
walk(node, { ...root.$defs[name], ...rule, $ref: undefined }, path);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const field = fieldName(path);
|
|
62
|
+
const actual = typeOf(node);
|
|
63
|
+
if (rule.type) {
|
|
64
|
+
const ok =
|
|
65
|
+
actual === rule.type ||
|
|
66
|
+
(rule.type === "number" && actual === "integer");
|
|
67
|
+
if (!ok) {
|
|
68
|
+
errors.push(`${field}: must be ${an(rule.type)}, not ${an(actual)}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (rule.enum && !rule.enum.includes(node)) {
|
|
73
|
+
errors.push(
|
|
74
|
+
`${field}: must be ${rule.enum.map((v) => JSON.stringify(v)).join(" or ")}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (actual === "string") {
|
|
78
|
+
if (rule.minLength && node.length < rule.minLength)
|
|
79
|
+
errors.push(`${field}: must not be empty`);
|
|
80
|
+
if (rule.pattern && !new RegExp(rule.pattern).test(node))
|
|
81
|
+
errors.push(`${field}: must match ${rule.pattern}`);
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
(actual === "integer" || actual === "number") &&
|
|
85
|
+
rule.minimum !== undefined
|
|
86
|
+
) {
|
|
87
|
+
if (node < rule.minimum)
|
|
88
|
+
errors.push(`${field}: must be at least ${rule.minimum}`);
|
|
89
|
+
}
|
|
90
|
+
if (actual === "array") {
|
|
91
|
+
if (rule.minItems && node.length < rule.minItems)
|
|
92
|
+
errors.push(
|
|
93
|
+
`${field}: needs at least ${rule.minItems} item${rule.minItems === 1 ? "" : "s"}`,
|
|
94
|
+
);
|
|
95
|
+
if (rule.items)
|
|
96
|
+
node.forEach((item, index) =>
|
|
97
|
+
walk(item, rule.items, [...path, `[${index}]`]),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (actual === "object") {
|
|
101
|
+
for (const key of rule.required ?? []) {
|
|
102
|
+
if (!(key in node))
|
|
103
|
+
errors.push(`${fieldName([...path, `.${key}`])}: is required`);
|
|
104
|
+
}
|
|
105
|
+
for (const [key, child] of Object.entries(node)) {
|
|
106
|
+
const childPath = [...path, `.${key}`];
|
|
107
|
+
if (rule.properties && key in rule.properties) {
|
|
108
|
+
walk(child, rule.properties[key], childPath);
|
|
109
|
+
} else if (rule.additionalProperties === false) {
|
|
110
|
+
errors.push(`${fieldName(childPath)}: unknown key`);
|
|
111
|
+
} else if (
|
|
112
|
+
rule.additionalProperties &&
|
|
113
|
+
typeof rule.additionalProperties === "object"
|
|
114
|
+
) {
|
|
115
|
+
walk(child, rule.additionalProperties, childPath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
walk(value, schema, []);
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function an(type) {
|
|
125
|
+
return /^[aeiou]/.test(type) ? `an ${type}` : `a ${type}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── loading ─────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** The nearest design-kit.config.json at or above `from`, or null. */
|
|
131
|
+
export function findConfig(from = process.cwd()) {
|
|
132
|
+
let dir = resolve(from);
|
|
133
|
+
for (;;) {
|
|
134
|
+
const candidate = join(dir, CONFIG_NAME);
|
|
135
|
+
if (existsSync(candidate)) return candidate;
|
|
136
|
+
const parent = dirname(dir);
|
|
137
|
+
if (parent === dir) return null;
|
|
138
|
+
dir = parent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reads, validates and resolves a config. `file` defaults to the nearest
|
|
144
|
+
* design-kit.config.json above the working directory.
|
|
145
|
+
*/
|
|
146
|
+
export function loadConfig(file) {
|
|
147
|
+
const path = file ? resolve(file) : findConfig();
|
|
148
|
+
if (!path) {
|
|
149
|
+
throw new ConfigError(
|
|
150
|
+
`no ${CONFIG_NAME} here or in any folder above; pass --config <file>`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!existsSync(path)) throw new ConfigError(`${path}: no such file`);
|
|
154
|
+
let raw;
|
|
155
|
+
try {
|
|
156
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new ConfigError(`${path}: not valid JSON (${error.message})`);
|
|
159
|
+
}
|
|
160
|
+
return resolveConfig(raw, path);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Validates a parsed config and resolves its paths against `path`'s folder. */
|
|
164
|
+
export function resolveConfig(raw, path) {
|
|
165
|
+
const errors = validate(raw);
|
|
166
|
+
if (!errors.length && !raw.stories && !raw.pages) {
|
|
167
|
+
errors.push("(the config): needs a stories or a pages section");
|
|
168
|
+
}
|
|
169
|
+
raw.stories?.widths?.forEach((width, index) => {
|
|
170
|
+
try {
|
|
171
|
+
new RegExp(width.titles);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
errors.push(
|
|
174
|
+
`stories.widths[${index}].titles: not a regular expression (${error.message})`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
for (const [key, section] of [
|
|
179
|
+
["stories", raw.stories],
|
|
180
|
+
["pages", raw.pages],
|
|
181
|
+
]) {
|
|
182
|
+
if (section?.fixedTime && Number.isNaN(Date.parse(section.fixedTime)))
|
|
183
|
+
errors.push(`${key}.fixedTime: not an ISO 8601 date`);
|
|
184
|
+
}
|
|
185
|
+
if (raw.pages?.auth?.storageState && raw.pages?.auth?.setup) {
|
|
186
|
+
errors.push("pages.auth: give storageState or setup, not both");
|
|
187
|
+
}
|
|
188
|
+
if (raw.pages?.startCommand && !raw.pages?.waitUrl) {
|
|
189
|
+
errors.push("pages.waitUrl: is required with pages.startCommand");
|
|
190
|
+
}
|
|
191
|
+
const names = new Set();
|
|
192
|
+
raw.pages?.pages?.forEach((page, index) => {
|
|
193
|
+
if (names.has(page?.name))
|
|
194
|
+
errors.push(`pages.pages[${index}].name: "${page.name}" is used twice`);
|
|
195
|
+
names.add(page?.name);
|
|
196
|
+
});
|
|
197
|
+
if (errors.length) {
|
|
198
|
+
throw new ConfigError(
|
|
199
|
+
`${path}:\n${errors.map((error) => ` ${error}`).join("\n")}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const configDir = dirname(path);
|
|
204
|
+
const at = (value) =>
|
|
205
|
+
value === undefined ? undefined : resolve(configDir, value);
|
|
206
|
+
const config = {
|
|
207
|
+
file: path,
|
|
208
|
+
configDir,
|
|
209
|
+
raw,
|
|
210
|
+
out: at(raw.out),
|
|
211
|
+
theme: raw.theme ? normalizeTheme(raw.theme) : undefined,
|
|
212
|
+
};
|
|
213
|
+
if (raw.stories) {
|
|
214
|
+
const s = raw.stories;
|
|
215
|
+
const root = at(s.root ?? ".");
|
|
216
|
+
config.stories = {
|
|
217
|
+
root,
|
|
218
|
+
storybookDir: at(s.storybookDir),
|
|
219
|
+
buildCommand: s.buildCommand,
|
|
220
|
+
staticDir: s.staticDir ? resolve(root, s.staticDir) : undefined,
|
|
221
|
+
viewport: s.viewport ?? { width: 1280, height: 800 },
|
|
222
|
+
widths: (s.widths ?? []).map((w) => ({
|
|
223
|
+
titles: new RegExp(w.titles),
|
|
224
|
+
width: w.width,
|
|
225
|
+
height: w.height,
|
|
226
|
+
})),
|
|
227
|
+
fonts: s.fonts ? { dir: at(s.fonts.dir), faces: s.fonts.faces } : null,
|
|
228
|
+
fixedTime: s.fixedTime ?? DEFAULT_FIXED_TIME,
|
|
229
|
+
gaps: s.gaps
|
|
230
|
+
? {
|
|
231
|
+
sourceRoot: s.gaps.sourceRoot ? at(s.gaps.sourceRoot) : root,
|
|
232
|
+
componentGlob: s.gaps.componentGlob,
|
|
233
|
+
}
|
|
234
|
+
: null,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (raw.pages) {
|
|
238
|
+
const p = raw.pages;
|
|
239
|
+
config.pages = {
|
|
240
|
+
baseUrl: p.baseUrl,
|
|
241
|
+
startCommand: p.startCommand,
|
|
242
|
+
waitUrl: p.waitUrl,
|
|
243
|
+
cwd: at(p.cwd) ?? configDir,
|
|
244
|
+
startTimeoutMs: p.startTimeoutMs ?? 120_000,
|
|
245
|
+
auth: {
|
|
246
|
+
storageState: at(p.auth?.storageState),
|
|
247
|
+
setup: at(p.auth?.setup),
|
|
248
|
+
},
|
|
249
|
+
hooks: at(p.hooks),
|
|
250
|
+
widths: p.widths ?? DEFAULT_PAGE_WIDTHS,
|
|
251
|
+
fixedTime: p.fixedTime ?? DEFAULT_FIXED_TIME,
|
|
252
|
+
shellReady: p.shellReady,
|
|
253
|
+
layout: p.layout
|
|
254
|
+
? { ...p.layout, default: p.layout.default ?? "default" }
|
|
255
|
+
: null,
|
|
256
|
+
fontsPath: p.fontsPath ?? null,
|
|
257
|
+
copyFonts: p.copyFonts ?? true,
|
|
258
|
+
timeoutMs: p.timeoutMs ?? 30_000,
|
|
259
|
+
forbidStrings: p.forbidStrings ?? [],
|
|
260
|
+
pages: p.pages,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return config;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The theme as the manifest records it, with its defaults filled in. */
|
|
267
|
+
export function normalizeTheme(theme) {
|
|
268
|
+
const attribute = theme.attribute ?? "class";
|
|
269
|
+
return {
|
|
270
|
+
attribute,
|
|
271
|
+
dark: theme.dark ?? "dark",
|
|
272
|
+
...(theme.light ? { light: theme.light } : {}),
|
|
273
|
+
targets:
|
|
274
|
+
theme.targets ?? (attribute === "class" ? ["html", "body"] : ["html"]),
|
|
275
|
+
};
|
|
276
|
+
}
|
package/src/glob.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small glob matcher for config paths: `**` (any number of folders), `*`
|
|
3
|
+
* and `?` (within one name), `[A-Z]` classes and `{a,b}` alternatives.
|
|
4
|
+
* Paths use `/`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @param {string} glob */
|
|
8
|
+
export function globToRegExp(glob) {
|
|
9
|
+
let out = "";
|
|
10
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
11
|
+
const ch = glob[i];
|
|
12
|
+
if (ch === "*") {
|
|
13
|
+
if (glob[i + 1] === "*") {
|
|
14
|
+
const slash = glob[i + 2] === "/";
|
|
15
|
+
out += slash ? "(?:[^/]*/)*" : ".*";
|
|
16
|
+
i += slash ? 2 : 1;
|
|
17
|
+
} else out += "[^/]*";
|
|
18
|
+
} else if (ch === "?") out += "[^/]";
|
|
19
|
+
else if (ch === "[") {
|
|
20
|
+
const close = glob.indexOf("]", i + 1);
|
|
21
|
+
if (close < 0) out += "\\[";
|
|
22
|
+
else {
|
|
23
|
+
const body = glob.slice(i + 1, close).replace(/\\/g, "\\\\");
|
|
24
|
+
out += `[${body.startsWith("!") ? `^${body.slice(1)}` : body}]`;
|
|
25
|
+
i = close;
|
|
26
|
+
}
|
|
27
|
+
} else if (ch === "{") {
|
|
28
|
+
const close = glob.indexOf("}", i + 1);
|
|
29
|
+
if (close < 0) out += "\\{";
|
|
30
|
+
else {
|
|
31
|
+
out += `(?:${glob
|
|
32
|
+
.slice(i + 1, close)
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((part) => globToRegExp(part).source.slice(1, -1))
|
|
35
|
+
.join("|")})`;
|
|
36
|
+
i = close;
|
|
37
|
+
}
|
|
38
|
+
} else out += ch.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
39
|
+
}
|
|
40
|
+
return new RegExp(`^${out}$`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {RegExp} pattern @param {string} path */
|
|
44
|
+
export function matchFile(pattern, path) {
|
|
45
|
+
return pattern.test(path.replace(/\\/g, "/"));
|
|
46
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Types for @codepress/design-kit's programmatic API. */
|
|
2
|
+
|
|
3
|
+
export const CONFIG_NAME: "design-kit.config.json";
|
|
4
|
+
export const SCHEMA_PATH: string;
|
|
5
|
+
|
|
6
|
+
export class ConfigError extends Error {
|
|
7
|
+
exitCode: 2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Size {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PageConfig {
|
|
16
|
+
name: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
path: string;
|
|
19
|
+
ready: string;
|
|
20
|
+
sourceFile?: string;
|
|
21
|
+
slots: { main: string[]; [slot: string]: string[] };
|
|
22
|
+
layoutSlots?: Record<string, { main: string[]; [slot: string]: string[] }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A loaded, validated config with its paths resolved. */
|
|
26
|
+
export interface DesignKitConfig {
|
|
27
|
+
file: string;
|
|
28
|
+
configDir: string;
|
|
29
|
+
raw: Record<string, unknown>;
|
|
30
|
+
out?: string;
|
|
31
|
+
theme?: {
|
|
32
|
+
attribute: string;
|
|
33
|
+
dark: string;
|
|
34
|
+
light?: string;
|
|
35
|
+
targets: ("html" | "body")[];
|
|
36
|
+
};
|
|
37
|
+
stories?: Record<string, unknown>;
|
|
38
|
+
pages?: {
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
fixedTime: string;
|
|
41
|
+
widths: Size[];
|
|
42
|
+
pages: PageConfig[];
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function findConfig(from?: string): string | null;
|
|
48
|
+
export function loadConfig(file?: string): DesignKitConfig;
|
|
49
|
+
export function resolveConfig(raw: unknown, path: string): DesignKitConfig;
|
|
50
|
+
/** "<field>: <problem>" for every place `value` breaks the v1 schema. */
|
|
51
|
+
export function validate(value: unknown): string[];
|
|
52
|
+
|
|
53
|
+
export function playwrightVersion(from?: string): string;
|
|
54
|
+
|
|
55
|
+
export function captureStories(
|
|
56
|
+
config: DesignKitConfig,
|
|
57
|
+
options?: {
|
|
58
|
+
out?: string;
|
|
59
|
+
storybookDir?: string;
|
|
60
|
+
only?: string[];
|
|
61
|
+
base?: string;
|
|
62
|
+
log?: (message: string) => void;
|
|
63
|
+
},
|
|
64
|
+
): Promise<{ out: string; rows: number; failed: number }>;
|
|
65
|
+
|
|
66
|
+
/** Returned by a `page` hook; both run inside the capture of one page. */
|
|
67
|
+
export interface PageHookResult {
|
|
68
|
+
/** The ready selector never appeared; may throw a clearer error. */
|
|
69
|
+
readyFailed?(): void | Promise<void>;
|
|
70
|
+
/** Runs just before serialization, in place of the optional screenshot. */
|
|
71
|
+
beforeSerialize?(info: { shotPath: string }): void | Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CaptureHookInfo {
|
|
75
|
+
page: PageConfig;
|
|
76
|
+
viewport: Size;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CapturePagesOptions {
|
|
80
|
+
out?: string;
|
|
81
|
+
only?: string[];
|
|
82
|
+
ready?: Record<string, string>;
|
|
83
|
+
screenshots?: string;
|
|
84
|
+
timeoutMs?: number;
|
|
85
|
+
baseUrl?: string;
|
|
86
|
+
/** A storageState file path or a Playwright storage state object. */
|
|
87
|
+
storageState?: string | object;
|
|
88
|
+
forbidStrings?: string[];
|
|
89
|
+
stripOrigins?: string[];
|
|
90
|
+
hooks?: {
|
|
91
|
+
/** Runs on each fresh context; may return a cleanup. */
|
|
92
|
+
context?(
|
|
93
|
+
context: unknown,
|
|
94
|
+
info: CaptureHookInfo,
|
|
95
|
+
): void | (() => Promise<void>) | Promise<void | (() => Promise<void>)>;
|
|
96
|
+
/** Runs on each page before it navigates. */
|
|
97
|
+
page?(
|
|
98
|
+
page: unknown,
|
|
99
|
+
info: CaptureHookInfo,
|
|
100
|
+
): void | PageHookResult | Promise<void | PageHookResult>;
|
|
101
|
+
};
|
|
102
|
+
/** The Playwright to drive; default the project's own. */
|
|
103
|
+
playwright?: { chromium: unknown };
|
|
104
|
+
log?: (message: string) => void;
|
|
105
|
+
/** Set `interrupted` to stop between pages; `browser` is set once launched. */
|
|
106
|
+
session?: { interrupted: boolean; browser: unknown };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 0, or 130 when `session.interrupted` stopped the run (nothing written). */
|
|
110
|
+
export function capturePages(
|
|
111
|
+
config: DesignKitConfig,
|
|
112
|
+
options?: CapturePagesOptions,
|
|
113
|
+
): Promise<number>;
|
|
114
|
+
|
|
115
|
+
/** What to look for per forbidden value: as written, a token's long segments, and escaped. */
|
|
116
|
+
export function forbiddenNeedles(values: string[]): string[];
|
|
117
|
+
/** The first needle `text` contains, or null. */
|
|
118
|
+
export function findForbidden(text: string, needles: string[]): string | null;
|
|
119
|
+
|
|
120
|
+
export function loadKit(dir: string): unknown;
|
|
121
|
+
export function find(kit: unknown, query: string): unknown[];
|
|
122
|
+
export function compose(
|
|
123
|
+
kit: unknown,
|
|
124
|
+
options: {
|
|
125
|
+
frame: string;
|
|
126
|
+
slots?: { name: string; content: string }[];
|
|
127
|
+
theme?: "light" | "dark";
|
|
128
|
+
showNew?: boolean;
|
|
129
|
+
},
|
|
130
|
+
): string;
|
|
131
|
+
/** Runs one `kit` command; returns its exit code. */
|
|
132
|
+
export function runKit(
|
|
133
|
+
argv: string[],
|
|
134
|
+
io?: { out(text: string): void; err(text: string): void },
|
|
135
|
+
env?: Record<string, string | undefined>,
|
|
136
|
+
defaultDir?: string,
|
|
137
|
+
): number;
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codepress/design-kit's programmatic API. The command line (`design-kit`)
|
|
3
|
+
* is a thin layer over these.
|
|
4
|
+
*/
|
|
5
|
+
export {
|
|
6
|
+
CONFIG_NAME,
|
|
7
|
+
ConfigError,
|
|
8
|
+
SCHEMA_PATH,
|
|
9
|
+
findConfig,
|
|
10
|
+
loadConfig,
|
|
11
|
+
resolveConfig,
|
|
12
|
+
validate,
|
|
13
|
+
} from "./config.mjs";
|
|
14
|
+
export { captureStories, playwrightVersion } from "./capture-stories.mjs";
|
|
15
|
+
export { capturePages } from "./capture-pages.mjs";
|
|
16
|
+
export { findForbidden, forbiddenNeedles } from "./pages.mjs";
|
|
17
|
+
export { compose, find, loadKit, run as runKit } from "./kit.mjs";
|