@bridge-ds/cli 0.9.0 → 0.11.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/dist/index.js +1359 -119
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
5
5
|
import { readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
|
|
6
|
-
import { isAbsolute, join as
|
|
6
|
+
import { isAbsolute, join as join8, relative as relative5, resolve as resolve6 } from "node:path";
|
|
7
7
|
|
|
8
8
|
// ../core/src/audit/types.ts
|
|
9
9
|
var AUDIT_SCHEMA_VERSION = 1;
|
|
@@ -1798,8 +1798,8 @@ function scanMarkup(path, text, lang, ds) {
|
|
|
1798
1798
|
out.themeRefs += r.themeRefs;
|
|
1799
1799
|
}
|
|
1800
1800
|
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
1801
|
-
const { line, column } = pos(offset);
|
|
1802
|
-
const src =
|
|
1801
|
+
const { line: line2, column } = pos(offset);
|
|
1802
|
+
const src = line2 >= 2 ? `${"\n".repeat(line2 - 2)}${prefix}
|
|
1803
1803
|
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
1804
1804
|
const c = scanCode(`${path}.tsx`, src, ds);
|
|
1805
1805
|
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
@@ -2445,7 +2445,7 @@ var DEFAULT_GOVERNANCE = {
|
|
|
2445
2445
|
owners: { design: [], tech: [] }
|
|
2446
2446
|
};
|
|
2447
2447
|
var FLAT_TOKEN_KEYS = ["primitives", "brands", "semantic", "base", "density", "components", "contrastChecks"];
|
|
2448
|
-
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers"];
|
|
2448
|
+
var CANONICAL_TOKEN_KEYS = ["root", "include", "exclude", "tiers", "layers", "usage"];
|
|
2449
2449
|
var FLAT_MODE_KEYS = ["brands", "themes", "densities", "defaults"];
|
|
2450
2450
|
var AXIS_OF = { brands: "brand", themes: "theme", densities: "density" };
|
|
2451
2451
|
var ConfigError = class extends Error {
|
|
@@ -2533,7 +2533,7 @@ function defineConfig(input) {
|
|
|
2533
2533
|
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
2534
2534
|
return {
|
|
2535
2535
|
repo: parseRepoInput(input.repo, input.branch),
|
|
2536
|
-
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers },
|
|
2536
|
+
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
|
|
2537
2537
|
axes,
|
|
2538
2538
|
modes,
|
|
2539
2539
|
contrast: { path: contrastFile, modes: input.contrast?.modes },
|
|
@@ -2543,7 +2543,8 @@ function defineConfig(input) {
|
|
|
2543
2543
|
styleSuffix: input.components.styleSuffix ?? ".module.css",
|
|
2544
2544
|
reference: input.components.reference,
|
|
2545
2545
|
index: input.components.index,
|
|
2546
|
-
specs: input.components.specs
|
|
2546
|
+
specs: input.components.specs,
|
|
2547
|
+
...input.components.styles ? { styles: input.components.styles.map(trimSlashes) } : {}
|
|
2547
2548
|
} : void 0,
|
|
2548
2549
|
docs: { ...input.docs },
|
|
2549
2550
|
changesets: pkg ? { dir: trimSlashes(input.changesets?.dir ?? ".changeset"), package: pkg } : void 0,
|
|
@@ -2568,6 +2569,7 @@ function defineConfig(input) {
|
|
|
2568
2569
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
2569
2570
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
2570
2571
|
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
2572
|
+
...input.library?.public === true ? { library: { public: true } } : {},
|
|
2571
2573
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
2572
2574
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
2573
2575
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -2612,7 +2614,7 @@ function loadConfig(source) {
|
|
|
2612
2614
|
if (!isObj(tokens2)) problems.push('"tokens" must be an object');
|
|
2613
2615
|
else
|
|
2614
2616
|
for (const [k, v] of Object.entries(tokens2)) {
|
|
2615
|
-
if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
|
|
2617
|
+
if (FLAT_TOKEN_KEYS.includes(k) || k === "root" || k === "usage") {
|
|
2616
2618
|
if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
|
|
2617
2619
|
} else if (k === "include" || k === "exclude") {
|
|
2618
2620
|
if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
|
|
@@ -2666,6 +2668,7 @@ function loadConfig(source) {
|
|
|
2666
2668
|
problems.push('"contrast" must be { "path", "modes"? }');
|
|
2667
2669
|
const comps = d.components;
|
|
2668
2670
|
if (comps !== void 0 && (!isObj(comps) || !isStr(comps.dir))) problems.push('"components.dir" must be a non-empty string');
|
|
2671
|
+
if (isObj(comps) && comps.styles !== void 0 && !isStrList(comps.styles)) problems.push('"components.styles" must be a list of globs');
|
|
2669
2672
|
const cs = d.changeset;
|
|
2670
2673
|
if (cs !== void 0 && (!isObj(cs) || !isStr(cs.package))) problems.push('"changeset.package" must be a non-empty string');
|
|
2671
2674
|
const css = d.changesets;
|
|
@@ -2698,6 +2701,14 @@ function loadConfig(source) {
|
|
|
2698
2701
|
else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
|
|
2699
2702
|
else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
|
|
2700
2703
|
}
|
|
2704
|
+
const lib = d.library;
|
|
2705
|
+
if (lib !== void 0) {
|
|
2706
|
+
if (!isObj(lib)) problems.push('"library" must be an object, for example { "public": true }');
|
|
2707
|
+
else {
|
|
2708
|
+
if (lib.public !== void 0 && typeof lib.public !== "boolean") problems.push('"library.public" must be true or false');
|
|
2709
|
+
for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2701
2712
|
problems.push(...productProblems(d.products));
|
|
2702
2713
|
const audit2 = d.audit;
|
|
2703
2714
|
if (audit2 !== void 0) {
|
|
@@ -2727,6 +2738,7 @@ function loadConfig(source) {
|
|
|
2727
2738
|
if (problems.length) throw new ConfigError(problems);
|
|
2728
2739
|
return defineConfig(d);
|
|
2729
2740
|
}
|
|
2741
|
+
var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
2730
2742
|
|
|
2731
2743
|
// ../core/src/node/git-local.ts
|
|
2732
2744
|
import { execFile } from "node:child_process";
|
|
@@ -2752,10 +2764,10 @@ function assertSafeBranch(branch, rules = {}) {
|
|
|
2752
2764
|
|
|
2753
2765
|
// ../core/src/node/git-local.ts
|
|
2754
2766
|
function git(root, args, opts = {}) {
|
|
2755
|
-
return new Promise((
|
|
2767
|
+
return new Promise((resolve7, reject) => {
|
|
2756
2768
|
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
2757
2769
|
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
2758
|
-
else
|
|
2770
|
+
else resolve7(stdout);
|
|
2759
2771
|
});
|
|
2760
2772
|
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
2761
2773
|
});
|
|
@@ -3034,6 +3046,44 @@ function classifyAuth(status, detail, opts = {}) {
|
|
|
3034
3046
|
}
|
|
3035
3047
|
return {};
|
|
3036
3048
|
}
|
|
3049
|
+
function memoryEtagStore(max = 500) {
|
|
3050
|
+
const map = /* @__PURE__ */ new Map();
|
|
3051
|
+
return {
|
|
3052
|
+
get(key) {
|
|
3053
|
+
const e = map.get(key);
|
|
3054
|
+
if (e) {
|
|
3055
|
+
map.delete(key);
|
|
3056
|
+
map.set(key, e);
|
|
3057
|
+
}
|
|
3058
|
+
return e;
|
|
3059
|
+
},
|
|
3060
|
+
set(key, entry) {
|
|
3061
|
+
map.delete(key);
|
|
3062
|
+
map.set(key, entry);
|
|
3063
|
+
while (map.size > max) map.delete(map.keys().next().value);
|
|
3064
|
+
},
|
|
3065
|
+
clear: () => map.clear()
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
var sharedEtags = memoryEtagStore();
|
|
3069
|
+
function tokenFingerprint(token) {
|
|
3070
|
+
if (!token) return "anon";
|
|
3071
|
+
let a = 2166136261;
|
|
3072
|
+
let b = 16777619 ^ token.length;
|
|
3073
|
+
for (let i = 0; i < token.length; i++) {
|
|
3074
|
+
const c = token.charCodeAt(i);
|
|
3075
|
+
a = Math.imul(a ^ c, 16777619) >>> 0;
|
|
3076
|
+
b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
|
|
3077
|
+
}
|
|
3078
|
+
return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
|
|
3079
|
+
}
|
|
3080
|
+
function installationFor(list2, owner) {
|
|
3081
|
+
const inst = list2.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
|
|
3082
|
+
if (!inst) return { reach: "no" };
|
|
3083
|
+
const selection = inst.repositorySelection ?? inst.repository_selection;
|
|
3084
|
+
return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
|
|
3085
|
+
}
|
|
3086
|
+
var MAX_PAGES = 10;
|
|
3037
3087
|
function rateLimitMessage(x) {
|
|
3038
3088
|
const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
|
|
3039
3089
|
if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
|
|
@@ -3041,8 +3091,11 @@ function rateLimitMessage(x) {
|
|
|
3041
3091
|
return `GitHub's hourly limit for requests without a token (60 an hour) is used up.${when} To keep going now, ${hint}: a token gets 5,000 requests an hour.`;
|
|
3042
3092
|
}
|
|
3043
3093
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
3094
|
+
var hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
|
|
3044
3095
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
3045
3096
|
var API = "https://api.github.com";
|
|
3097
|
+
var ACCEPT_JSON = "application/vnd.github+json";
|
|
3098
|
+
var ACCEPT_RAW = "application/vnd.github.raw";
|
|
3046
3099
|
var RAW = "https://raw.githubusercontent.com";
|
|
3047
3100
|
function parseRepo(repo) {
|
|
3048
3101
|
const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
|
|
@@ -3061,6 +3114,7 @@ function base64ToUtf8(b64) {
|
|
|
3061
3114
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
3062
3115
|
return new TextDecoder("utf-8").decode(bytes);
|
|
3063
3116
|
}
|
|
3117
|
+
var copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
3064
3118
|
var encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
|
|
3065
3119
|
var GitHubClient = class _GitHubClient {
|
|
3066
3120
|
constructor(repo, token, fetchImpl = (url, init2) => fetch(url, init2), retry = DEFAULT_RETRY, options = {}) {
|
|
@@ -3073,6 +3127,8 @@ var GitHubClient = class _GitHubClient {
|
|
|
3073
3127
|
this.name = r.name;
|
|
3074
3128
|
this.api = (options.apiUrl ?? API).replace(/\/$/, "");
|
|
3075
3129
|
this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
3130
|
+
this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
|
|
3131
|
+
this.etagScope = options.etagScope ?? tokenFingerprint(token);
|
|
3076
3132
|
}
|
|
3077
3133
|
token;
|
|
3078
3134
|
fetchImpl;
|
|
@@ -3083,6 +3139,15 @@ var GitHubClient = class _GitHubClient {
|
|
|
3083
3139
|
blobCache = /* @__PURE__ */ new Map();
|
|
3084
3140
|
api;
|
|
3085
3141
|
protectedBranches;
|
|
3142
|
+
etags;
|
|
3143
|
+
etagScope;
|
|
3144
|
+
rate;
|
|
3145
|
+
/** What the conditional request cache did for this client. */
|
|
3146
|
+
etagStats = { conditional: 0, notModified: 0, stale: 0 };
|
|
3147
|
+
/** GitHub's rate-limit headers from the last API response, or undefined before any response carried them. */
|
|
3148
|
+
get rateLimit() {
|
|
3149
|
+
return this.rate;
|
|
3150
|
+
}
|
|
3086
3151
|
/** "owner/name". */
|
|
3087
3152
|
get fullName() {
|
|
3088
3153
|
return `${this.owner}/${this.name}`;
|
|
@@ -3106,45 +3171,77 @@ var GitHubClient = class _GitHubClient {
|
|
|
3106
3171
|
get repoPath() {
|
|
3107
3172
|
return `/repos/${this.owner}/${this.name}`;
|
|
3108
3173
|
}
|
|
3109
|
-
async request(method, path, body, retryable = method === "GET") {
|
|
3174
|
+
async request(method, path, body, retryable = method === "GET", opts = {}) {
|
|
3110
3175
|
const attempts = retryable ? Math.max(1, this.retry.attempts) : 1;
|
|
3111
3176
|
for (let attempt = 1; ; attempt++) {
|
|
3112
3177
|
try {
|
|
3113
|
-
return await this.once(method, path, body);
|
|
3178
|
+
return await this.once(method, path, body, opts);
|
|
3114
3179
|
} catch (err) {
|
|
3115
3180
|
const e = err;
|
|
3116
|
-
if (attempt >= attempts || !(e instanceof GitHubError) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
3181
|
+
if (attempt >= attempts || !(e instanceof GitHubError) || hourlyLimitErrors.has(e) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
3117
3182
|
await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
|
|
3118
3183
|
}
|
|
3119
3184
|
}
|
|
3120
3185
|
}
|
|
3121
|
-
|
|
3186
|
+
noteRateLimit(headers) {
|
|
3187
|
+
const remaining = headers?.get("x-ratelimit-remaining");
|
|
3188
|
+
if (remaining == null) return;
|
|
3189
|
+
const limit = headers?.get("x-ratelimit-limit");
|
|
3190
|
+
const reset = headers?.get("x-ratelimit-reset");
|
|
3191
|
+
this.rate = {
|
|
3192
|
+
limit: limit != null ? Number(limit) : void 0,
|
|
3193
|
+
remaining: Number(remaining),
|
|
3194
|
+
resetsAt: reset ? new Date(Number(reset) * 1e3) : void 0,
|
|
3195
|
+
resource: headers?.get("x-ratelimit-resource") ?? void 0
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
async once(method, path, body, opts = {}) {
|
|
3199
|
+
const accept = opts.accept ?? ACCEPT_JSON;
|
|
3122
3200
|
const headers = {
|
|
3123
|
-
Accept:
|
|
3201
|
+
Accept: accept,
|
|
3124
3202
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
3125
3203
|
};
|
|
3126
3204
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
3127
3205
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
3206
|
+
const key = method === "GET" && this.etags && opts.etag !== false ? `${this.etagScope} ${accept} ${this.api}${path}` : void 0;
|
|
3207
|
+
const cached = key ? this.etags.get(key) : void 0;
|
|
3208
|
+
if (cached && this.token) {
|
|
3209
|
+
headers["If-None-Match"] = cached.etag;
|
|
3210
|
+
this.etagStats.conditional++;
|
|
3211
|
+
}
|
|
3128
3212
|
let res;
|
|
3129
3213
|
try {
|
|
3130
3214
|
res = await this.fetchImpl(`${this.api}${path}`, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
|
|
3131
3215
|
} catch (err) {
|
|
3216
|
+
if (err instanceof GitHubError) throw err;
|
|
3132
3217
|
throw new GitHubError(0, "Could not reach GitHub. Check your connection, then try again.", err.message);
|
|
3133
3218
|
}
|
|
3219
|
+
this.noteRateLimit(res.headers);
|
|
3220
|
+
if (res.status === 304 && cached && key && this.token) {
|
|
3221
|
+
this.etagStats.notModified++;
|
|
3222
|
+
this.etags.set(key, { ...cached, at: Date.now() });
|
|
3223
|
+
return copy(cached.body);
|
|
3224
|
+
}
|
|
3134
3225
|
if (res.status === 204) return void 0;
|
|
3135
3226
|
if (!res.ok) {
|
|
3136
3227
|
let detail = "";
|
|
3137
3228
|
try {
|
|
3138
|
-
const
|
|
3139
|
-
detail = [
|
|
3229
|
+
const data2 = await res.json();
|
|
3230
|
+
detail = [data2.message, ...(data2.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
|
|
3140
3231
|
} catch {
|
|
3141
3232
|
detail = "";
|
|
3142
3233
|
}
|
|
3143
3234
|
const remaining = res.headers?.get("x-ratelimit-remaining");
|
|
3144
3235
|
const reset = res.headers?.get("x-ratelimit-reset");
|
|
3145
3236
|
if ((res.status === 403 || res.status === 429) && remaining === "0") {
|
|
3237
|
+
if (cached && this.options.serveStaleOnRateLimit) {
|
|
3238
|
+
this.etagStats.stale++;
|
|
3239
|
+
return copy(cached.body);
|
|
3240
|
+
}
|
|
3146
3241
|
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
3147
|
-
|
|
3242
|
+
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
3243
|
+
hourlyLimitErrors.add(limited);
|
|
3244
|
+
throw limited;
|
|
3148
3245
|
}
|
|
3149
3246
|
const info = classifyAuth(res.status, detail, {
|
|
3150
3247
|
path,
|
|
@@ -3153,7 +3250,24 @@ var GitHubClient = class _GitHubClient {
|
|
|
3153
3250
|
});
|
|
3154
3251
|
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
3155
3252
|
}
|
|
3156
|
-
|
|
3253
|
+
const data = opts.text ? await res.text() : await res.json();
|
|
3254
|
+
const etag = key ? res.headers?.get("etag") : null;
|
|
3255
|
+
if (key && etag) this.etags.set(key, { etag, body: copy(data), at: Date.now() });
|
|
3256
|
+
return data;
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Every item of a paged listing (`per_page=100`), at most 10 pages. `pick` takes the items out of one page (the
|
|
3260
|
+
* installation endpoints wrap them in an object with `total_count`).
|
|
3261
|
+
*/
|
|
3262
|
+
async paged(path, pick) {
|
|
3263
|
+
const out = [];
|
|
3264
|
+
const sep3 = path.includes("?") ? "&" : "?";
|
|
3265
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
3266
|
+
const { items, total } = pick(await this.request("GET", `${path}${sep3}per_page=100&page=${page}`));
|
|
3267
|
+
out.push(...items);
|
|
3268
|
+
if (items.length < 100 || total !== void 0 && out.length >= total) break;
|
|
3269
|
+
}
|
|
3270
|
+
return out;
|
|
3157
3271
|
}
|
|
3158
3272
|
async getRepo() {
|
|
3159
3273
|
const r = await this.request(
|
|
@@ -3536,6 +3650,14 @@ var GitHubClient = class _GitHubClient {
|
|
|
3536
3650
|
*/
|
|
3537
3651
|
async readFile(path, ref) {
|
|
3538
3652
|
if (this.token) return this.getFileText(path, ref);
|
|
3653
|
+
return this.readRawFile(path, ref);
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Text of a file from the raw host (raw.githubusercontent.com), sent without any header so the browser makes a
|
|
3657
|
+
* simple CORS request (no preflight) and nothing counts against the API limit. Works for public repositories only:
|
|
3658
|
+
* a private file answers 404 there, like a missing one. Undefined on 404.
|
|
3659
|
+
*/
|
|
3660
|
+
async readRawFile(path, ref) {
|
|
3539
3661
|
const url = `${(this.options.rawUrl ?? RAW).replace(/\/$/, "")}/${this.owner}/${this.name}/${encodeRef(ref)}/${encodeRef(path)}`;
|
|
3540
3662
|
let res;
|
|
3541
3663
|
try {
|
|
@@ -3547,6 +3669,76 @@ var GitHubClient = class _GitHubClient {
|
|
|
3547
3669
|
if (!res.ok) throw new GitHubError(res.status, friendlyMessage(res.status, ""), `raw ${path}@${ref}`);
|
|
3548
3670
|
return res.text();
|
|
3549
3671
|
}
|
|
3672
|
+
/**
|
|
3673
|
+
* Text of a file at a ref through the contents API with the raw media type: the way to read a private file (the raw
|
|
3674
|
+
* host cannot take the token without a preflight). Counts against the API limit; conditional when cached.
|
|
3675
|
+
* Undefined when the file does not exist.
|
|
3676
|
+
*/
|
|
3677
|
+
async getRawText(path, ref) {
|
|
3678
|
+
try {
|
|
3679
|
+
return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}?ref=${encodeURIComponent(ref)}`, void 0, true, { accept: ACCEPT_RAW, text: true });
|
|
3680
|
+
} catch (err) {
|
|
3681
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
3682
|
+
throw err;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* Text of a blob by its sha through the git blobs API with the raw media type: how a signed-in reader loads a file
|
|
3687
|
+
* listed by a tree (the raw host refuses any request that carries a token). A blob sha names its content, so the
|
|
3688
|
+
* answer is cached for the client's life and never revalidated. Undefined on 404.
|
|
3689
|
+
*/
|
|
3690
|
+
async getBlobRaw(sha) {
|
|
3691
|
+
const cached = this.blobCache.get(sha);
|
|
3692
|
+
if (cached !== void 0) return cached;
|
|
3693
|
+
try {
|
|
3694
|
+
const text = await this.request("GET", `${this.repoPath}/git/blobs/${encodeURIComponent(sha)}`, void 0, true, { accept: ACCEPT_RAW, text: true, etag: false });
|
|
3695
|
+
this.blobCache.set(sha, text);
|
|
3696
|
+
return text;
|
|
3697
|
+
} catch (err) {
|
|
3698
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
3699
|
+
throw err;
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
// --- GitHub App installations (user-to-server tokens) ---------------------------------------
|
|
3703
|
+
/**
|
|
3704
|
+
* Installations of the signing-in GitHub App that the signed-in user can reach (GET /user/installations, every
|
|
3705
|
+
* page). Needs a user-to-server token of that same App: a personal or OAuth token is refused (403), and each App
|
|
3706
|
+
* sees only its own installations. GitHub lists only installations on accounts and repositories the user has
|
|
3707
|
+
* explicit permission on: a public repository where the user is not a collaborator is never listed, installed or
|
|
3708
|
+
* not. (GET /repos/{o}/{r}/installation would say, but it needs the App's private key, which Bridge never holds.)
|
|
3709
|
+
*/
|
|
3710
|
+
async listUserInstallations() {
|
|
3711
|
+
this.assertToken("Reading where Bridge is installed");
|
|
3712
|
+
const list2 = await this.paged("/user/installations", (p) => ({ items: p.installations ?? [], total: p.total_count }));
|
|
3713
|
+
return list2.map((i) => ({
|
|
3714
|
+
id: i.id,
|
|
3715
|
+
account: i.account?.login ? { login: i.account.login, type: i.account.type ?? "User" } : null,
|
|
3716
|
+
repositorySelection: i.repository_selection ?? "selected",
|
|
3717
|
+
appSlug: i.app_slug
|
|
3718
|
+
}));
|
|
3719
|
+
}
|
|
3720
|
+
/** Repositories of one installation the signed-in user can reach (GET /user/installations/{id}/repositories, every page). */
|
|
3721
|
+
async listInstallationRepos(installationId) {
|
|
3722
|
+
this.assertToken("Reading where Bridge is installed");
|
|
3723
|
+
const list2 = await this.paged(`/user/installations/${encodeURIComponent(String(installationId))}/repositories`, (p) => ({
|
|
3724
|
+
items: p.repositories ?? [],
|
|
3725
|
+
total: p.total_count
|
|
3726
|
+
}));
|
|
3727
|
+
return list2.map((r) => ({ owner: r.owner?.login ?? r.full_name.split("/")[0], name: r.name, fullName: r.full_name, private: Boolean(r.private) }));
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* The App installation that gives the signed-in user this repository (this client's by default): its id, or null
|
|
3731
|
+
* when the App is not installed there or the user cannot reach it. One call when the installation covers all of
|
|
3732
|
+
* the owner's repositories, two (paged) when it covers selected ones.
|
|
3733
|
+
*/
|
|
3734
|
+
async findInstallation(owner = this.owner, repo = this.name) {
|
|
3735
|
+
const found = installationFor(await this.listUserInstallations(), owner);
|
|
3736
|
+
if (found.reach === "no") return null;
|
|
3737
|
+
if (found.reach === "yes") return { id: found.id, selection: "all" };
|
|
3738
|
+
const repos = await this.listInstallationRepos(found.id);
|
|
3739
|
+
const hit = repos.some((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.name.toLowerCase() === repo.toLowerCase());
|
|
3740
|
+
return hit ? { id: found.id, selection: "selected" } : null;
|
|
3741
|
+
}
|
|
3550
3742
|
/** The repository's default branch. */
|
|
3551
3743
|
async getDefaultBranch() {
|
|
3552
3744
|
return (await this.getRepo()).defaultBranch;
|
|
@@ -3924,14 +4116,101 @@ var SUGGESTION_APPLY_EXIT = {
|
|
|
3924
4116
|
refused: 1
|
|
3925
4117
|
};
|
|
3926
4118
|
|
|
4119
|
+
// ../core/src/node/fs-source.ts
|
|
4120
|
+
import { execFileSync } from "node:child_process";
|
|
4121
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
4122
|
+
import { join as join2 } from "node:path";
|
|
4123
|
+
|
|
4124
|
+
// ../core/src/repo-source.ts
|
|
4125
|
+
function normalizeRepoPath(path) {
|
|
4126
|
+
return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
|
|
4127
|
+
}
|
|
4128
|
+
var parentDir = (path) => {
|
|
4129
|
+
const p = normalizeRepoPath(path);
|
|
4130
|
+
const i = p.lastIndexOf("/");
|
|
4131
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
4132
|
+
};
|
|
4133
|
+
async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, err) => {
|
|
4134
|
+
throw err;
|
|
4135
|
+
}, onProgress) {
|
|
4136
|
+
const unique = [...new Set(paths)];
|
|
4137
|
+
const out = /* @__PURE__ */ new Map();
|
|
4138
|
+
onProgress?.(0, unique.length);
|
|
4139
|
+
let next = 0;
|
|
4140
|
+
const worker = async () => {
|
|
4141
|
+
while (next < unique.length) {
|
|
4142
|
+
const path = unique[next++];
|
|
4143
|
+
let body;
|
|
4144
|
+
try {
|
|
4145
|
+
body = await source.text(path);
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
body = onError(path, err);
|
|
4148
|
+
}
|
|
4149
|
+
out.set(path, body);
|
|
4150
|
+
onProgress?.(out.size, unique.length);
|
|
4151
|
+
}
|
|
4152
|
+
};
|
|
4153
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, unique.length)) }, worker));
|
|
4154
|
+
return out;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// ../core/src/node/fs-source.ts
|
|
4158
|
+
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
4159
|
+
function gitOut(root, args) {
|
|
4160
|
+
try {
|
|
4161
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4162
|
+
} catch {
|
|
4163
|
+
return "";
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
function localRepoRef(root) {
|
|
4167
|
+
const origin = /github\.com[:/]([\w.-]+)\/([\w.-]+?)(\.git)?$/.exec(gitOut(root, ["remote", "get-url", "origin"]));
|
|
4168
|
+
return { owner: origin?.[1] ?? "", repo: origin?.[2] ?? "", sha: gitOut(root, ["rev-parse", "HEAD"]) || "working-tree" };
|
|
4169
|
+
}
|
|
4170
|
+
function fsSource(root, ref = {}) {
|
|
4171
|
+
let resolved;
|
|
4172
|
+
const walk2 = (rel, out) => {
|
|
4173
|
+
for (const entry of readdirSync2(join2(root, rel), { withFileTypes: true })) {
|
|
4174
|
+
if (SKIP.has(entry.name)) continue;
|
|
4175
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4176
|
+
if (entry.isDirectory()) walk2(path, out);
|
|
4177
|
+
else if (entry.isFile()) out.push(path);
|
|
4178
|
+
else if (entry.isSymbolicLink() && statSync2(join2(root, path), { throwIfNoEntry: false })?.isFile()) out.push(path);
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
return {
|
|
4182
|
+
get ref() {
|
|
4183
|
+
resolved ??= { ...localRepoRef(root), ...Object.fromEntries(Object.entries(ref).filter(([, v]) => v !== void 0)) };
|
|
4184
|
+
return resolved;
|
|
4185
|
+
},
|
|
4186
|
+
async list(prefix = "") {
|
|
4187
|
+
const rel = normalizeRepoPath(prefix);
|
|
4188
|
+
const abs = join2(root, rel);
|
|
4189
|
+
if (!existsSync2(abs)) return [];
|
|
4190
|
+
if (!statSync2(abs).isDirectory()) return [rel];
|
|
4191
|
+
const out = [];
|
|
4192
|
+
walk2(rel, out);
|
|
4193
|
+
return out.sort();
|
|
4194
|
+
},
|
|
4195
|
+
async text(path) {
|
|
4196
|
+
const abs = join2(root, normalizeRepoPath(path));
|
|
4197
|
+
try {
|
|
4198
|
+
return statSync2(abs).isFile() ? readFileSync2(abs, "utf8") : null;
|
|
4199
|
+
} catch {
|
|
4200
|
+
return null;
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
|
|
3927
4206
|
// src/index.ts
|
|
3928
4207
|
import { fileURLToPath } from "node:url";
|
|
3929
4208
|
|
|
3930
4209
|
// ../visual/src/run.ts
|
|
3931
|
-
import { existsSync as
|
|
4210
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
3932
4211
|
import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
3933
4212
|
import { tmpdir } from "node:os";
|
|
3934
|
-
import { join as
|
|
4213
|
+
import { join as join5, relative as relative3, resolve as resolve2 } from "node:path";
|
|
3935
4214
|
|
|
3936
4215
|
// ../core/src/protection.ts
|
|
3937
4216
|
var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
|
|
@@ -4328,6 +4607,858 @@ function storybookIframeUrl(base, id, o = {}) {
|
|
|
4328
4607
|
return url;
|
|
4329
4608
|
}
|
|
4330
4609
|
|
|
4610
|
+
// ../core/src/token-rows.ts
|
|
4611
|
+
var FALLBACK_BRANDS = ["bridge", "ocean"];
|
|
4612
|
+
var FALLBACK_THEMES = ["light", "dark"];
|
|
4613
|
+
var FALLBACK_DENSITIES = ["comfortable", "compact"];
|
|
4614
|
+
function tokenRowAxes(config, readText = () => void 0) {
|
|
4615
|
+
const brandAxis = config.axes.brand;
|
|
4616
|
+
const brands = brandAxis?.values.length ? [...brandAxis.values] : [...FALLBACK_BRANDS];
|
|
4617
|
+
const defaultBrand = brandAxis?.default ?? brands[0];
|
|
4618
|
+
const themes = config.modes.themes.length ? [...config.modes.themes] : [...FALLBACK_THEMES];
|
|
4619
|
+
const densities = config.modes.densities.length ? [...config.modes.densities] : [...FALLBACK_DENSITIES];
|
|
4620
|
+
const files = tokenRowFiles(config);
|
|
4621
|
+
const brandName = (b) => {
|
|
4622
|
+
try {
|
|
4623
|
+
const text = readText(files.brandFile(b));
|
|
4624
|
+
const name = text ? JSON.parse(text).$extensions?.["dev.bridge.brand"]?.name : void 0;
|
|
4625
|
+
if (typeof name === "string" && name) return name;
|
|
4626
|
+
} catch {
|
|
4627
|
+
}
|
|
4628
|
+
return b.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4629
|
+
};
|
|
4630
|
+
return {
|
|
4631
|
+
brands,
|
|
4632
|
+
defaultBrand,
|
|
4633
|
+
brandLabels: Object.fromEntries(brands.map((b) => [b, brandName(b)])),
|
|
4634
|
+
themes,
|
|
4635
|
+
densities,
|
|
4636
|
+
defaultDensity: config.modes.defaults.density ?? densities[0]
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
function tokenRowFiles(config) {
|
|
4640
|
+
const t = config.tokens;
|
|
4641
|
+
const at = (rel) => tokensPath(config, rel);
|
|
4642
|
+
return {
|
|
4643
|
+
primitivesDir: at(t.primitives),
|
|
4644
|
+
semanticDir: at(t.semantic),
|
|
4645
|
+
brandsDir: at(t.brands),
|
|
4646
|
+
densityDir: at(t.density),
|
|
4647
|
+
componentsDir: at(t.components),
|
|
4648
|
+
baseFile: at(`${t.semantic}/${t.base}.tokens.json`),
|
|
4649
|
+
semanticFile: (theme) => at(`${t.semantic}/${theme}.tokens.json`),
|
|
4650
|
+
brandFile: (brand2) => at(`${t.brands}/${brand2}.tokens.json`),
|
|
4651
|
+
overrideFile: (brand2, theme) => at(`${t.brands}/${brand2}.${theme}.tokens.json`),
|
|
4652
|
+
densityFile: (density) => at(`${t.density}/${density}.tokens.json`)
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
function flattenTokenFile(node, file, path = [], inheritedType = "") {
|
|
4656
|
+
const type = node.$type ?? inheritedType;
|
|
4657
|
+
if ("$value" in node) {
|
|
4658
|
+
return [
|
|
4659
|
+
{
|
|
4660
|
+
path,
|
|
4661
|
+
type,
|
|
4662
|
+
value: node.$value,
|
|
4663
|
+
description: node.$description,
|
|
4664
|
+
deprecated: node.$deprecated,
|
|
4665
|
+
file
|
|
4666
|
+
}
|
|
4667
|
+
];
|
|
4668
|
+
}
|
|
4669
|
+
const out = [];
|
|
4670
|
+
for (const [key, child] of Object.entries(node)) {
|
|
4671
|
+
if (key.startsWith("$") || typeof child !== "object" || child === null) continue;
|
|
4672
|
+
out.push(...flattenTokenFile(child, file, [...path, key], type));
|
|
4673
|
+
}
|
|
4674
|
+
return out;
|
|
4675
|
+
}
|
|
4676
|
+
var toHex22 = (n) => Math.round(n * 255).toString(16).padStart(2, "0");
|
|
4677
|
+
function colorCss(c) {
|
|
4678
|
+
if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
|
|
4679
|
+
const v = c;
|
|
4680
|
+
const [r = 0, g = 0, b = 0] = v.components;
|
|
4681
|
+
const hex = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
|
|
4682
|
+
const alpha = v.alpha ?? 1;
|
|
4683
|
+
if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
|
|
4684
|
+
return { css: hex, hex };
|
|
4685
|
+
}
|
|
4686
|
+
var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
|
|
4687
|
+
function shadowCss(s) {
|
|
4688
|
+
if (typeof s === "string") return s;
|
|
4689
|
+
const layers = Array.isArray(s) ? s : [s];
|
|
4690
|
+
return layers.map((l) => [l.inset ? "inset" : "", dimCss(l.offsetX), dimCss(l.offsetY), dimCss(l.blur), dimCss(l.spread), colorCss(l.color).css].filter(Boolean).join(" ")).join(", ");
|
|
4691
|
+
}
|
|
4692
|
+
function tokenValueCss(type, value) {
|
|
4693
|
+
switch (type) {
|
|
4694
|
+
case "color":
|
|
4695
|
+
return colorCss(value);
|
|
4696
|
+
case "dimension":
|
|
4697
|
+
case "duration":
|
|
4698
|
+
return { css: dimCss(value) };
|
|
4699
|
+
case "shadow":
|
|
4700
|
+
return { css: shadowCss(value) };
|
|
4701
|
+
case "cubicBezier":
|
|
4702
|
+
return { css: Array.isArray(value) ? `cubic-bezier(${value.join(", ")})` : String(value) };
|
|
4703
|
+
case "fontFamily":
|
|
4704
|
+
return { css: Array.isArray(value) ? value.map((n) => /\s/.test(n) ? `"${n}"` : n).join(", ") : String(value) };
|
|
4705
|
+
default:
|
|
4706
|
+
return { css: String(value) };
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
var tokenRowCssVar = (segments) => `--${segments.join("-")}`;
|
|
4710
|
+
var tokenRowFigmaName = (segments) => segments.join("/");
|
|
4711
|
+
var isAlias2 = (v) => typeof v === "string" && /^\{[^}]+\}$/.test(v);
|
|
4712
|
+
function buildTokenRows(config, readText, listFiles) {
|
|
4713
|
+
const axes = tokenRowAxes(config, readText);
|
|
4714
|
+
const { brands: BRANDS2, defaultBrand: DEFAULT_BRAND, themes: MODES, densities: DENSITIES, defaultDensity: DEFAULT_DENSITY } = axes;
|
|
4715
|
+
const files = tokenRowFiles(config);
|
|
4716
|
+
const REF_THEME = MODES[0];
|
|
4717
|
+
const jsonFiles = (dir) => listFiles(dir).filter((f) => f.endsWith(".tokens.json")).sort();
|
|
4718
|
+
const flatCache = /* @__PURE__ */ new Map();
|
|
4719
|
+
const flatFile = (f) => {
|
|
4720
|
+
if (!flatCache.has(f)) {
|
|
4721
|
+
const text = readText(f);
|
|
4722
|
+
flatCache.set(f, text === void 0 ? [] : flattenTokenFile(JSON.parse(text), f));
|
|
4723
|
+
}
|
|
4724
|
+
return flatCache.get(f);
|
|
4725
|
+
};
|
|
4726
|
+
const byPathOf = (tokens2) => new Map(tokens2.map((t) => [t.path.join("."), t]));
|
|
4727
|
+
const primitiveFiles = jsonFiles(files.primitivesDir);
|
|
4728
|
+
const componentFiles = jsonFiles(files.componentsDir);
|
|
4729
|
+
const sources = [
|
|
4730
|
+
...primitiveFiles,
|
|
4731
|
+
...jsonFiles(files.brandsDir),
|
|
4732
|
+
...MODES.map(files.semanticFile),
|
|
4733
|
+
files.baseFile,
|
|
4734
|
+
...jsonFiles(files.densityDir),
|
|
4735
|
+
...componentFiles
|
|
4736
|
+
];
|
|
4737
|
+
const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
|
|
4738
|
+
const { css, hex } = tokenValueCss(t.type, t.value);
|
|
4739
|
+
return {
|
|
4740
|
+
path: t.path.join("."),
|
|
4741
|
+
segments: t.path,
|
|
4742
|
+
type: t.type,
|
|
4743
|
+
css,
|
|
4744
|
+
hex,
|
|
4745
|
+
cssVar: tokenRowCssVar(t.path),
|
|
4746
|
+
figma: tokenRowFigmaName(t.path),
|
|
4747
|
+
description: t.description,
|
|
4748
|
+
deprecated: t.deprecated,
|
|
4749
|
+
raw: t.value,
|
|
4750
|
+
file: t.file
|
|
4751
|
+
};
|
|
4752
|
+
});
|
|
4753
|
+
const primByPath = new Map(primitives.map((p) => [p.path, p]));
|
|
4754
|
+
const brandRaw = Object.fromEntries(BRANDS2.map((b) => [b, flatFile(files.brandFile(b))]));
|
|
4755
|
+
const themeRaw = Object.fromEntries(MODES.map((m) => [m, flatFile(files.semanticFile(m))]));
|
|
4756
|
+
const baseRaw = flatFile(files.baseFile);
|
|
4757
|
+
const densityRaw = Object.fromEntries(DENSITIES.map((d) => [d, flatFile(files.densityFile(d))]));
|
|
4758
|
+
const componentRaw = componentFiles.flatMap((f) => flatFile(f));
|
|
4759
|
+
const layerCache = /* @__PURE__ */ new Map();
|
|
4760
|
+
const layers = (b, m, d) => {
|
|
4761
|
+
const k = `${b}.${m}.${d}`;
|
|
4762
|
+
if (!layerCache.has(k))
|
|
4763
|
+
layerCache.set(k, [
|
|
4764
|
+
byPathOf(flatFile(files.overrideFile(b, m))),
|
|
4765
|
+
byPathOf(themeRaw[m] ?? []),
|
|
4766
|
+
byPathOf(baseRaw),
|
|
4767
|
+
byPathOf(densityRaw[d] ?? []),
|
|
4768
|
+
byPathOf(componentRaw),
|
|
4769
|
+
byPathOf(brandRaw[b] ?? [])
|
|
4770
|
+
]);
|
|
4771
|
+
return layerCache.get(k);
|
|
4772
|
+
};
|
|
4773
|
+
const resolve7 = (b, m, d, value, type) => {
|
|
4774
|
+
const chain = [];
|
|
4775
|
+
let current = value;
|
|
4776
|
+
let currentType = type;
|
|
4777
|
+
let guard = 0;
|
|
4778
|
+
while (isAlias2(current) && guard++ < 30) {
|
|
4779
|
+
const target = current.slice(1, -1);
|
|
4780
|
+
chain.push(target);
|
|
4781
|
+
const prim = primByPath.get(target);
|
|
4782
|
+
if (prim) return { alias: chain[0] ?? null, chain, css: prim.css, hex: prim.hex };
|
|
4783
|
+
const hit = layers(b, m, d).map((l) => l.get(target)).find(Boolean);
|
|
4784
|
+
if (!hit) throw new Error(`tokens: alias {${target}} does not resolve in ${b} ${m} ${d}`);
|
|
4785
|
+
current = hit.value;
|
|
4786
|
+
currentType = hit.type || currentType;
|
|
4787
|
+
}
|
|
4788
|
+
const { css, hex } = tokenValueCss(currentType, current);
|
|
4789
|
+
return { alias: chain[0] ?? null, chain, css, hex };
|
|
4790
|
+
};
|
|
4791
|
+
const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
|
|
4792
|
+
const build = (t, tier, valueFor) => {
|
|
4793
|
+
const at = (b, m, d = DEFAULT_DENSITY) => {
|
|
4794
|
+
const raw = valueFor(b, m, d) ?? t;
|
|
4795
|
+
return resolve7(b, m, d, raw.value, raw.type || t.type);
|
|
4796
|
+
};
|
|
4797
|
+
const brands = Object.fromEntries(BRANDS2.map((b) => [b, Object.fromEntries(MODES.map((m) => [m, at(b, m)]))]));
|
|
4798
|
+
return {
|
|
4799
|
+
path: t.path.join("."),
|
|
4800
|
+
segments: t.path,
|
|
4801
|
+
type: t.type,
|
|
4802
|
+
cssVar: tokenRowCssVar(t.path),
|
|
4803
|
+
figma: tokenRowFigmaName(t.path),
|
|
4804
|
+
description: t.description,
|
|
4805
|
+
group: t.path[0] === "color" ? t.path.slice(0, 2).join(".") : t.path[0],
|
|
4806
|
+
deprecated: t.deprecated,
|
|
4807
|
+
tier,
|
|
4808
|
+
file: t.file,
|
|
4809
|
+
modes: brands[DEFAULT_BRAND],
|
|
4810
|
+
brands,
|
|
4811
|
+
densities: tier === "density" ? Object.fromEntries(DENSITIES.map((d) => [d, at(DEFAULT_BRAND, REF_THEME, d)])) : void 0,
|
|
4812
|
+
brandSpecific: BRANDS2.some((b) => MODES.some((m) => !same2(brands[b][m], brands[DEFAULT_BRAND][m])))
|
|
4813
|
+
};
|
|
4814
|
+
};
|
|
4815
|
+
const brand2 = (brandRaw[DEFAULT_BRAND] ?? []).map((t) => build(t, "brand", (b) => byPathOf(brandRaw[b] ?? []).get(t.path.join("."))));
|
|
4816
|
+
const themedBy = Object.fromEntries(MODES.map((m) => [m, byPathOf(themeRaw[m] ?? [])]));
|
|
4817
|
+
const themed = (themeRaw[REF_THEME] ?? []).map((t) => {
|
|
4818
|
+
const path = t.path.join(".");
|
|
4819
|
+
const others = MODES.slice(1).map((m) => {
|
|
4820
|
+
const hit = themedBy[m].get(path);
|
|
4821
|
+
if (!hit) throw new Error(`tokens: semantic token ${path} is missing in ${files.semanticFile(m)}`);
|
|
4822
|
+
return hit;
|
|
4823
|
+
});
|
|
4824
|
+
const s = build(t, "semantic", (b, m) => byPathOf(flatFile(files.overrideFile(b, m))).get(path) ?? (m === REF_THEME ? t : themedBy[m].get(path)));
|
|
4825
|
+
s.description = [t, ...others].map((x) => x.description).find((x) => x !== void 0);
|
|
4826
|
+
s.deprecated = [t, ...others].map((x) => x.deprecated).find((x) => x !== void 0);
|
|
4827
|
+
s.file = MODES.map(files.semanticFile).join(", ");
|
|
4828
|
+
return s;
|
|
4829
|
+
});
|
|
4830
|
+
const base = baseRaw.map((t) => build(t, "semantic", () => t));
|
|
4831
|
+
const semantic = [...themed, ...base];
|
|
4832
|
+
const density = (densityRaw[DEFAULT_DENSITY] ?? []).map((t) => build(t, "density", (_b, _m, d) => byPathOf(densityRaw[d] ?? []).get(t.path.join("."))));
|
|
4833
|
+
const component = componentRaw.map((t) => build(t, "component", () => t));
|
|
4834
|
+
return { ...axes, primitives, semantic, brand: brand2, density, component, all: [...brand2, ...semantic, ...density, ...component], sources };
|
|
4835
|
+
}
|
|
4836
|
+
function tokenRowIndex(rows) {
|
|
4837
|
+
const map = /* @__PURE__ */ new Map();
|
|
4838
|
+
for (const p of rows.primitives) map.set(p.cssVar, { path: p.path, kind: "primitive", type: p.type });
|
|
4839
|
+
for (const s of rows.all) map.set(s.cssVar, { path: s.path, kind: s.tier, type: s.type });
|
|
4840
|
+
return map;
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
// ../core/src/contrast-model.ts
|
|
4844
|
+
var CONTRAST_THRESHOLDS = { text: { aa: 4.5, aaa: 7 }, ui: { aa: 3, aaa: null } };
|
|
4845
|
+
var channel = (c) => {
|
|
4846
|
+
const s = c / 255;
|
|
4847
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
4848
|
+
};
|
|
4849
|
+
function hexLuminance(hex) {
|
|
4850
|
+
const n = parseInt(hex.slice(1), 16);
|
|
4851
|
+
return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
|
|
4852
|
+
}
|
|
4853
|
+
function hexContrastRatio(a, b) {
|
|
4854
|
+
const [hi, lo] = [hexLuminance(a), hexLuminance(b)].sort((x, y) => y - x);
|
|
4855
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
4856
|
+
}
|
|
4857
|
+
function parseContrastSpecs(text) {
|
|
4858
|
+
if (!text) return [];
|
|
4859
|
+
const data = JSON.parse(text);
|
|
4860
|
+
return (data.pairs ?? []).map((p) => {
|
|
4861
|
+
const [fg, bg, min] = Array.isArray(p) ? p : [p.fg, p.bg, p.min];
|
|
4862
|
+
return { fg, bg, min, usage: min >= CONTRAST_THRESHOLDS.text.aa ? "text" : "ui" };
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
function evaluateContrastPairs(specs, rows) {
|
|
4866
|
+
const byPath = new Map(rows.all.map((s) => [s.path, s]));
|
|
4867
|
+
return specs.map((spec) => {
|
|
4868
|
+
const fgToken = byPath.get(spec.fg);
|
|
4869
|
+
const bgToken = byPath.get(spec.bg);
|
|
4870
|
+
if (!fgToken || !bgToken) throw new Error(`contrast: pair ${spec.fg} on ${spec.bg} references a token that does not exist`);
|
|
4871
|
+
const t = CONTRAST_THRESHOLDS[spec.usage];
|
|
4872
|
+
const result = (b, m) => {
|
|
4873
|
+
const fgHex = fgToken.brands[b][m].hex;
|
|
4874
|
+
const bgHex = bgToken.brands[b][m].hex;
|
|
4875
|
+
const r = hexContrastRatio(fgHex, bgHex);
|
|
4876
|
+
return { fgHex, bgHex, ratio: Math.floor(r * 100) / 100, aa: r >= spec.min, aaa: t.aaa === null ? null : r >= t.aaa };
|
|
4877
|
+
};
|
|
4878
|
+
const brands = Object.fromEntries(rows.brands.map((b) => [b, Object.fromEntries(rows.themes.map((m) => [m, result(b, m)]))]));
|
|
4879
|
+
return { ...spec, fgToken, bgToken, modes: brands[rows.defaultBrand], brands };
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// ../core/src/component-scan.ts
|
|
4884
|
+
var componentSlug = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
4885
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4886
|
+
function parseComponentFamilies(indexSrc, componentsRel = "components") {
|
|
4887
|
+
const out = [];
|
|
4888
|
+
const exportRe = new RegExp(`export \\* from '\\./${escapeRe(componentsRel)}/(\\w+)'`);
|
|
4889
|
+
for (const line2 of indexSrc.split("\n")) {
|
|
4890
|
+
const head = line2.match(/^\/\/\s*([A-Z][\w ]+)$/);
|
|
4891
|
+
if (head) out.push({ name: head[1].trim(), members: [] });
|
|
4892
|
+
const exp = line2.match(exportRe);
|
|
4893
|
+
if (exp && out.length) out[out.length - 1].members.push(exp[1]);
|
|
4894
|
+
}
|
|
4895
|
+
return out.filter((f) => f.members.length);
|
|
4896
|
+
}
|
|
4897
|
+
var NATIVE = {
|
|
4898
|
+
HTMLButtonElement: "button",
|
|
4899
|
+
HTMLInputElement: "input",
|
|
4900
|
+
HTMLSpanElement: "span",
|
|
4901
|
+
HTMLDivElement: "div",
|
|
4902
|
+
HTMLAnchorElement: "a"
|
|
4903
|
+
};
|
|
4904
|
+
function parseExtends(clause) {
|
|
4905
|
+
const el = clause.match(/<(HTML\w+Element)>/)?.[1];
|
|
4906
|
+
const omitted = [...(clause.match(/Omit<[^,]+,\s*([^>]+)>/)?.[1] ?? "").matchAll(/'([^']+)'/g)].map((m) => m[1]);
|
|
4907
|
+
return { inherits: el ? NATIVE[el] ?? el : void 0, omitted };
|
|
4908
|
+
}
|
|
4909
|
+
function parseComponentTsx(name, src) {
|
|
4910
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
4911
|
+
for (const m of src.matchAll(/export type (\w+)\s*=\s*([^;]+);/g)) aliases.set(m[1], m[2].trim());
|
|
4912
|
+
let props = [];
|
|
4913
|
+
let inherits;
|
|
4914
|
+
let omitted = [];
|
|
4915
|
+
const iface = src.match(new RegExp(`export interface ${name}Props(?:\\s+extends\\s+([^{]+))?\\s*\\{([\\s\\S]*?)\\n\\}`));
|
|
4916
|
+
if (iface) {
|
|
4917
|
+
({ inherits, omitted } = parseExtends(iface[1] ?? ""));
|
|
4918
|
+
for (const m of iface[2].matchAll(/(?:\/\*\*([\s\S]*?)\*\/\s*)?(\w+)(\?)?:\s*([^;]+);/g)) {
|
|
4919
|
+
const rawType = m[4].trim();
|
|
4920
|
+
props.push({
|
|
4921
|
+
name: m[2],
|
|
4922
|
+
type: aliases.get(rawType) ?? rawType,
|
|
4923
|
+
required: !m[3],
|
|
4924
|
+
description: m[1]?.replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim() || void 0
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
} else {
|
|
4928
|
+
const alias = aliases.get(`${name}Props`);
|
|
4929
|
+
if (alias) ({ inherits, omitted } = parseExtends(alias));
|
|
4930
|
+
}
|
|
4931
|
+
const sig = src.match(new RegExp(`function ${name}\\(\\s*\\{([^}]*)\\}`));
|
|
4932
|
+
if (sig) {
|
|
4933
|
+
for (const m of sig[1].matchAll(/(\w+)\s*=\s*('[^']*'|[\w.]+)/g)) {
|
|
4934
|
+
const p = props.find((x) => x.name === m[1]);
|
|
4935
|
+
if (p) p.defaultValue = m[2];
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
const docBefore = (re) => {
|
|
4939
|
+
const idx = src.search(re);
|
|
4940
|
+
if (idx < 0) return void 0;
|
|
4941
|
+
const before = src.slice(0, idx).trimEnd();
|
|
4942
|
+
if (!before.endsWith("*/")) return void 0;
|
|
4943
|
+
const start = before.lastIndexOf("/*", before.length - 2);
|
|
4944
|
+
if (start < 0 || before[start + 2] !== "*") return void 0;
|
|
4945
|
+
return before.slice(start + 3, -2).replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim();
|
|
4946
|
+
};
|
|
4947
|
+
const description = docBefore(new RegExp(`export (?:function|const) ${name}\\b`));
|
|
4948
|
+
const subcomponents = [...src.matchAll(/export function (\w+)/g)].map((m) => m[1]).filter((n) => n !== name);
|
|
4949
|
+
props = props.sort((a, b) => Number(b.required) - Number(a.required));
|
|
4950
|
+
return { props, inherits, omitted, description, subcomponents };
|
|
4951
|
+
}
|
|
4952
|
+
var stateOf = (selector) => {
|
|
4953
|
+
const states = [
|
|
4954
|
+
[/:focus-visible/, "focus"],
|
|
4955
|
+
[/:hover/, "hover"],
|
|
4956
|
+
[/:disabled(?!,)|^[^(]*:disabled/, "disabled"],
|
|
4957
|
+
[/\[data-loading\]/, "loading"],
|
|
4958
|
+
[/::placeholder/, "placeholder"]
|
|
4959
|
+
];
|
|
4960
|
+
const hit = states.filter(([re]) => re.test(selector)).map(([, s]) => s);
|
|
4961
|
+
return hit.length ? `, ${hit.join(" + ")}` : "";
|
|
4962
|
+
};
|
|
4963
|
+
var BASE_UI_RUNTIME = /^--(_|accordion-panel-|collapsible-panel-|active-tab-|anchor-|available-|drawer-swipe-|toast-(frontmost-height|height|index|offset-y|swipe-)|transform-origin)/;
|
|
4964
|
+
function parseComponentCss(src, labels, describe) {
|
|
4965
|
+
const clean = src.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4966
|
+
const rules = [];
|
|
4967
|
+
const walk2 = (text, prefix = "") => {
|
|
4968
|
+
let i = 0;
|
|
4969
|
+
let buf = "";
|
|
4970
|
+
while (i < text.length) {
|
|
4971
|
+
const ch = text[i];
|
|
4972
|
+
if (ch === "{") {
|
|
4973
|
+
const head = buf.trim();
|
|
4974
|
+
buf = "";
|
|
4975
|
+
let depth = 1;
|
|
4976
|
+
const start = ++i;
|
|
4977
|
+
while (i < text.length && depth > 0) {
|
|
4978
|
+
if (text[i] === "{") depth++;
|
|
4979
|
+
else if (text[i] === "}") depth--;
|
|
4980
|
+
i++;
|
|
4981
|
+
}
|
|
4982
|
+
const inner = text.slice(start, i - 1);
|
|
4983
|
+
if (head.startsWith("@keyframes")) continue;
|
|
4984
|
+
if (head.startsWith("@")) {
|
|
4985
|
+
walk2(inner, `${head} `);
|
|
4986
|
+
continue;
|
|
4987
|
+
}
|
|
4988
|
+
const names = [...new Set([...inner.matchAll(/var\((--[\w-]+)/g)].map((m) => m[1]))].filter((n) => !BASE_UI_RUNTIME.test(n));
|
|
4989
|
+
if (!names.length) continue;
|
|
4990
|
+
const className = head.match(/\.([\w-]+)/)?.[1] ?? head;
|
|
4991
|
+
rules.push({
|
|
4992
|
+
selector: prefix + head.replace(/\s+/g, " "),
|
|
4993
|
+
className,
|
|
4994
|
+
label: (labels[className] ?? className) + stateOf(head),
|
|
4995
|
+
vars: names.map((n) => {
|
|
4996
|
+
const d = describe(n);
|
|
4997
|
+
return { name: n, href: d?.href, known: !!d, path: d?.path, kind: d?.kind };
|
|
4998
|
+
})
|
|
4999
|
+
});
|
|
5000
|
+
} else {
|
|
5001
|
+
buf += ch;
|
|
5002
|
+
i++;
|
|
5003
|
+
}
|
|
5004
|
+
}
|
|
5005
|
+
};
|
|
5006
|
+
walk2(clean);
|
|
5007
|
+
return rules;
|
|
5008
|
+
}
|
|
5009
|
+
function parseComponentStories(src) {
|
|
5010
|
+
const title = src.match(/title:\s*'([^']+)'/)?.[1];
|
|
5011
|
+
if (!title) return [];
|
|
5012
|
+
const base = title.split("/").map((s) => componentSlug(s.trim()).replace(/\s+/g, "-")).join("-");
|
|
5013
|
+
return [...src.matchAll(/export const (\w+):\s*Story/g)].map((m) => ({ name: m[1], id: `${base}--${componentSlug(m[1])}` }));
|
|
5014
|
+
}
|
|
5015
|
+
function resolveInheritedTokens(list2) {
|
|
5016
|
+
const byName = new Map(list2.map((c) => [c.name, c]));
|
|
5017
|
+
for (const c of list2) {
|
|
5018
|
+
const seen = /* @__PURE__ */ new Set([c.name]);
|
|
5019
|
+
const queue = [...c.reuses ?? []];
|
|
5020
|
+
const inherited = [];
|
|
5021
|
+
const taken = new Set(c.tokenPaths);
|
|
5022
|
+
while (queue.length) {
|
|
5023
|
+
const from = byName.get(queue.shift());
|
|
5024
|
+
if (!from || seen.has(from.name)) continue;
|
|
5025
|
+
seen.add(from.name);
|
|
5026
|
+
const paths = from.tokenPaths.filter((p) => !taken.has(p));
|
|
5027
|
+
paths.forEach((p) => taken.add(p));
|
|
5028
|
+
inherited.push({ from: from.name, slug: from.slug, paths });
|
|
5029
|
+
queue.push(...from.reuses ?? []);
|
|
5030
|
+
}
|
|
5031
|
+
c.inherited = inherited;
|
|
5032
|
+
c.allTokenPaths = [...taken].sort();
|
|
5033
|
+
}
|
|
5034
|
+
for (const c of list2) delete c.reuses;
|
|
5035
|
+
}
|
|
5036
|
+
function scanComponents(input) {
|
|
5037
|
+
const { dir, readText, describeVar } = input;
|
|
5038
|
+
const docsSuffix = input.docsSuffix ?? ".docs.json";
|
|
5039
|
+
const styleSuffix = input.styleSuffix ?? ".module.css";
|
|
5040
|
+
const fileSet = new Set(input.files);
|
|
5041
|
+
const has8 = (p) => fileSet.has(p);
|
|
5042
|
+
const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
|
|
5043
|
+
const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
|
|
5044
|
+
const names = [
|
|
5045
|
+
...new Set(
|
|
5046
|
+
input.files.map((f) => f.startsWith(`${dir}/`) ? f.slice(dir.length + 1).split("/") : []).filter((parts) => parts.length === 2 && parts[1] === `${parts[0]}.tsx`).map((parts) => parts[0])
|
|
5047
|
+
)
|
|
5048
|
+
].sort();
|
|
5049
|
+
const reuseRe = new RegExp(`from\\s+'\\.\\.\\/(\\w+)\\/(\\w+)${escapeRe(styleSuffix)}'`, "g");
|
|
5050
|
+
const result = names.map((name) => {
|
|
5051
|
+
const folder = `${dir}/${name}`;
|
|
5052
|
+
const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
|
|
5053
|
+
let docs = null;
|
|
5054
|
+
if (has8(files.docs)) {
|
|
5055
|
+
try {
|
|
5056
|
+
docs = JSON.parse(readText(files.docs) ?? "");
|
|
5057
|
+
} catch (err) {
|
|
5058
|
+
input.problems?.push(`${files.docs}: ${err.message}`);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
const tsxSrc = readText(files.tsx) ?? "";
|
|
5062
|
+
const tsx = parseComponentTsx(name, tsxSrc);
|
|
5063
|
+
const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
|
|
5064
|
+
const cssText = has8(files.css) ? readText(files.css) : void 0;
|
|
5065
|
+
const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
|
|
5066
|
+
const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
|
|
5067
|
+
const storiesText = has8(files.stories) ? readText(files.stories) : void 0;
|
|
5068
|
+
return {
|
|
5069
|
+
name,
|
|
5070
|
+
slug: componentSlug(name),
|
|
5071
|
+
dir: folder,
|
|
5072
|
+
files,
|
|
5073
|
+
...tsx,
|
|
5074
|
+
rules,
|
|
5075
|
+
tokensUsed: used.map((n) => {
|
|
5076
|
+
const d = describeVar(n);
|
|
5077
|
+
return { name: n, href: d?.href, known: !!d, kind: d?.kind };
|
|
5078
|
+
}),
|
|
5079
|
+
tokenPaths: [...new Set(used.flatMap((n) => describeVar(n)?.path ?? []))].sort(),
|
|
5080
|
+
inherited: [],
|
|
5081
|
+
allTokenPaths: [],
|
|
5082
|
+
reuses,
|
|
5083
|
+
stories: storiesText === void 0 ? [] : parseComponentStories(storiesText),
|
|
5084
|
+
docs,
|
|
5085
|
+
category: docs?.category ?? familyOf(name) ?? "Core"
|
|
5086
|
+
};
|
|
5087
|
+
});
|
|
5088
|
+
const order = (c) => {
|
|
5089
|
+
const i = fam.findIndex((f) => f.name === c.category);
|
|
5090
|
+
return i < 0 ? fam.length : i;
|
|
5091
|
+
};
|
|
5092
|
+
result.sort((a, b) => order(a) - order(b) || a.name.localeCompare(b.name));
|
|
5093
|
+
resolveInheritedTokens(result);
|
|
5094
|
+
return result;
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
// ../core/src/repo-model.ts
|
|
5098
|
+
var REPO_MODEL_DEFAULTS = {
|
|
5099
|
+
tokensRoot: "packages/tokens/src",
|
|
5100
|
+
componentsDir: "packages/react/src/components",
|
|
5101
|
+
componentSpecs: "packages/react/component-specs.json",
|
|
5102
|
+
/** Optional token usage notes (group -> sentence), relative to tokens.root, when tokens.usage is not set. */
|
|
5103
|
+
tokensUsage: "usage.json",
|
|
5104
|
+
/** Most global stylesheets read (components.styles): a broad glob never turns into hundreds of reads. */
|
|
5105
|
+
maxStylesheets: 50,
|
|
5106
|
+
repo: { owner: "peterbono", repo: "ai-design-system-demo", defaultBranch: "main" }
|
|
5107
|
+
};
|
|
5108
|
+
function siteRepoFromConfig(config, raw, fallback = REPO_MODEL_DEFAULTS.repo) {
|
|
5109
|
+
const legacy = raw && typeof raw === "object" ? raw : void 0;
|
|
5110
|
+
const g = config.governance;
|
|
5111
|
+
const legacyOwners = legacy?.governance?.owners === void 0 ? legacy?.owners : void 0;
|
|
5112
|
+
return {
|
|
5113
|
+
owner: typeof legacy?.owner === "string" && legacy.owner ? legacy.owner : config.repo?.owner ?? fallback.owner,
|
|
5114
|
+
repo: config.repo?.name ?? fallback.repo,
|
|
5115
|
+
defaultBranch: typeof legacy?.defaultBranch === "string" && legacy.defaultBranch ? legacy.defaultBranch : config.repo?.defaultBranch ?? fallback.defaultBranch,
|
|
5116
|
+
owners: legacyOwners ? { design: legacyOwners.design ?? [], tech: legacyOwners.tech ?? [] } : { design: [...g.owners.design], tech: [...g.owners.tech] },
|
|
5117
|
+
override: g.override ? { ...g.override } : { ...DEFAULT_OVERRIDE },
|
|
5118
|
+
rights: g.rights ?? null,
|
|
5119
|
+
soloOwnerCanApproveBoth: g.soloOwnerCanApproveBoth === true,
|
|
5120
|
+
visualImpact: { mode: config.visualImpact?.mode === "on-demand" ? "on-demand" : "auto" },
|
|
5121
|
+
tokensRoot: config.tokens.root,
|
|
5122
|
+
integrations: config.integrations ?? {},
|
|
5123
|
+
products: config.products.length ? config.products : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
5124
|
+
changes: config.changes ?? DEFAULT_CHANGES_MODE
|
|
5125
|
+
};
|
|
5126
|
+
}
|
|
5127
|
+
function styleGlobs(config) {
|
|
5128
|
+
if (config.components?.styles) return [...config.components.styles];
|
|
5129
|
+
const parent = parentDir(config.components?.dir ?? REPO_MODEL_DEFAULTS.componentsDir);
|
|
5130
|
+
const at = (dir) => parent ? `${parent}/${dir}/**/*.css` : `${dir}/**/*.css`;
|
|
5131
|
+
return [at("styles"), at("theme")];
|
|
5132
|
+
}
|
|
5133
|
+
function globBase(glob) {
|
|
5134
|
+
const segs = glob.split("/");
|
|
5135
|
+
const fixed = [];
|
|
5136
|
+
for (const seg of segs.slice(0, -1)) {
|
|
5137
|
+
if (/[*?[{]/.test(seg)) break;
|
|
5138
|
+
fixed.push(seg);
|
|
5139
|
+
}
|
|
5140
|
+
return fixed.join("/");
|
|
5141
|
+
}
|
|
5142
|
+
async function listStylesheets(source, config) {
|
|
5143
|
+
const globs = styleGlobs(config);
|
|
5144
|
+
const matchers = globs.map(globToRegExp);
|
|
5145
|
+
const compDir = config.components?.dir ?? REPO_MODEL_DEFAULTS.componentsDir;
|
|
5146
|
+
const bases = [...new Set(globs.map(globBase))].filter((b, _, all) => !all.some((o) => o !== b && (o === "" || b.startsWith(`${o}/`))));
|
|
5147
|
+
const listed = (await Promise.all(bases.map((b) => source.list(b)))).flat();
|
|
5148
|
+
const paths = [...new Set(listed)].filter((p) => matchers.some((re) => re.test(p)) && !p.startsWith(`${compDir}/`)).sort();
|
|
5149
|
+
const max = REPO_MODEL_DEFAULTS.maxStylesheets;
|
|
5150
|
+
if (paths.length <= max) return { globs, paths, problems: [] };
|
|
5151
|
+
return { globs, paths: paths.slice(0, max), problems: [`components.styles matches ${paths.length} files: Bridge reads the first ${max}. Narrow the globs to the stylesheets the library ships.`] };
|
|
5152
|
+
}
|
|
5153
|
+
function withModelDefaults(raw) {
|
|
5154
|
+
const tokens2 = raw.tokens && typeof raw.tokens === "object" ? raw.tokens : {};
|
|
5155
|
+
return {
|
|
5156
|
+
...raw,
|
|
5157
|
+
tokens: { ...tokens2, root: tokens2.root ?? REPO_MODEL_DEFAULTS.tokensRoot },
|
|
5158
|
+
components: raw.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir }
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
var defaultModelConfig = () => defineConfig(withModelDefaults({}));
|
|
5162
|
+
function parseModelConfig(text) {
|
|
5163
|
+
if (text === null) return { config: defaultModelConfig(), raw: void 0, problems: [] };
|
|
5164
|
+
let raw;
|
|
5165
|
+
try {
|
|
5166
|
+
raw = JSON.parse(text);
|
|
5167
|
+
} catch (err) {
|
|
5168
|
+
return { config: defaultModelConfig(), raw: void 0, problems: [`${CONFIG_FILE} is not valid JSON (${err.message})`] };
|
|
5169
|
+
}
|
|
5170
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: defaultModelConfig(), raw, problems: [`${CONFIG_FILE}: top level must be an object`] };
|
|
5171
|
+
try {
|
|
5172
|
+
return { config: loadConfig(withModelDefaults(raw)), raw, problems: [] };
|
|
5173
|
+
} catch (err) {
|
|
5174
|
+
const problems = err instanceof ConfigError ? err.problems.map((p) => `${CONFIG_FILE}: ${p}`) : [`${CONFIG_FILE}: ${err.message}`];
|
|
5175
|
+
return { config: defaultModelConfig(), raw, problems };
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
var emptyRows = (config) => ({ ...tokenRowAxes(config), primitives: [], semantic: [], brand: [], density: [], component: [], all: [], sources: [] });
|
|
5179
|
+
async function loadRepoModel(source, options = {}) {
|
|
5180
|
+
const concurrency = options.concurrency ?? 8;
|
|
5181
|
+
const problems = [];
|
|
5182
|
+
const tooLarge = (path, err) => {
|
|
5183
|
+
if (err?.status !== 413) throw err;
|
|
5184
|
+
problems.push(`${path}: ${err.message}`);
|
|
5185
|
+
return null;
|
|
5186
|
+
};
|
|
5187
|
+
const configText = await source.text(CONFIG_FILE);
|
|
5188
|
+
const parsed = parseModelConfig(configText);
|
|
5189
|
+
const { config } = parsed;
|
|
5190
|
+
problems.push(...parsed.problems);
|
|
5191
|
+
const fallbackRepo = source.ref.owner && source.ref.repo ? { owner: source.ref.owner, repo: source.ref.repo, defaultBranch: REPO_MODEL_DEFAULTS.repo.defaultBranch } : REPO_MODEL_DEFAULTS.repo;
|
|
5192
|
+
const site = siteRepoFromConfig(config, parsed.raw, fallbackRepo);
|
|
5193
|
+
const root = config.tokens.root;
|
|
5194
|
+
const underRoot = await source.list(root === "." ? "" : root);
|
|
5195
|
+
const tokenRel = selectTokenFiles(config, underRoot);
|
|
5196
|
+
const contrastPath = config.contrast?.path;
|
|
5197
|
+
const usagePath = tokensPath(config, config.tokens.usage ?? REPO_MODEL_DEFAULTS.tokensUsage);
|
|
5198
|
+
const tokenPaths = tokenRel.map((f) => tokensPath(config, f));
|
|
5199
|
+
const tokenRead = [.../* @__PURE__ */ new Set([...tokenPaths, ...contrastPath ? [contrastPath] : [], usagePath])];
|
|
5200
|
+
const comps = config.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir, docsSuffix: ".docs.json", styleSuffix: ".module.css" };
|
|
5201
|
+
const specsPath = comps.specs ?? REPO_MODEL_DEFAULTS.componentSpecs;
|
|
5202
|
+
const indexPath = comps.index ?? (parentDir(comps.dir) ? `${parentDir(comps.dir)}/index.ts` : "index.ts");
|
|
5203
|
+
let compFiles = [];
|
|
5204
|
+
const codeRead = [specsPath];
|
|
5205
|
+
const docsRead = [];
|
|
5206
|
+
if (options.components !== false) {
|
|
5207
|
+
compFiles = await source.list(comps.dir);
|
|
5208
|
+
for (const f of compFiles) {
|
|
5209
|
+
const parts = f.slice(comps.dir.length + 1).split("/");
|
|
5210
|
+
if (parts.length !== 2) continue;
|
|
5211
|
+
const [n, file] = parts;
|
|
5212
|
+
if (file === `${n}${comps.docsSuffix}`) docsRead.push(f);
|
|
5213
|
+
else if ([`${n}.tsx`, `${n}${comps.styleSuffix}`, `${n}.stories.tsx`].includes(file)) codeRead.push(f);
|
|
5214
|
+
}
|
|
5215
|
+
codeRead.push(indexPath);
|
|
5216
|
+
}
|
|
5217
|
+
const listedStyles = await listStylesheets(source, config);
|
|
5218
|
+
problems.push(...listedStyles.problems);
|
|
5219
|
+
const stylePaths = listedStyles.paths;
|
|
5220
|
+
codeRead.push(...stylePaths);
|
|
5221
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5222
|
+
const fresh = (paths) => paths.filter((p) => {
|
|
5223
|
+
if (seen.has(p)) return false;
|
|
5224
|
+
seen.add(p);
|
|
5225
|
+
return true;
|
|
5226
|
+
});
|
|
5227
|
+
const phases = [
|
|
5228
|
+
["tokens", fresh(tokenRead)],
|
|
5229
|
+
["components", fresh(codeRead)],
|
|
5230
|
+
["docs", fresh(docsRead)]
|
|
5231
|
+
];
|
|
5232
|
+
const filesTotal = phases.reduce((n, [, paths]) => n + paths.length, 0);
|
|
5233
|
+
let before = 0;
|
|
5234
|
+
const readPhase = async (phase, paths) => {
|
|
5235
|
+
const offset = before;
|
|
5236
|
+
const out = await readRepoTexts(source, paths, concurrency, tooLarge, (done, total) => options.onProgress?.({ phase, done, total, filesDone: offset + done, filesTotal }));
|
|
5237
|
+
before += paths.length;
|
|
5238
|
+
return out;
|
|
5239
|
+
};
|
|
5240
|
+
const bodies = await readPhase("tokens", phases[0][1]);
|
|
5241
|
+
const read = (p) => bodies.get(p) ?? void 0;
|
|
5242
|
+
const files = {};
|
|
5243
|
+
tokenRel.forEach((rel, i) => {
|
|
5244
|
+
const body = read(tokenPaths[i]);
|
|
5245
|
+
if (body !== void 0) files[rel] = body;
|
|
5246
|
+
});
|
|
5247
|
+
const listFiles = (dir) => underRoot.filter((p) => parentDir(p) === dir);
|
|
5248
|
+
let rows;
|
|
5249
|
+
try {
|
|
5250
|
+
rows = buildTokenRows(config, read, listFiles);
|
|
5251
|
+
} catch (err) {
|
|
5252
|
+
problems.push(err.message);
|
|
5253
|
+
rows = emptyRows(config);
|
|
5254
|
+
}
|
|
5255
|
+
let usage = null;
|
|
5256
|
+
const usageText = read(usagePath);
|
|
5257
|
+
if (usageText !== void 0) {
|
|
5258
|
+
try {
|
|
5259
|
+
usage = JSON.parse(usageText);
|
|
5260
|
+
} catch (err) {
|
|
5261
|
+
problems.push(`${usagePath}: ${err.message}`);
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
let specs = [];
|
|
5265
|
+
let pairs3 = [];
|
|
5266
|
+
try {
|
|
5267
|
+
specs = parseContrastSpecs(contrastPath ? read(contrastPath) : void 0);
|
|
5268
|
+
pairs3 = evaluateContrastPairs(specs, rows);
|
|
5269
|
+
} catch (err) {
|
|
5270
|
+
problems.push(err.message);
|
|
5271
|
+
}
|
|
5272
|
+
const index = tokenRowIndex(rows);
|
|
5273
|
+
const describeVar = (cssVar) => {
|
|
5274
|
+
const hit = index.get(cssVar);
|
|
5275
|
+
return hit ? { path: hit.path, kind: hit.kind, href: options.href?.(cssVar, hit.type, hit.kind, hit.path) } : void 0;
|
|
5276
|
+
};
|
|
5277
|
+
let components = null;
|
|
5278
|
+
const code = await readPhase("components", phases[1][1]);
|
|
5279
|
+
const docs = await readPhase("docs", phases[2][1]);
|
|
5280
|
+
const readMore = (p) => code.get(p) ?? docs.get(p) ?? bodies.get(p) ?? void 0;
|
|
5281
|
+
if (options.components !== false) {
|
|
5282
|
+
const indexText = readMore(indexPath);
|
|
5283
|
+
const indexDir = parentDir(indexPath);
|
|
5284
|
+
const indexRel = indexDir && comps.dir.startsWith(`${indexDir}/`) ? comps.dir.slice(indexDir.length + 1) : comps.dir;
|
|
5285
|
+
const list2 = scanComponents({ dir: comps.dir, files: compFiles, readText: readMore, indexText, indexRel, docsSuffix: comps.docsSuffix, styleSuffix: comps.styleSuffix, describeVar, problems });
|
|
5286
|
+
components = { dir: comps.dir, index: indexText === void 0 ? void 0 : indexPath, families: indexText === void 0 ? [] : parseComponentFamilies(indexText, indexRel), list: list2 };
|
|
5287
|
+
}
|
|
5288
|
+
const styleVars = {};
|
|
5289
|
+
for (const p of stylePaths) {
|
|
5290
|
+
const text = readMore(p);
|
|
5291
|
+
if (text !== void 0) styleVars[p] = cssVarsIn(text);
|
|
5292
|
+
}
|
|
5293
|
+
const specsText = readMore(specsPath);
|
|
5294
|
+
let specsFile = null;
|
|
5295
|
+
let coverage = [];
|
|
5296
|
+
if (specsText !== void 0) {
|
|
5297
|
+
try {
|
|
5298
|
+
specsFile = parseComponentSpecs(specsText);
|
|
5299
|
+
} catch {
|
|
5300
|
+
}
|
|
5301
|
+
try {
|
|
5302
|
+
coverage = JSON.parse(specsText).coverage ?? [];
|
|
5303
|
+
} catch (err) {
|
|
5304
|
+
problems.push(`${specsPath}: ${err.message}`);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
return {
|
|
5308
|
+
ref: source.ref,
|
|
5309
|
+
configText,
|
|
5310
|
+
config,
|
|
5311
|
+
site,
|
|
5312
|
+
problems,
|
|
5313
|
+
tokens: { root, files, rows, usage },
|
|
5314
|
+
contrast: { path: contrastPath, specs, pairs: pairs3 },
|
|
5315
|
+
components,
|
|
5316
|
+
styles: { globs: listedStyles.globs, vars: styleVars },
|
|
5317
|
+
specs: { path: specsPath, file: specsFile, coverage }
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
// ../core/src/snapshot.ts
|
|
5322
|
+
var SNAPSHOT_SCHEMA_VERSION = 1;
|
|
5323
|
+
var SNAPSHOT_BRANCH = "bridge-data";
|
|
5324
|
+
var SNAPSHOT_FILE = "snapshot.json";
|
|
5325
|
+
var SNAPSHOT_AUDITS_DIR = "audits";
|
|
5326
|
+
var SNAPSHOT_MAX_BYTES = 5e6;
|
|
5327
|
+
function recordingSource(source) {
|
|
5328
|
+
const paths = /* @__PURE__ */ new Set();
|
|
5329
|
+
const files = /* @__PURE__ */ new Map();
|
|
5330
|
+
return {
|
|
5331
|
+
paths,
|
|
5332
|
+
files,
|
|
5333
|
+
source: {
|
|
5334
|
+
get ref() {
|
|
5335
|
+
return source.ref;
|
|
5336
|
+
},
|
|
5337
|
+
async list(prefix) {
|
|
5338
|
+
const out = await source.list(prefix);
|
|
5339
|
+
for (const p of out) paths.add(p);
|
|
5340
|
+
return out;
|
|
5341
|
+
},
|
|
5342
|
+
async text(path) {
|
|
5343
|
+
const body = await source.text(path);
|
|
5344
|
+
if (body !== null) files.set(normalizeRepoPath(path), body);
|
|
5345
|
+
return body;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
function snapshotAudit(id, report) {
|
|
5351
|
+
return {
|
|
5352
|
+
id,
|
|
5353
|
+
path: `${SNAPSHOT_AUDITS_DIR}/${id}.json`,
|
|
5354
|
+
...report.label ? { label: report.label } : {},
|
|
5355
|
+
generatedAt: report.generatedAt,
|
|
5356
|
+
target: { path: report.target.path, files: report.target.files },
|
|
5357
|
+
metrics: report.metrics
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5360
|
+
var auditId = (s) => s.toLowerCase().replace(/\.json$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "audit";
|
|
5361
|
+
var pairResults = (p) => {
|
|
5362
|
+
const brands = Object.values(p.brands);
|
|
5363
|
+
return brands.length ? brands.flatMap((b) => Object.values(b)) : Object.values(p.modes);
|
|
5364
|
+
};
|
|
5365
|
+
function snapshotSummary(model) {
|
|
5366
|
+
const results = model.contrast.pairs.flatMap((p) => pairResults(p).map((r) => r.ratio >= p.min));
|
|
5367
|
+
const coverage = model.specs.coverage;
|
|
5368
|
+
const visual = coverage.filter((c) => c.status !== "layout").length;
|
|
5369
|
+
const inFigma = coverage.filter((c) => c.status === "generated" || c.status === "drafted").length;
|
|
5370
|
+
return {
|
|
5371
|
+
tokens: model.tokens.rows.all.length + model.tokens.rows.primitives.length,
|
|
5372
|
+
components: model.components ? model.components.list.length : null,
|
|
5373
|
+
contrast: { pairs: model.contrast.pairs.length, checks: results.length, failing: results.filter((ok) => !ok).length },
|
|
5374
|
+
figma: coverage.length ? { inFigma, visual } : null
|
|
5375
|
+
};
|
|
5376
|
+
}
|
|
5377
|
+
function buildSnapshot(input) {
|
|
5378
|
+
const { model } = input;
|
|
5379
|
+
const files = {};
|
|
5380
|
+
const entries = input.files instanceof Map ? [...input.files] : Object.entries(input.files);
|
|
5381
|
+
const specsPath = normalizeRepoPath(model.specs.path);
|
|
5382
|
+
for (const [k, v] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
5383
|
+
const p = normalizeRepoPath(k);
|
|
5384
|
+
if (p !== specsPath) files[p] = v;
|
|
5385
|
+
}
|
|
5386
|
+
const hasSpecs = entries.some(([k]) => normalizeRepoPath(k) === specsPath);
|
|
5387
|
+
return {
|
|
5388
|
+
kind: "bridge-snapshot",
|
|
5389
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
5390
|
+
tool: { name: "bridge", version: input.version },
|
|
5391
|
+
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5392
|
+
repo: { owner: model.ref.owner || model.site.owner, name: model.ref.repo || model.site.repo, branch: input.branch ?? model.site.defaultBranch },
|
|
5393
|
+
sourceSha: model.ref.sha,
|
|
5394
|
+
...input.committedAt ? { committedAt: input.committedAt } : {},
|
|
5395
|
+
paths: [...new Set([...input.paths].map(normalizeRepoPath))].filter((p) => p !== specsPath).sort(),
|
|
5396
|
+
files,
|
|
5397
|
+
figma: hasSpecs ? { path: specsPath, ...model.specs.file?.generated ? { generated: model.specs.file.generated } : {}, coverage: model.specs.coverage } : null,
|
|
5398
|
+
summary: snapshotSummary(model),
|
|
5399
|
+
contrast: { ...model.contrast.path ? { path: model.contrast.path } : {}, pairs: model.contrast.pairs.map(({ fgToken: _fg, bgToken: _bg, ...rest }) => rest) },
|
|
5400
|
+
audits: input.audits ?? [],
|
|
5401
|
+
problems: [...model.problems],
|
|
5402
|
+
styles: [...model.styles.globs]
|
|
5403
|
+
};
|
|
5404
|
+
}
|
|
5405
|
+
var SNAPSHOT_LLMS_FILE = "llms.txt";
|
|
5406
|
+
var LLMS_MAX_TOKENS = 600;
|
|
5407
|
+
var line = (text) => typeof text === "string" ? text.replace(/\s+/g, " ").trim() : "";
|
|
5408
|
+
var strings = (list2) => Array.isArray(list2) ? list2.map(line).filter(Boolean) : [];
|
|
5409
|
+
function snapshotLlmsTxt(model, snapshot) {
|
|
5410
|
+
const { owner, name, branch } = snapshot.repo;
|
|
5411
|
+
const date2 = (snapshot.committedAt ?? snapshot.generatedAt).slice(0, 10);
|
|
5412
|
+
const rows = model.tokens.rows;
|
|
5413
|
+
const tokens2 = rows.all.filter((t) => !t.deprecated);
|
|
5414
|
+
const shown = tokens2.slice(0, LLMS_MAX_TOKENS);
|
|
5415
|
+
const comps = model.components?.list ?? [];
|
|
5416
|
+
const out = [
|
|
5417
|
+
`# ${owner}/${name} design system`,
|
|
5418
|
+
"",
|
|
5419
|
+
`> Tokens, components and usage guidance of the design system in github.com/${owner}/${name}, read at ${snapshot.sourceSha.slice(0, 7)} on ${branch} (${date2}). Published by Bridge for AI agents and people.`,
|
|
5420
|
+
"",
|
|
5421
|
+
`- Source: https://github.com/${owner}/${name}/tree/${snapshot.sourceSha}`,
|
|
5422
|
+
`- Every file Bridge read, as JSON: ${SNAPSHOT_FILE}, next to this file on the ${SNAPSHOT_BRANCH} branch.`,
|
|
5423
|
+
`- Themes: ${rows.themes.join(", ") || "none"}. Brands: ${rows.brands.join(", ") || "none"}. Densities: ${rows.densities.join(", ") || "none"}.`,
|
|
5424
|
+
"",
|
|
5425
|
+
"## Tokens",
|
|
5426
|
+
"",
|
|
5427
|
+
`${tokens2.length} tokens (brand, semantic, density and component tiers) on top of ${rows.primitives.length} primitives. Use the semantic tokens in product code, never the primitives. Values are for the default brand${rows.defaultBrand ? ` (${rows.defaultBrand})` : ""}.`,
|
|
5428
|
+
""
|
|
5429
|
+
];
|
|
5430
|
+
for (const t of shown) {
|
|
5431
|
+
const values = Object.entries(t.modes).map(([theme, v]) => `${theme} ${line(v.css)}`).join(", ");
|
|
5432
|
+
const note = line(t.description);
|
|
5433
|
+
out.push(`- \`${t.path}\` (\`${t.cssVar}\`, ${t.tier} ${t.type}): ${values || "no value"}${note ? `. ${note}` : ""}`);
|
|
5434
|
+
}
|
|
5435
|
+
if (tokens2.length > shown.length) out.push("", `${tokens2.length - shown.length} more tokens are in ${SNAPSHOT_FILE}.`);
|
|
5436
|
+
out.push("", "## Components", "");
|
|
5437
|
+
if (!comps.length) out.push("No components were read (components.dir in bridge.config.json names the folder).");
|
|
5438
|
+
for (const c of comps) {
|
|
5439
|
+
const d = c.docs;
|
|
5440
|
+
out.push(`### ${line(c.name)}`, "");
|
|
5441
|
+
const summary = line(d?.summary) || line(c.description);
|
|
5442
|
+
if (summary) out.push(summary, "");
|
|
5443
|
+
out.push(`- Family: ${line(c.category)}${c.docs?.status && c.docs.status !== "stable" ? `, ${c.docs.status}` : ""}`);
|
|
5444
|
+
if (c.allTokenPaths.length) out.push(`- Tokens: ${c.allTokenPaths.slice(0, 40).join(", ")}${c.allTokenPaths.length > 40 ? ", ..." : ""}`);
|
|
5445
|
+
for (const [label, list2] of [
|
|
5446
|
+
["Use it for", strings(d?.whenToUse)],
|
|
5447
|
+
["Not for", strings(d?.whenNotToUse)],
|
|
5448
|
+
["Accessibility", strings(d?.accessibility)]
|
|
5449
|
+
]) {
|
|
5450
|
+
if (list2.length) out.push(`- ${label}:`, ...list2.map((x) => ` - ${x}`));
|
|
5451
|
+
}
|
|
5452
|
+
const dos = Array.isArray(d?.dos) ? d.dos : [];
|
|
5453
|
+
const donts = Array.isArray(d?.donts) ? d.donts : [];
|
|
5454
|
+
for (const g of dos) if (line(g?.title)) out.push(`- Do: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
|
|
5455
|
+
for (const g of donts) if (line(g?.title)) out.push(`- Don't: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
|
|
5456
|
+
out.push("");
|
|
5457
|
+
}
|
|
5458
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
|
|
5459
|
+
`;
|
|
5460
|
+
}
|
|
5461
|
+
|
|
4331
5462
|
// ../visual/src/capture.ts
|
|
4332
5463
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4333
5464
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -4425,15 +5556,15 @@ async function shoot(page, r) {
|
|
|
4425
5556
|
}
|
|
4426
5557
|
async function switchStory(page, storyId2) {
|
|
4427
5558
|
const ok = await page.evaluate(
|
|
4428
|
-
(id) => new Promise((
|
|
5559
|
+
(id) => new Promise((resolve7) => {
|
|
4429
5560
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
4430
|
-
if (!ch) return
|
|
5561
|
+
if (!ch) return resolve7(false);
|
|
4431
5562
|
const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
|
|
4432
5563
|
const timer = setTimeout(() => done(), 15e3);
|
|
4433
5564
|
function done() {
|
|
4434
5565
|
clearTimeout(timer);
|
|
4435
5566
|
for (const e of events) ch.off(e, done);
|
|
4436
|
-
|
|
5567
|
+
resolve7(true);
|
|
4437
5568
|
}
|
|
4438
5569
|
for (const e of events) ch.on(e, done);
|
|
4439
5570
|
window.scrollTo(0, 0);
|
|
@@ -4804,9 +5935,9 @@ function storiesFromSource(files) {
|
|
|
4804
5935
|
}
|
|
4805
5936
|
|
|
4806
5937
|
// ../visual/src/serve.ts
|
|
4807
|
-
import { createReadStream, existsSync as
|
|
5938
|
+
import { createReadStream, existsSync as existsSync3, statSync as statSync3 } from "node:fs";
|
|
4808
5939
|
import { createServer } from "node:http";
|
|
4809
|
-
import { extname, join as
|
|
5940
|
+
import { extname, join as join3, normalize, sep as sep2 } from "node:path";
|
|
4810
5941
|
var TYPES = {
|
|
4811
5942
|
".html": "text/html; charset=utf-8",
|
|
4812
5943
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -4830,50 +5961,50 @@ function serveDir(dir) {
|
|
|
4830
5961
|
const root = normalize(dir);
|
|
4831
5962
|
const server = createServer((req, res) => {
|
|
4832
5963
|
const path = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
|
|
4833
|
-
let file = normalize(
|
|
5964
|
+
let file = normalize(join3(root, path));
|
|
4834
5965
|
if (!file.startsWith(root + sep2) && file !== root) {
|
|
4835
5966
|
res.writeHead(403).end();
|
|
4836
5967
|
return;
|
|
4837
5968
|
}
|
|
4838
|
-
if (
|
|
4839
|
-
if (!
|
|
5969
|
+
if (existsSync3(file) && statSync3(file).isDirectory()) file = join3(file, "index.html");
|
|
5970
|
+
if (!existsSync3(file)) {
|
|
4840
5971
|
if (extname(path)) {
|
|
4841
5972
|
res.writeHead(404).end();
|
|
4842
5973
|
return;
|
|
4843
5974
|
}
|
|
4844
|
-
file =
|
|
5975
|
+
file = join3(root, "index.html");
|
|
4845
5976
|
}
|
|
4846
5977
|
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
|
|
4847
5978
|
createReadStream(file).pipe(res);
|
|
4848
5979
|
});
|
|
4849
|
-
return new Promise((
|
|
5980
|
+
return new Promise((resolve7, reject) => {
|
|
4850
5981
|
server.once("error", reject);
|
|
4851
5982
|
server.listen(0, "127.0.0.1", () => {
|
|
4852
5983
|
const addr = server.address();
|
|
4853
5984
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
4854
|
-
|
|
5985
|
+
resolve7({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
|
|
4855
5986
|
});
|
|
4856
5987
|
});
|
|
4857
5988
|
}
|
|
4858
5989
|
|
|
4859
5990
|
// ../visual/src/workspace.ts
|
|
4860
5991
|
import { spawn } from "node:child_process";
|
|
4861
|
-
import { existsSync as
|
|
5992
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4862
5993
|
import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, realpath, rm, symlink } from "node:fs/promises";
|
|
4863
|
-
import { dirname as dirname3, join as
|
|
5994
|
+
import { dirname as dirname3, join as join4, relative as relative2 } from "node:path";
|
|
4864
5995
|
var PRIVATE = /* @__PURE__ */ new Set([".cache", ".vite", ".vite-temp", ".package-lock.json"]);
|
|
4865
5996
|
async function workspaceDirs(root) {
|
|
4866
|
-
const pkg = JSON.parse(await readFile2(
|
|
5997
|
+
const pkg = JSON.parse(await readFile2(join4(root, "package.json"), "utf8"));
|
|
4867
5998
|
const pats = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? [];
|
|
4868
5999
|
const out = [];
|
|
4869
6000
|
for (const p of pats) {
|
|
4870
6001
|
if (!p.endsWith("/*")) {
|
|
4871
|
-
if (
|
|
6002
|
+
if (existsSync4(join4(root, p, "package.json"))) out.push(p);
|
|
4872
6003
|
continue;
|
|
4873
6004
|
}
|
|
4874
6005
|
const parent = p.slice(0, -2);
|
|
4875
|
-
if (!
|
|
4876
|
-
for (const d of await readdir(
|
|
6006
|
+
if (!existsSync4(join4(root, parent))) continue;
|
|
6007
|
+
for (const d of await readdir(join4(root, parent))) if (existsSync4(join4(root, parent, d, "package.json"))) out.push(`${parent}/${d}`);
|
|
4877
6008
|
}
|
|
4878
6009
|
return out;
|
|
4879
6010
|
}
|
|
@@ -4882,30 +6013,30 @@ async function link(target, at) {
|
|
|
4882
6013
|
await symlink(target, at);
|
|
4883
6014
|
}
|
|
4884
6015
|
async function linkNodeModules(src, dst) {
|
|
4885
|
-
const srcNm = await realpath(
|
|
4886
|
-
const dstNm =
|
|
4887
|
-
if (
|
|
6016
|
+
const srcNm = await realpath(join4(src, "node_modules"));
|
|
6017
|
+
const dstNm = join4(dst, "node_modules");
|
|
6018
|
+
if (existsSync4(dstNm)) return;
|
|
4888
6019
|
const dirs = await workspaceDirs(dst);
|
|
4889
6020
|
const names = /* @__PURE__ */ new Map();
|
|
4890
6021
|
for (const d of dirs) {
|
|
4891
|
-
const { name } = JSON.parse(await readFile2(
|
|
6022
|
+
const { name } = JSON.parse(await readFile2(join4(dst, d, "package.json"), "utf8"));
|
|
4892
6023
|
if (name) names.set(name, d);
|
|
4893
6024
|
}
|
|
4894
6025
|
await mkdir2(dstNm, { recursive: true });
|
|
4895
6026
|
for (const e of await readdir(srcNm)) {
|
|
4896
6027
|
if (PRIVATE.has(e)) continue;
|
|
4897
6028
|
if (e.startsWith("@")) {
|
|
4898
|
-
for (const c of await readdir(
|
|
4899
|
-
} else if (!names.has(e)) await link(
|
|
6029
|
+
for (const c of await readdir(join4(srcNm, e))) if (!names.has(`${e}/${c}`)) await link(join4(srcNm, e, c), join4(dstNm, e, c));
|
|
6030
|
+
} else if (!names.has(e)) await link(join4(srcNm, e), join4(dstNm, e));
|
|
4900
6031
|
}
|
|
4901
6032
|
for (const [name, d] of names) {
|
|
4902
|
-
const at =
|
|
6033
|
+
const at = join4(dstNm, name);
|
|
4903
6034
|
await rm(at, { force: true, recursive: false }).catch(() => void 0);
|
|
4904
|
-
await link(relative2(dirname3(at),
|
|
6035
|
+
await link(relative2(dirname3(at), join4(dst, d)), at);
|
|
4905
6036
|
}
|
|
4906
6037
|
for (const d of dirs) {
|
|
4907
|
-
const nested =
|
|
4908
|
-
if (
|
|
6038
|
+
const nested = join4(src, d, "node_modules");
|
|
6039
|
+
if (existsSync4(nested) && !existsSync4(join4(dst, d, "node_modules"))) await link(await realpath(nested), join4(dst, d, "node_modules"));
|
|
4909
6040
|
}
|
|
4910
6041
|
}
|
|
4911
6042
|
async function addWorktree(repo, sha, dir) {
|
|
@@ -4915,7 +6046,7 @@ async function addWorktree(repo, sha, dir) {
|
|
|
4915
6046
|
}
|
|
4916
6047
|
async function removeWorktree(repo, dir) {
|
|
4917
6048
|
try {
|
|
4918
|
-
await rm(
|
|
6049
|
+
await rm(join4(dir, "node_modules"), { recursive: true, force: true });
|
|
4919
6050
|
await git(repo, ["worktree", "remove", "--force", dir]);
|
|
4920
6051
|
} catch {
|
|
4921
6052
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -4923,8 +6054,8 @@ async function removeWorktree(repo, dir) {
|
|
|
4923
6054
|
await git(repo, ["worktree", "prune"]).catch(() => void 0);
|
|
4924
6055
|
}
|
|
4925
6056
|
function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
4926
|
-
return new Promise((
|
|
4927
|
-
const bin = [.../* @__PURE__ */ new Set([
|
|
6057
|
+
return new Promise((resolve7, reject) => {
|
|
6058
|
+
const bin = [.../* @__PURE__ */ new Set([join4(cwd, "node_modules", ".bin"), join4(root, "node_modules", ".bin")])].join(":");
|
|
4928
6059
|
const child = spawn(cmd, args, {
|
|
4929
6060
|
cwd,
|
|
4930
6061
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=3072", STORYBOOK_DISABLE_TELEMETRY: "1", ...env },
|
|
@@ -4936,7 +6067,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
|
4936
6067
|
child.stderr.on("data", keep);
|
|
4937
6068
|
child.on("error", reject);
|
|
4938
6069
|
child.on("close", (code) => {
|
|
4939
|
-
if (code === 0)
|
|
6070
|
+
if (code === 0) resolve7();
|
|
4940
6071
|
else {
|
|
4941
6072
|
log(tail);
|
|
4942
6073
|
reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
|
|
@@ -4952,17 +6083,17 @@ var DEFAULT_BUILD = {
|
|
|
4952
6083
|
var isDir = async (p) => (await lstat(p).catch(() => null))?.isDirectory() ?? false;
|
|
4953
6084
|
async function buildRef(o) {
|
|
4954
6085
|
const cfg = o.config ?? DEFAULT_BUILD;
|
|
4955
|
-
const out =
|
|
4956
|
-
const sbOut =
|
|
4957
|
-
const appOut =
|
|
4958
|
-
const needSb = o.plan.storybook && !
|
|
4959
|
-
const needApp = o.plan.app && !
|
|
6086
|
+
const out = join4(o.cache, o.sha);
|
|
6087
|
+
const sbOut = join4(out, "storybook");
|
|
6088
|
+
const appOut = join4(out, "app");
|
|
6089
|
+
const needSb = o.plan.storybook && !existsSync4(join4(sbOut, "index.json"));
|
|
6090
|
+
const needApp = o.plan.app && !existsSync4(join4(appOut, "index.html"));
|
|
4960
6091
|
const built = { ...o.plan.storybook ? { storybook: sbOut } : {}, ...o.plan.app ? { app: appOut } : {} };
|
|
4961
6092
|
if (!needSb && !needApp) {
|
|
4962
6093
|
o.log(` ${o.sha.slice(0, 7)}: builds cached`);
|
|
4963
6094
|
return built;
|
|
4964
6095
|
}
|
|
4965
|
-
const dir =
|
|
6096
|
+
const dir = join4(o.work, `wt-${o.sha.slice(0, 12)}`);
|
|
4966
6097
|
o.log(` ${o.sha.slice(0, 7)}: worktree ${dir}`);
|
|
4967
6098
|
await addWorktree(o.repo, o.sha, dir);
|
|
4968
6099
|
try {
|
|
@@ -4974,15 +6105,15 @@ async function buildRef(o) {
|
|
|
4974
6105
|
await rm(sbOut, { recursive: true, force: true });
|
|
4975
6106
|
await run("storybook", ["build", "-c", cfg.storybookConfigDir, "-o", sbOut, "--quiet", "--disable-telemetry"], dir, o.log);
|
|
4976
6107
|
}
|
|
4977
|
-
if (needApp && await isDir(
|
|
6108
|
+
if (needApp && await isDir(join4(dir, cfg.appDir))) {
|
|
4978
6109
|
o.log(` ${o.sha.slice(0, 7)}: app build`);
|
|
4979
6110
|
await rm(appOut, { recursive: true, force: true });
|
|
4980
|
-
await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"],
|
|
6111
|
+
await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"], join4(dir, cfg.appDir), o.log, dir);
|
|
4981
6112
|
}
|
|
4982
6113
|
} finally {
|
|
4983
6114
|
if (!o.keep) await removeWorktree(o.repo, dir);
|
|
4984
6115
|
}
|
|
4985
|
-
if (o.plan.app && !
|
|
6116
|
+
if (o.plan.app && !existsSync4(join4(appOut, "index.html"))) delete built.app;
|
|
4986
6117
|
return built;
|
|
4987
6118
|
}
|
|
4988
6119
|
|
|
@@ -4990,7 +6121,7 @@ async function buildRef(o) {
|
|
|
4990
6121
|
var STORY_READY = `document.body.classList.contains('sb-show-main') && (document.querySelector('#storybook-root')?.childElementCount > 0 || document.querySelector('[role="dialog"]')) || document.body.classList.contains('sb-show-errordisplay')`;
|
|
4991
6122
|
var APP_READY = `document.querySelector('#root')?.childElementCount > 0 && !document.querySelector('[aria-busy="true"]')`;
|
|
4992
6123
|
async function readConfig(root, sha) {
|
|
4993
|
-
const text = await showFile(root, sha, "bridge.config.json") ?? (
|
|
6124
|
+
const text = await showFile(root, sha, "bridge.config.json") ?? (existsSync5(join5(root, "bridge.config.json")) ? await readFile3(join5(root, "bridge.config.json"), "utf8") : null);
|
|
4994
6125
|
if (!text) throw new Error("bridge.config.json not found: run `bridge init` first.");
|
|
4995
6126
|
return loadConfig(text);
|
|
4996
6127
|
}
|
|
@@ -5013,7 +6144,7 @@ function changedTokens(before, after) {
|
|
|
5013
6144
|
return [...modes.entries()].map(([path, m]) => ({ path, kind: direct.has(path) ? "direct" : "alias", modes: m })).sort((a, b) => Number(a.kind === "alias") - Number(b.kind === "alias") || a.path.localeCompare(b.path));
|
|
5014
6145
|
}
|
|
5015
6146
|
async function readIndexJson(dir) {
|
|
5016
|
-
const index = parseStorybookIndex(JSON.parse(await readFile3(
|
|
6147
|
+
const index = parseStorybookIndex(JSON.parse(await readFile3(join5(dir, "index.json"), "utf8")), "index.json");
|
|
5017
6148
|
return index.entries.filter((e) => e.type === "story").map(({ id, title, name, importPath }) => ({ id, title, name, importPath }));
|
|
5018
6149
|
}
|
|
5019
6150
|
async function planImpact(o) {
|
|
@@ -5055,7 +6186,7 @@ async function runImpact(o) {
|
|
|
5055
6186
|
const log = o.log ?? ((m) => process.stderr.write(`${m}
|
|
5056
6187
|
`));
|
|
5057
6188
|
const root = resolve2(o.root);
|
|
5058
|
-
const outDir = resolve2(o.out ??
|
|
6189
|
+
const outDir = resolve2(o.out ?? join5(root, "visual-impact"));
|
|
5059
6190
|
const threshold = o.threshold ?? 0.03;
|
|
5060
6191
|
const only = o.only?.length ? o.only : ["stories", "app"];
|
|
5061
6192
|
const vp = { stories: o.viewport?.stories ?? { width: 1024, height: 768 }, app: o.viewport?.app ?? { width: 1280, height: 800 } };
|
|
@@ -5082,12 +6213,12 @@ async function runImpact(o) {
|
|
|
5082
6213
|
shots: shots2
|
|
5083
6214
|
};
|
|
5084
6215
|
await mkdir3(outDir, { recursive: true });
|
|
5085
|
-
const manifestPath =
|
|
6216
|
+
const manifestPath = join5(outDir, "manifest.json");
|
|
5086
6217
|
await writeFile3(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
5087
6218
|
`);
|
|
5088
6219
|
let commentPath;
|
|
5089
6220
|
if (o.imageBase !== void 0 || o.artifactUrl) {
|
|
5090
|
-
commentPath =
|
|
6221
|
+
commentPath = join5(outDir, "comment.md");
|
|
5091
6222
|
await writeFile3(commentPath, renderComment(manifest, { ...o.imageBase ? { imageBase: o.imageBase } : {}, ...o.artifactUrl ? { artifactUrl: o.artifactUrl } : {} }));
|
|
5092
6223
|
}
|
|
5093
6224
|
return { manifest, outDir, manifestPath, ...commentPath ? { commentPath } : {}, summary: summaryLine(manifest) };
|
|
@@ -5099,8 +6230,8 @@ async function runImpact(o) {
|
|
|
5099
6230
|
log("Nothing visual to capture.");
|
|
5100
6231
|
return finish([]);
|
|
5101
6232
|
}
|
|
5102
|
-
const cache2 = resolve2(o.cacheDir ??
|
|
5103
|
-
const work = resolve2(o.workDir ??
|
|
6233
|
+
const cache2 = resolve2(o.cacheDir ?? join5(root, "node_modules", ".cache", "bridge-visual"));
|
|
6234
|
+
const work = resolve2(o.workDir ?? join5(tmpdir(), `bridge-visual-${process.pid}`));
|
|
5104
6235
|
await mkdir3(work, { recursive: true });
|
|
5105
6236
|
const builds = { base: {}, head: {} };
|
|
5106
6237
|
try {
|
|
@@ -5129,7 +6260,7 @@ async function runImpact(o) {
|
|
|
5129
6260
|
}
|
|
5130
6261
|
const byId = /* @__PURE__ */ new Map();
|
|
5131
6262
|
if (builds.head.storybook) for (const s of await readIndexJson(builds.head.storybook)) byId.set(s.id, s);
|
|
5132
|
-
if (builds.base.storybook &&
|
|
6263
|
+
if (builds.base.storybook && existsSync5(join5(builds.base.storybook, "index.json"))) {
|
|
5133
6264
|
for (const s of await readIndexJson(builds.base.storybook)) if (!byId.has(s.id)) byId.set(s.id, s);
|
|
5134
6265
|
}
|
|
5135
6266
|
const jobs = [];
|
|
@@ -5144,18 +6275,18 @@ async function runImpact(o) {
|
|
|
5144
6275
|
for (const theme of THEMES)
|
|
5145
6276
|
for (const r of routes) jobs.push({ key: shotKey("app", r.id, t.id, theme), kind: "route", label: r.label, route: r, tenant: t, brand: t.brand, theme });
|
|
5146
6277
|
await rm2(outDir, { recursive: true, force: true });
|
|
5147
|
-
for (const d of ["before", "after", "diff", "composite"]) await mkdir3(
|
|
6278
|
+
for (const d of ["before", "after", "diff", "composite"]) await mkdir3(join5(outDir, d), { recursive: true });
|
|
5148
6279
|
const errors = /* @__PURE__ */ new Map();
|
|
5149
6280
|
for (const side of ["base", "head"]) {
|
|
5150
6281
|
const b = builds[side];
|
|
5151
6282
|
const served = [];
|
|
5152
6283
|
const browser = await launch();
|
|
5153
6284
|
try {
|
|
5154
|
-
const sb = b.storybook &&
|
|
5155
|
-
const app = b.app &&
|
|
6285
|
+
const sb = b.storybook && existsSync5(join5(b.storybook, "index.json")) ? await serveDir(b.storybook) : null;
|
|
6286
|
+
const app = b.app && existsSync5(join5(b.app, "index.html")) ? await serveDir(b.app) : null;
|
|
5156
6287
|
if (sb) served.push(sb);
|
|
5157
6288
|
if (app) served.push(app);
|
|
5158
|
-
const dir =
|
|
6289
|
+
const dir = join5(outDir, side === "base" ? "before" : "after");
|
|
5159
6290
|
const storyJobs = jobs.filter((j) => j.kind === "story");
|
|
5160
6291
|
if (sb && storyJobs.length) {
|
|
5161
6292
|
log(`Capturing ${storyJobs.length} story shots at ${side}`);
|
|
@@ -5170,7 +6301,7 @@ async function runImpact(o) {
|
|
|
5170
6301
|
if (!known.has(j.storyId)) continue;
|
|
5171
6302
|
const err = await shoot(page, {
|
|
5172
6303
|
url: storyUrl(sb.url, j.storyId, j.brand, j.theme),
|
|
5173
|
-
file:
|
|
6304
|
+
file: join5(dir, `${j.key}.png`),
|
|
5174
6305
|
fullPage: true,
|
|
5175
6306
|
readyFn: STORY_READY,
|
|
5176
6307
|
// Loading stories (Spinner, Skeleton) stay aria-busy on purpose: do not wait for them.
|
|
@@ -5196,7 +6327,7 @@ async function runImpact(o) {
|
|
|
5196
6327
|
const ctx = await newContext(browser, vp.app);
|
|
5197
6328
|
const page = await newPage(ctx);
|
|
5198
6329
|
for (const j of routeJobs.filter((x) => x.tenant.id === t.id && x.theme === theme)) {
|
|
5199
|
-
const err = await shoot(page, { url: routeUrl(app.url, j.route, t.id, theme), file:
|
|
6330
|
+
const err = await shoot(page, { url: routeUrl(app.url, j.route, t.id, theme), file: join5(dir, `${j.key}.png`), fullPage: false, readyFn: APP_READY, ...j.route.waitFor ? { waitFor: j.route.waitFor } : {}, mask: ["[data-visual-mask]", ...j.route.mask ?? []] });
|
|
5200
6331
|
if (err) errors.set(`${side}:${j.key}`, err);
|
|
5201
6332
|
}
|
|
5202
6333
|
await ctx.close();
|
|
@@ -5210,10 +6341,10 @@ async function runImpact(o) {
|
|
|
5210
6341
|
log("Diffing");
|
|
5211
6342
|
const shots = [];
|
|
5212
6343
|
for (const j of jobs) {
|
|
5213
|
-
const bf =
|
|
5214
|
-
const af =
|
|
5215
|
-
const hasB =
|
|
5216
|
-
const hasA =
|
|
6344
|
+
const bf = join5(outDir, "before", `${j.key}.png`);
|
|
6345
|
+
const af = join5(outDir, "after", `${j.key}.png`);
|
|
6346
|
+
const hasB = existsSync5(bf);
|
|
6347
|
+
const hasA = existsSync5(af);
|
|
5217
6348
|
const base = {
|
|
5218
6349
|
key: j.key,
|
|
5219
6350
|
kind: j.kind,
|
|
@@ -5253,7 +6384,7 @@ async function runImpact(o) {
|
|
|
5253
6384
|
if (changed || o.keepUnchanged) Object.assign(images, { before: `before/${j.key}.png`, after: `after/${j.key}.png` });
|
|
5254
6385
|
else await Promise.all([unlink(bf), unlink(af)]);
|
|
5255
6386
|
if (d.diff) {
|
|
5256
|
-
await writeFile3(
|
|
6387
|
+
await writeFile3(join5(outDir, "diff", `${j.key}.png`), encodePng(d.diff));
|
|
5257
6388
|
images.diff = `diff/${j.key}.png`;
|
|
5258
6389
|
}
|
|
5259
6390
|
shots.push({
|
|
@@ -5278,13 +6409,13 @@ async function runImpact(o) {
|
|
|
5278
6409
|
for (const s of toComposite) {
|
|
5279
6410
|
const w = Math.max(s.size.before?.width ?? 0, s.size.after?.width ?? 0);
|
|
5280
6411
|
const h = Math.max(s.size.before?.height ?? 0, s.size.after?.height ?? 0);
|
|
5281
|
-
const file =
|
|
6412
|
+
const file = join5(outDir, "composite", `${s.key}.png`);
|
|
5282
6413
|
await composite(page, {
|
|
5283
6414
|
title: s.label,
|
|
5284
6415
|
subtitle: `${s.tenant ? `${DEFAULT_TENANTS.find((t) => t.id === s.tenant)?.name ?? s.tenant}, ` : ""}${s.brand}, ${s.theme} \xB7 ${s.status === "changed" ? `${s.percent.toFixed(2)}% changed` : s.status}`,
|
|
5285
|
-
before: s.images.before ?
|
|
5286
|
-
after: s.images.after ?
|
|
5287
|
-
diff: s.images.diff ?
|
|
6416
|
+
before: s.images.before ? join5(outDir, s.images.before) : null,
|
|
6417
|
+
after: s.images.after ? join5(outDir, s.images.after) : null,
|
|
6418
|
+
diff: s.images.diff ? join5(outDir, s.images.diff) : null,
|
|
5288
6419
|
width: w,
|
|
5289
6420
|
height: h,
|
|
5290
6421
|
boxes: s.boxes,
|
|
@@ -5303,16 +6434,16 @@ async function runImpact(o) {
|
|
|
5303
6434
|
}
|
|
5304
6435
|
|
|
5305
6436
|
// ../visual/src/references.ts
|
|
5306
|
-
import { existsSync as
|
|
6437
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
5307
6438
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
5308
|
-
import { dirname as dirname4, join as
|
|
6439
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
5309
6440
|
async function captureFigmaReferences(o) {
|
|
5310
6441
|
const log = o.log ?? (() => void 0);
|
|
5311
6442
|
const scale = o.scale ?? 2;
|
|
5312
6443
|
const brand2 = o.brand ?? "bridge";
|
|
5313
6444
|
const theme = o.theme ?? "light";
|
|
5314
|
-
if (!
|
|
5315
|
-
const index = JSON.parse(await readFile4(
|
|
6445
|
+
if (!existsSync6(join6(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
|
|
6446
|
+
const index = JSON.parse(await readFile4(join6(o.storybook, "index.json"), "utf8"));
|
|
5316
6447
|
if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
|
|
5317
6448
|
const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
|
|
5318
6449
|
const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
|
|
@@ -5347,12 +6478,12 @@ async function captureFigmaReferences(o) {
|
|
|
5347
6478
|
}
|
|
5348
6479
|
const plan = JSON.stringify(j.plan.render);
|
|
5349
6480
|
await page.evaluate(
|
|
5350
|
-
({ id, plan: plan2 }) => new Promise((
|
|
6481
|
+
({ id, plan: plan2 }) => new Promise((resolve7) => {
|
|
5351
6482
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
5352
6483
|
const done = () => {
|
|
5353
6484
|
clearTimeout(timer);
|
|
5354
6485
|
ch.off("storyRendered", done);
|
|
5355
|
-
|
|
6486
|
+
resolve7();
|
|
5356
6487
|
};
|
|
5357
6488
|
const timer = setTimeout(done, 1e4);
|
|
5358
6489
|
ch.on("storyRendered", done);
|
|
@@ -5375,7 +6506,7 @@ async function captureFigmaReferences(o) {
|
|
|
5375
6506
|
continue;
|
|
5376
6507
|
}
|
|
5377
6508
|
const file = referenceImagePath(j.component, j.key);
|
|
5378
|
-
const path =
|
|
6509
|
+
const path = join6(o.out, file);
|
|
5379
6510
|
await mkdir4(dirname4(path), { recursive: true });
|
|
5380
6511
|
await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
|
|
5381
6512
|
const box = await target.boundingBox();
|
|
@@ -5388,7 +6519,7 @@ async function captureFigmaReferences(o) {
|
|
|
5388
6519
|
await served.close();
|
|
5389
6520
|
}
|
|
5390
6521
|
await mkdir4(o.out, { recursive: true });
|
|
5391
|
-
const manifestPath =
|
|
6522
|
+
const manifestPath = join6(o.out, "manifest.json");
|
|
5392
6523
|
await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
5393
6524
|
`);
|
|
5394
6525
|
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
@@ -5396,10 +6527,10 @@ async function captureFigmaReferences(o) {
|
|
|
5396
6527
|
}
|
|
5397
6528
|
|
|
5398
6529
|
// src/index.ts
|
|
5399
|
-
import { execFileSync as
|
|
6530
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5400
6531
|
|
|
5401
6532
|
// src/request.ts
|
|
5402
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6533
|
+
import { execFileSync as execFileSync2, spawnSync } from "node:child_process";
|
|
5403
6534
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
5404
6535
|
import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
|
|
5405
6536
|
|
|
@@ -5515,7 +6646,7 @@ var defaultIO = () => ({
|
|
|
5515
6646
|
env: process.env,
|
|
5516
6647
|
ghToken() {
|
|
5517
6648
|
try {
|
|
5518
|
-
return
|
|
6649
|
+
return execFileSync2("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim() || void 0;
|
|
5519
6650
|
} catch {
|
|
5520
6651
|
return void 0;
|
|
5521
6652
|
}
|
|
@@ -6312,7 +7443,7 @@ ${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
|
|
|
6312
7443
|
}
|
|
6313
7444
|
|
|
6314
7445
|
// src/figma.ts
|
|
6315
|
-
import { readFileSync as
|
|
7446
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
6316
7447
|
import { resolve as resolve4 } from "node:path";
|
|
6317
7448
|
import { BridgeContext as BridgeContext7, branches as branches3, figma } from "@bridge-ds/mcp";
|
|
6318
7449
|
var FIGMA_HELP = `bridge figma: component changes sent from Figma
|
|
@@ -6379,7 +7510,7 @@ ${r.brief}`);
|
|
|
6379
7510
|
if (!file) throw new RequestUsageError("Usage: bridge figma push <diff.json> [--dry-run]");
|
|
6380
7511
|
let diff2;
|
|
6381
7512
|
try {
|
|
6382
|
-
diff2 = JSON.parse(
|
|
7513
|
+
diff2 = JSON.parse(readFileSync3(resolve4(root, file), "utf8"));
|
|
6383
7514
|
} catch (err) {
|
|
6384
7515
|
throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
|
|
6385
7516
|
}
|
|
@@ -6399,6 +7530,94 @@ ${r.brief}`);
|
|
|
6399
7530
|
}
|
|
6400
7531
|
}
|
|
6401
7532
|
|
|
7533
|
+
// src/snapshot.ts
|
|
7534
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
7535
|
+
import { existsSync as existsSync7, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7536
|
+
import { basename, join as join7, relative as relative4, resolve as resolve5 } from "node:path";
|
|
7537
|
+
var git2 = (root, args) => {
|
|
7538
|
+
try {
|
|
7539
|
+
return execFileSync3("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7540
|
+
} catch {
|
|
7541
|
+
return "";
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7544
|
+
var isReport = (x) => {
|
|
7545
|
+
const r = x;
|
|
7546
|
+
return !!r && typeof r === "object" && r.schemaVersion === 1 && typeof r.generatedAt === "string" && !!r.metrics && Array.isArray(r.files);
|
|
7547
|
+
};
|
|
7548
|
+
function reportFiles(root, entry) {
|
|
7549
|
+
const abs = resolve5(root, entry);
|
|
7550
|
+
if (!existsSync7(abs)) throw new Error(`Audit report not found: ${entry}`);
|
|
7551
|
+
if (!statSync4(abs).isDirectory()) return [abs];
|
|
7552
|
+
return readdirSync3(abs).filter((n) => n.endsWith(".json")).sort().map((n) => join7(abs, n));
|
|
7553
|
+
}
|
|
7554
|
+
async function writeSnapshot(opts) {
|
|
7555
|
+
const rec = recordingSource(fsSource(opts.root));
|
|
7556
|
+
const model = await loadRepoModel(rec.source);
|
|
7557
|
+
const audits = /* @__PURE__ */ new Map();
|
|
7558
|
+
for (const entry of opts.auditReports ?? []) {
|
|
7559
|
+
for (const file of reportFiles(opts.root, entry)) {
|
|
7560
|
+
let data;
|
|
7561
|
+
try {
|
|
7562
|
+
data = JSON.parse(readFileSync4(file, "utf8"));
|
|
7563
|
+
} catch (err) {
|
|
7564
|
+
throw new Error(`${relative4(opts.root, file)} is not valid JSON (${err.message})`);
|
|
7565
|
+
}
|
|
7566
|
+
if (!isReport(data)) continue;
|
|
7567
|
+
const id = auditId(basename(file));
|
|
7568
|
+
audits.set(id, { summary: snapshotAudit(id, data), report: data });
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
for (const spec of opts.audit ?? []) {
|
|
7572
|
+
const [path, label] = spec.split(/=(.*)/s, 2);
|
|
7573
|
+
const abs = resolve5(opts.root, path);
|
|
7574
|
+
if (!existsSync7(abs)) throw new Error(`No such file or folder to audit: ${path}`);
|
|
7575
|
+
const report = runAudit(abs, { ds: opts.root, version: opts.version, ...label ? { label } : {} });
|
|
7576
|
+
const id = auditId(label || relative4(opts.root, abs) || basename(abs));
|
|
7577
|
+
audits.set(id, { summary: snapshotAudit(id, report), report });
|
|
7578
|
+
}
|
|
7579
|
+
const snapshot = buildSnapshot({
|
|
7580
|
+
model,
|
|
7581
|
+
paths: rec.paths,
|
|
7582
|
+
files: rec.files,
|
|
7583
|
+
version: opts.version,
|
|
7584
|
+
...opts.branch ? { branch: opts.branch } : {},
|
|
7585
|
+
...git2(opts.root, ["log", "-1", "--format=%cI"]) ? { committedAt: git2(opts.root, ["log", "-1", "--format=%cI"]) } : {},
|
|
7586
|
+
...opts.generatedAt ? { generatedAt: opts.generatedAt } : {},
|
|
7587
|
+
audits: [...audits.values()].map((a) => a.summary)
|
|
7588
|
+
});
|
|
7589
|
+
const text = `${JSON.stringify(snapshot)}
|
|
7590
|
+
`;
|
|
7591
|
+
const bytes = Buffer.byteLength(text);
|
|
7592
|
+
if (bytes > SNAPSHOT_MAX_BYTES) throw new Error(`${SNAPSHOT_FILE} would be ${(bytes / 1e6).toFixed(1)} MB, over the ${SNAPSHOT_MAX_BYTES / 1e6} MB limit: is tokens.root or components.dir too wide in bridge.config.json?`);
|
|
7593
|
+
rmSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true, force: true });
|
|
7594
|
+
mkdirSync(opts.out, { recursive: true });
|
|
7595
|
+
writeFileSync2(join7(opts.out, SNAPSHOT_FILE), text);
|
|
7596
|
+
writeFileSync2(join7(opts.out, SNAPSHOT_LLMS_FILE), snapshotLlmsTxt(model, snapshot));
|
|
7597
|
+
const written = [SNAPSHOT_FILE, SNAPSHOT_LLMS_FILE];
|
|
7598
|
+
if (audits.size) mkdirSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true });
|
|
7599
|
+
for (const { summary, report } of audits.values()) {
|
|
7600
|
+
writeFileSync2(join7(opts.out, summary.path), `${JSON.stringify(report)}
|
|
7601
|
+
`);
|
|
7602
|
+
written.push(summary.path);
|
|
7603
|
+
}
|
|
7604
|
+
return { snapshot, written, bytes };
|
|
7605
|
+
}
|
|
7606
|
+
function snapshotLines(r, out) {
|
|
7607
|
+
const s = r.snapshot;
|
|
7608
|
+
const c = s.summary.contrast;
|
|
7609
|
+
const kb = Math.max(1, Math.round(r.bytes / 1024));
|
|
7610
|
+
return [
|
|
7611
|
+
`Wrote ${join7(out, SNAPSHOT_FILE)} (${kb} KB) for ${s.repo.owner}/${s.repo.name} at ${s.sourceSha.slice(0, 7)}.`,
|
|
7612
|
+
` tokens ${s.summary.tokens}`,
|
|
7613
|
+
` components ${s.summary.components ?? "not scanned"}`,
|
|
7614
|
+
` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
|
|
7615
|
+
` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
|
|
7616
|
+
` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
|
|
7617
|
+
...s.problems.map((p) => ` problem ${p}`)
|
|
7618
|
+
];
|
|
7619
|
+
}
|
|
7620
|
+
|
|
6402
7621
|
// src/protection.ts
|
|
6403
7622
|
var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
|
|
6404
7623
|
async function protectionReport(opts) {
|
|
@@ -6431,7 +7650,8 @@ async function rolesReport(opts) {
|
|
|
6431
7650
|
}
|
|
6432
7651
|
|
|
6433
7652
|
// src/index.ts
|
|
6434
|
-
import { brand, BridgeContext as BridgeContext8, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson
|
|
7653
|
+
import { brand, BridgeContext as BridgeContext8, CONFIG_FILE as CONFIG_FILE2, core, detectConfig, GitRefSource, history as hist, LocalSource, ops, optionsFromArgv, serveStdio, toConfigJson } from "@bridge-ds/mcp";
|
|
7654
|
+
var VERSION = true ? "0.11.1" : "dev";
|
|
6435
7655
|
var MCP_PACKAGE = "@bridge-ds/mcp";
|
|
6436
7656
|
var HELP = `bridge ${VERSION}: design system as code, for you and your Claude
|
|
6437
7657
|
|
|
@@ -6491,6 +7711,13 @@ Usage:
|
|
|
6491
7711
|
Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
|
|
6492
7712
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
6493
7713
|
Dry run unless --write
|
|
7714
|
+
bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
|
|
7715
|
+
[--branch main] [--json]
|
|
7716
|
+
the repo model of this checkout for hosted Bridge (Studio and Library):
|
|
7717
|
+
snapshot.json (token, contrast and component files, Figma coverage,
|
|
7718
|
+
contrast results) plus audits/<id>.json. --audit runs bridge audit on a
|
|
7719
|
+
product folder; --audit-report carries reports already written.
|
|
7720
|
+
The bridge-data workflow publishes the folder to the bridge-data branch
|
|
6494
7721
|
bridge docs draft <Component> [--json]
|
|
6495
7722
|
docs.json pre-fill from the code (props, tokens, anatomy, contrast,
|
|
6496
7723
|
changelog line, missing keys) and a prompt for Claude to write the
|
|
@@ -6579,9 +7806,9 @@ function printSent(r) {
|
|
|
6579
7806
|
if (r.hint) print(String(r.hint));
|
|
6580
7807
|
}
|
|
6581
7808
|
async function init(a, root) {
|
|
6582
|
-
const cfgPath =
|
|
7809
|
+
const cfgPath = join8(root, CONFIG_FILE2);
|
|
6583
7810
|
let config;
|
|
6584
|
-
if (
|
|
7811
|
+
if (existsSync8(cfgPath) && !has7(a, "force")) {
|
|
6585
7812
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
6586
7813
|
config = (await (await BridgeContext8.create({ root })).config()).config;
|
|
6587
7814
|
} else {
|
|
@@ -6595,11 +7822,11 @@ async function init(a, root) {
|
|
|
6595
7822
|
print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
|
|
6596
7823
|
}
|
|
6597
7824
|
const serverJs = fileURLToPath(new URL("./index.js", import.meta.resolve("@bridge-ds/mcp")));
|
|
6598
|
-
const relServer =
|
|
7825
|
+
const relServer = relative5(root, serverJs);
|
|
6599
7826
|
const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
|
|
6600
7827
|
const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
|
|
6601
|
-
const mcpPath =
|
|
6602
|
-
const existing =
|
|
7828
|
+
const mcpPath = join8(root, ".mcp.json");
|
|
7829
|
+
const existing = existsSync8(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
|
|
6603
7830
|
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
|
|
6604
7831
|
await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
6605
7832
|
`);
|
|
@@ -6631,8 +7858,8 @@ async function check(a, root) {
|
|
|
6631
7858
|
const tight = c.tightest?.[0];
|
|
6632
7859
|
if (tight) print(` tightest: ${tight.pair} [${tight.modes}] ${tight.ratio}:1 (min ${tight.min})`);
|
|
6633
7860
|
} else print(`contrast: ${c.message}`);
|
|
6634
|
-
for (const
|
|
6635
|
-
for (const
|
|
7861
|
+
for (const line2 of protection?.lines ?? []) print(line2);
|
|
7862
|
+
for (const line2 of roles?.lines ?? []) print(line2);
|
|
6636
7863
|
print(failed ? "FAIL" : "OK");
|
|
6637
7864
|
return failed ? 1 : 0;
|
|
6638
7865
|
}
|
|
@@ -6724,7 +7951,7 @@ async function propose(a, root) {
|
|
|
6724
7951
|
edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
|
|
6725
7952
|
}
|
|
6726
7953
|
const editsFile = flag5(a, "edits");
|
|
6727
|
-
if (editsFile) edits.push(...JSON.parse(await readFile7(
|
|
7954
|
+
if (editsFile) edits.push(...JSON.parse(await readFile7(resolve6(editsFile), "utf8")));
|
|
6728
7955
|
if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
|
|
6729
7956
|
const pr = flag5(a, "pr");
|
|
6730
7957
|
const changes = changesFlag(a);
|
|
@@ -6810,19 +8037,19 @@ async function figmaReferences(a, root) {
|
|
|
6810
8037
|
let sha = flag5(a, "sha");
|
|
6811
8038
|
if (!sha) {
|
|
6812
8039
|
try {
|
|
6813
|
-
sha =
|
|
8040
|
+
sha = execFileSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
6814
8041
|
} catch {
|
|
6815
8042
|
sha = "unknown";
|
|
6816
8043
|
}
|
|
6817
8044
|
}
|
|
6818
8045
|
const only = (flag5(a, "only") ?? "").split(",").filter(Boolean);
|
|
6819
8046
|
const r = await captureFigmaReferences({
|
|
6820
|
-
storybook:
|
|
6821
|
-
specs:
|
|
6822
|
-
out:
|
|
8047
|
+
storybook: resolve6(root, flag5(a, "storybook") ?? "storybook-static"),
|
|
8048
|
+
specs: resolve6(root, flag5(a, "specs") ?? "packages/react/component-specs.json"),
|
|
8049
|
+
out: resolve6(root, flag5(a, "out") ?? "figma-references"),
|
|
6823
8050
|
sha,
|
|
6824
8051
|
...only.length ? { only } : {},
|
|
6825
|
-
log: has7(a, "json") ? void 0 : (
|
|
8052
|
+
log: has7(a, "json") ? void 0 : (line2) => print(line2)
|
|
6826
8053
|
});
|
|
6827
8054
|
if (has7(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
6828
8055
|
return 0;
|
|
@@ -6842,10 +8069,10 @@ async function impact(a, root) {
|
|
|
6842
8069
|
root,
|
|
6843
8070
|
base: flag5(a, "base") ?? "main",
|
|
6844
8071
|
head: flag5(a, "head") ?? "HEAD",
|
|
6845
|
-
...out ? { out:
|
|
8072
|
+
...out ? { out: resolve6(out) } : {},
|
|
6846
8073
|
...only.length ? { only } : {},
|
|
6847
8074
|
...threshold !== void 0 ? { threshold } : {},
|
|
6848
|
-
...cacheDir ? { cacheDir:
|
|
8075
|
+
...cacheDir ? { cacheDir: resolve6(cacheDir) } : {},
|
|
6849
8076
|
...maxStories ? { maxStoriesPerComponent: maxStories } : {},
|
|
6850
8077
|
...imageBase !== void 0 ? { imageBase } : {},
|
|
6851
8078
|
...artifactUrl ? { artifactUrl } : {},
|
|
@@ -6937,8 +8164,8 @@ ${r.suggestion?.body ?? r.prBody}`);
|
|
|
6937
8164
|
async function audit(a, root) {
|
|
6938
8165
|
const target = a._[1];
|
|
6939
8166
|
if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
|
|
6940
|
-
if (!
|
|
6941
|
-
const dsRoot = flag5(a, "ds") ?
|
|
8167
|
+
if (!existsSync8(target)) throw new UsageError(`No such file or folder: ${target}`);
|
|
8168
|
+
const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
6942
8169
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
6943
8170
|
const ds = loadDesignSystem(dsRoot);
|
|
6944
8171
|
if (has7(a, "fix")) {
|
|
@@ -6952,12 +8179,12 @@ async function audit(a, root) {
|
|
|
6952
8179
|
const baselinePath = flag5(a, "baseline");
|
|
6953
8180
|
let baseline;
|
|
6954
8181
|
if (baselinePath) {
|
|
6955
|
-
if (!
|
|
8182
|
+
if (!existsSync8(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
|
|
6956
8183
|
baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
|
|
6957
8184
|
}
|
|
6958
8185
|
const label = flag5(a, "label");
|
|
6959
8186
|
const allowlistPath = flag5(a, "allowlist");
|
|
6960
|
-
if (allowlistPath && !
|
|
8187
|
+
if (allowlistPath && !existsSync8(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
6961
8188
|
let allowlist;
|
|
6962
8189
|
try {
|
|
6963
8190
|
allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
|
|
@@ -7027,6 +8254,17 @@ async function audit(a, root) {
|
|
|
7027
8254
|
}
|
|
7028
8255
|
return code;
|
|
7029
8256
|
}
|
|
8257
|
+
async function snapshotCmd(a, root) {
|
|
8258
|
+
const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
|
|
8259
|
+
const branch = flag5(a, "branch");
|
|
8260
|
+
const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {} });
|
|
8261
|
+
if (has7(a, "json")) print(JSON.stringify({ out, written: r.written, bytes: r.bytes, sourceSha: r.snapshot.sourceSha, summary: r.snapshot.summary, audits: r.snapshot.audits.map((x) => x.path), problems: r.snapshot.problems }, null, 2));
|
|
8262
|
+
else {
|
|
8263
|
+
const shown = relative5(process.cwd(), out);
|
|
8264
|
+
for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
|
|
8265
|
+
}
|
|
8266
|
+
return 0;
|
|
8267
|
+
}
|
|
7030
8268
|
async function docsCmd(a, root) {
|
|
7031
8269
|
const [, sub, name] = a._;
|
|
7032
8270
|
if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
|
|
@@ -7063,7 +8301,7 @@ async function main() {
|
|
|
7063
8301
|
const argv = process.argv.slice(2);
|
|
7064
8302
|
const a = parse(argv);
|
|
7065
8303
|
const cmd = a._[0];
|
|
7066
|
-
const root =
|
|
8304
|
+
const root = resolve6(flag5(a, "root") ?? process.cwd());
|
|
7067
8305
|
if (cmd === "request") return runRequest(a, root);
|
|
7068
8306
|
if (cmd === "explore") return runExplore(a, root);
|
|
7069
8307
|
if (cmd === "branch") return runBranch(a, root);
|
|
@@ -7074,11 +8312,11 @@ async function main() {
|
|
|
7074
8312
|
if (cmd === "review") return runReview(a, root);
|
|
7075
8313
|
if (cmd === "products" || cmd === "settings" || cmd === "coverage") return runStudioRead(cmd, a, root);
|
|
7076
8314
|
if (cmd === "docs" && a._[1] === "save") return runDocsSave(a, root);
|
|
8315
|
+
if (cmd === "version" || !cmd && has7(a, "version")) return print(VERSION), 0;
|
|
7077
8316
|
if (!cmd || has7(a, "help") || cmd === "help") {
|
|
7078
8317
|
print(HELP);
|
|
7079
8318
|
return cmd || has7(a, "help") ? 0 : 2;
|
|
7080
8319
|
}
|
|
7081
|
-
if (cmd === "--version" || cmd === "version") return print(VERSION), 0;
|
|
7082
8320
|
switch (cmd) {
|
|
7083
8321
|
case "init":
|
|
7084
8322
|
await init(a, root);
|
|
@@ -7106,6 +8344,8 @@ async function main() {
|
|
|
7106
8344
|
return audit(a, root);
|
|
7107
8345
|
case "docs":
|
|
7108
8346
|
return docsCmd(a, root);
|
|
8347
|
+
case "snapshot":
|
|
8348
|
+
return snapshotCmd(a, root);
|
|
7109
8349
|
case "mcp":
|
|
7110
8350
|
await serveStdio(optionsFromArgv(argv.slice(1)));
|
|
7111
8351
|
return -1;
|