@bridge-ds/cli 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1408 -130
- 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: {} });
|
|
@@ -1813,10 +1813,10 @@ ${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
|
1813
1813
|
out.themeRefs += r.themeRefs;
|
|
1814
1814
|
return;
|
|
1815
1815
|
}
|
|
1816
|
-
const
|
|
1817
|
-
if (
|
|
1818
|
-
const body =
|
|
1819
|
-
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart +
|
|
1816
|
+
const str2 = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
1817
|
+
if (str2) {
|
|
1818
|
+
const body = str2[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
1819
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str2[1].length + 1, source: "style" });
|
|
1820
1820
|
out.findings.push(...r.findings);
|
|
1821
1821
|
out.tokenUsages += r.tokenUsages;
|
|
1822
1822
|
out.themeRefs += r.themeRefs;
|
|
@@ -2176,8 +2176,8 @@ function parseOwnerEntry(raw) {
|
|
|
2176
2176
|
var teamKey = (org, slug) => `@${org.toLowerCase()}/${slug.toLowerCase()}`;
|
|
2177
2177
|
function ownerEntryProblems(raw) {
|
|
2178
2178
|
if (raw === void 0) return [];
|
|
2179
|
-
const
|
|
2180
|
-
if (!
|
|
2179
|
+
const isObj3 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
|
|
2180
|
+
if (!isObj3(raw)) return ['"governance.owners" must be { "design": [people or teams], "tech": [people or teams] }'];
|
|
2181
2181
|
const out = [];
|
|
2182
2182
|
for (const [k, v] of Object.entries(raw)) {
|
|
2183
2183
|
if (k !== "design" && k !== "tech") {
|
|
@@ -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 },
|
|
@@ -2562,11 +2562,13 @@ function defineConfig(input) {
|
|
|
2562
2562
|
minReasonLength: g.override.minReasonLength ?? DEFAULT_OVERRIDE.minReasonLength
|
|
2563
2563
|
}
|
|
2564
2564
|
} : {},
|
|
2565
|
-
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
2565
|
+
...g.rights ? { rights: normalizeRights(g.rights) } : {},
|
|
2566
|
+
...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {}
|
|
2566
2567
|
},
|
|
2567
2568
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
2568
2569
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
2569
2570
|
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
2571
|
+
...input.library?.public === true ? { library: { public: true } } : {},
|
|
2570
2572
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
2571
2573
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
2572
2574
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -2611,7 +2613,7 @@ function loadConfig(source) {
|
|
|
2611
2613
|
if (!isObj(tokens2)) problems.push('"tokens" must be an object');
|
|
2612
2614
|
else
|
|
2613
2615
|
for (const [k, v] of Object.entries(tokens2)) {
|
|
2614
|
-
if (FLAT_TOKEN_KEYS.includes(k) || k === "root") {
|
|
2616
|
+
if (FLAT_TOKEN_KEYS.includes(k) || k === "root" || k === "usage") {
|
|
2615
2617
|
if (!isStr(v)) problems.push(`"tokens.${k}" must be a non-empty string`);
|
|
2616
2618
|
} else if (k === "include" || k === "exclude") {
|
|
2617
2619
|
if (!isStrList(v)) problems.push(`"tokens.${k}" must be a list of globs`);
|
|
@@ -2686,6 +2688,7 @@ function loadConfig(source) {
|
|
|
2686
2688
|
}
|
|
2687
2689
|
}
|
|
2688
2690
|
problems.push(...rightsProblems(gov.rights));
|
|
2691
|
+
if (gov.soloOwnerCanApproveBoth !== void 0 && typeof gov.soloOwnerCanApproveBoth !== "boolean") problems.push('"governance.soloOwnerCanApproveBoth" must be true or false');
|
|
2689
2692
|
}
|
|
2690
2693
|
}
|
|
2691
2694
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
@@ -2696,6 +2699,14 @@ function loadConfig(source) {
|
|
|
2696
2699
|
else if (!/^https:\/\/[^\s/]+/.test(sb.url.trim())) problems.push('"storybook.url" must start with https://, for example https://storybook.example.com');
|
|
2697
2700
|
else for (const k of Object.keys(sb)) if (k !== "url") problems.push(`"storybook.${k}" is not a known key`);
|
|
2698
2701
|
}
|
|
2702
|
+
const lib = d.library;
|
|
2703
|
+
if (lib !== void 0) {
|
|
2704
|
+
if (!isObj(lib)) problems.push('"library" must be an object, for example { "public": true }');
|
|
2705
|
+
else {
|
|
2706
|
+
if (lib.public !== void 0 && typeof lib.public !== "boolean") problems.push('"library.public" must be true or false');
|
|
2707
|
+
for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2699
2710
|
problems.push(...productProblems(d.products));
|
|
2700
2711
|
const audit2 = d.audit;
|
|
2701
2712
|
if (audit2 !== void 0) {
|
|
@@ -2725,6 +2736,7 @@ function loadConfig(source) {
|
|
|
2725
2736
|
if (problems.length) throw new ConfigError(problems);
|
|
2726
2737
|
return defineConfig(d);
|
|
2727
2738
|
}
|
|
2739
|
+
var tokensPath = (config, rel = "") => [config.tokens.root === "." ? "" : config.tokens.root, rel].filter(Boolean).join("/");
|
|
2728
2740
|
|
|
2729
2741
|
// ../core/src/node/git-local.ts
|
|
2730
2742
|
import { execFile } from "node:child_process";
|
|
@@ -2750,10 +2762,10 @@ function assertSafeBranch(branch, rules = {}) {
|
|
|
2750
2762
|
|
|
2751
2763
|
// ../core/src/node/git-local.ts
|
|
2752
2764
|
function git(root, args, opts = {}) {
|
|
2753
|
-
return new Promise((
|
|
2765
|
+
return new Promise((resolve7, reject) => {
|
|
2754
2766
|
const child = execFile("git", args, { cwd: root, env: { ...process.env, ...opts.env }, maxBuffer: 32 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
2755
2767
|
if (err) reject(new Error(`git ${args.join(" ")}: ${stderr || err.message}`.trim()));
|
|
2756
|
-
else
|
|
2768
|
+
else resolve7(stdout);
|
|
2757
2769
|
});
|
|
2758
2770
|
if (opts.input !== void 0) child.stdin.end(opts.input);
|
|
2759
2771
|
});
|
|
@@ -3032,6 +3044,44 @@ function classifyAuth(status, detail, opts = {}) {
|
|
|
3032
3044
|
}
|
|
3033
3045
|
return {};
|
|
3034
3046
|
}
|
|
3047
|
+
function memoryEtagStore(max = 500) {
|
|
3048
|
+
const map = /* @__PURE__ */ new Map();
|
|
3049
|
+
return {
|
|
3050
|
+
get(key) {
|
|
3051
|
+
const e = map.get(key);
|
|
3052
|
+
if (e) {
|
|
3053
|
+
map.delete(key);
|
|
3054
|
+
map.set(key, e);
|
|
3055
|
+
}
|
|
3056
|
+
return e;
|
|
3057
|
+
},
|
|
3058
|
+
set(key, entry) {
|
|
3059
|
+
map.delete(key);
|
|
3060
|
+
map.set(key, entry);
|
|
3061
|
+
while (map.size > max) map.delete(map.keys().next().value);
|
|
3062
|
+
},
|
|
3063
|
+
clear: () => map.clear()
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
var sharedEtags = memoryEtagStore();
|
|
3067
|
+
function tokenFingerprint(token) {
|
|
3068
|
+
if (!token) return "anon";
|
|
3069
|
+
let a = 2166136261;
|
|
3070
|
+
let b = 16777619 ^ token.length;
|
|
3071
|
+
for (let i = 0; i < token.length; i++) {
|
|
3072
|
+
const c = token.charCodeAt(i);
|
|
3073
|
+
a = Math.imul(a ^ c, 16777619) >>> 0;
|
|
3074
|
+
b = Math.imul(b ^ c ^ i & 255, 1540483477) >>> 0;
|
|
3075
|
+
}
|
|
3076
|
+
return a.toString(16).padStart(8, "0") + b.toString(16).padStart(8, "0");
|
|
3077
|
+
}
|
|
3078
|
+
function installationFor(list2, owner) {
|
|
3079
|
+
const inst = list2.find((i) => i.account?.login?.toLowerCase() === owner.toLowerCase());
|
|
3080
|
+
if (!inst) return { reach: "no" };
|
|
3081
|
+
const selection = inst.repositorySelection ?? inst.repository_selection;
|
|
3082
|
+
return selection === "all" ? { reach: "yes", id: inst.id } : { reach: "check", id: inst.id };
|
|
3083
|
+
}
|
|
3084
|
+
var MAX_PAGES = 10;
|
|
3035
3085
|
function rateLimitMessage(x) {
|
|
3036
3086
|
const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
|
|
3037
3087
|
if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
|
|
@@ -3039,8 +3089,11 @@ function rateLimitMessage(x) {
|
|
|
3039
3089
|
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.`;
|
|
3040
3090
|
}
|
|
3041
3091
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
3092
|
+
var hourlyLimitErrors = /* @__PURE__ */ new WeakSet();
|
|
3042
3093
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
3043
3094
|
var API = "https://api.github.com";
|
|
3095
|
+
var ACCEPT_JSON = "application/vnd.github+json";
|
|
3096
|
+
var ACCEPT_RAW = "application/vnd.github.raw";
|
|
3044
3097
|
var RAW = "https://raw.githubusercontent.com";
|
|
3045
3098
|
function parseRepo(repo) {
|
|
3046
3099
|
const m = /^\s*(?:https?:\/\/github\.com\/|git@github\.com:)?([A-Za-z0-9-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?\/?\s*$/.exec(repo);
|
|
@@ -3059,6 +3112,7 @@ function base64ToUtf8(b64) {
|
|
|
3059
3112
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
3060
3113
|
return new TextDecoder("utf-8").decode(bytes);
|
|
3061
3114
|
}
|
|
3115
|
+
var copy = (value) => value === null || typeof value !== "object" ? value : typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
3062
3116
|
var encodeRef = (ref) => ref.split("/").map(encodeURIComponent).join("/");
|
|
3063
3117
|
var GitHubClient = class _GitHubClient {
|
|
3064
3118
|
constructor(repo, token, fetchImpl = (url, init2) => fetch(url, init2), retry = DEFAULT_RETRY, options = {}) {
|
|
@@ -3071,6 +3125,8 @@ var GitHubClient = class _GitHubClient {
|
|
|
3071
3125
|
this.name = r.name;
|
|
3072
3126
|
this.api = (options.apiUrl ?? API).replace(/\/$/, "");
|
|
3073
3127
|
this.protectedBranches = options.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
3128
|
+
this.etags = options.etags === false ? void 0 : options.etags ?? sharedEtags;
|
|
3129
|
+
this.etagScope = options.etagScope ?? tokenFingerprint(token);
|
|
3074
3130
|
}
|
|
3075
3131
|
token;
|
|
3076
3132
|
fetchImpl;
|
|
@@ -3081,6 +3137,15 @@ var GitHubClient = class _GitHubClient {
|
|
|
3081
3137
|
blobCache = /* @__PURE__ */ new Map();
|
|
3082
3138
|
api;
|
|
3083
3139
|
protectedBranches;
|
|
3140
|
+
etags;
|
|
3141
|
+
etagScope;
|
|
3142
|
+
rate;
|
|
3143
|
+
/** What the conditional request cache did for this client. */
|
|
3144
|
+
etagStats = { conditional: 0, notModified: 0, stale: 0 };
|
|
3145
|
+
/** GitHub's rate-limit headers from the last API response, or undefined before any response carried them. */
|
|
3146
|
+
get rateLimit() {
|
|
3147
|
+
return this.rate;
|
|
3148
|
+
}
|
|
3084
3149
|
/** "owner/name". */
|
|
3085
3150
|
get fullName() {
|
|
3086
3151
|
return `${this.owner}/${this.name}`;
|
|
@@ -3104,45 +3169,77 @@ var GitHubClient = class _GitHubClient {
|
|
|
3104
3169
|
get repoPath() {
|
|
3105
3170
|
return `/repos/${this.owner}/${this.name}`;
|
|
3106
3171
|
}
|
|
3107
|
-
async request(method, path, body, retryable = method === "GET") {
|
|
3172
|
+
async request(method, path, body, retryable = method === "GET", opts = {}) {
|
|
3108
3173
|
const attempts = retryable ? Math.max(1, this.retry.attempts) : 1;
|
|
3109
3174
|
for (let attempt = 1; ; attempt++) {
|
|
3110
3175
|
try {
|
|
3111
|
-
return await this.once(method, path, body);
|
|
3176
|
+
return await this.once(method, path, body, opts);
|
|
3112
3177
|
} catch (err) {
|
|
3113
3178
|
const e = err;
|
|
3114
|
-
if (attempt >= attempts || !(e instanceof GitHubError) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
3179
|
+
if (attempt >= attempts || !(e instanceof GitHubError) || hourlyLimitErrors.has(e) || !isTransient(e.status, e.detail ?? "")) throw err;
|
|
3115
3180
|
await this.retry.sleep(this.retry.baseDelayMs * 2 ** (attempt - 1));
|
|
3116
3181
|
}
|
|
3117
3182
|
}
|
|
3118
3183
|
}
|
|
3119
|
-
|
|
3184
|
+
noteRateLimit(headers) {
|
|
3185
|
+
const remaining = headers?.get("x-ratelimit-remaining");
|
|
3186
|
+
if (remaining == null) return;
|
|
3187
|
+
const limit = headers?.get("x-ratelimit-limit");
|
|
3188
|
+
const reset = headers?.get("x-ratelimit-reset");
|
|
3189
|
+
this.rate = {
|
|
3190
|
+
limit: limit != null ? Number(limit) : void 0,
|
|
3191
|
+
remaining: Number(remaining),
|
|
3192
|
+
resetsAt: reset ? new Date(Number(reset) * 1e3) : void 0,
|
|
3193
|
+
resource: headers?.get("x-ratelimit-resource") ?? void 0
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
async once(method, path, body, opts = {}) {
|
|
3197
|
+
const accept = opts.accept ?? ACCEPT_JSON;
|
|
3120
3198
|
const headers = {
|
|
3121
|
-
Accept:
|
|
3199
|
+
Accept: accept,
|
|
3122
3200
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
3123
3201
|
};
|
|
3124
3202
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
3125
3203
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
3204
|
+
const key = method === "GET" && this.etags && opts.etag !== false ? `${this.etagScope} ${accept} ${this.api}${path}` : void 0;
|
|
3205
|
+
const cached = key ? this.etags.get(key) : void 0;
|
|
3206
|
+
if (cached && this.token) {
|
|
3207
|
+
headers["If-None-Match"] = cached.etag;
|
|
3208
|
+
this.etagStats.conditional++;
|
|
3209
|
+
}
|
|
3126
3210
|
let res;
|
|
3127
3211
|
try {
|
|
3128
3212
|
res = await this.fetchImpl(`${this.api}${path}`, { method, headers, ...body !== void 0 ? { body: JSON.stringify(body) } : {} });
|
|
3129
3213
|
} catch (err) {
|
|
3214
|
+
if (err instanceof GitHubError) throw err;
|
|
3130
3215
|
throw new GitHubError(0, "Could not reach GitHub. Check your connection, then try again.", err.message);
|
|
3131
3216
|
}
|
|
3217
|
+
this.noteRateLimit(res.headers);
|
|
3218
|
+
if (res.status === 304 && cached && key && this.token) {
|
|
3219
|
+
this.etagStats.notModified++;
|
|
3220
|
+
this.etags.set(key, { ...cached, at: Date.now() });
|
|
3221
|
+
return copy(cached.body);
|
|
3222
|
+
}
|
|
3132
3223
|
if (res.status === 204) return void 0;
|
|
3133
3224
|
if (!res.ok) {
|
|
3134
3225
|
let detail = "";
|
|
3135
3226
|
try {
|
|
3136
|
-
const
|
|
3137
|
-
detail = [
|
|
3227
|
+
const data2 = await res.json();
|
|
3228
|
+
detail = [data2.message, ...(data2.errors ?? []).map((e) => typeof e === "string" ? e : e.message)].filter(Boolean).join(" ");
|
|
3138
3229
|
} catch {
|
|
3139
3230
|
detail = "";
|
|
3140
3231
|
}
|
|
3141
3232
|
const remaining = res.headers?.get("x-ratelimit-remaining");
|
|
3142
3233
|
const reset = res.headers?.get("x-ratelimit-reset");
|
|
3143
3234
|
if ((res.status === 403 || res.status === 429) && remaining === "0") {
|
|
3235
|
+
if (cached && this.options.serveStaleOnRateLimit) {
|
|
3236
|
+
this.etagStats.stale++;
|
|
3237
|
+
return copy(cached.body);
|
|
3238
|
+
}
|
|
3144
3239
|
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
3145
|
-
|
|
3240
|
+
const limited = new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
3241
|
+
hourlyLimitErrors.add(limited);
|
|
3242
|
+
throw limited;
|
|
3146
3243
|
}
|
|
3147
3244
|
const info = classifyAuth(res.status, detail, {
|
|
3148
3245
|
path,
|
|
@@ -3151,7 +3248,24 @@ var GitHubClient = class _GitHubClient {
|
|
|
3151
3248
|
});
|
|
3152
3249
|
throw new GitHubError(res.status, friendlyMessage(res.status, detail, info), detail, info);
|
|
3153
3250
|
}
|
|
3154
|
-
|
|
3251
|
+
const data = opts.text ? await res.text() : await res.json();
|
|
3252
|
+
const etag = key ? res.headers?.get("etag") : null;
|
|
3253
|
+
if (key && etag) this.etags.set(key, { etag, body: copy(data), at: Date.now() });
|
|
3254
|
+
return data;
|
|
3255
|
+
}
|
|
3256
|
+
/**
|
|
3257
|
+
* Every item of a paged listing (`per_page=100`), at most 10 pages. `pick` takes the items out of one page (the
|
|
3258
|
+
* installation endpoints wrap them in an object with `total_count`).
|
|
3259
|
+
*/
|
|
3260
|
+
async paged(path, pick) {
|
|
3261
|
+
const out = [];
|
|
3262
|
+
const sep3 = path.includes("?") ? "&" : "?";
|
|
3263
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
3264
|
+
const { items, total } = pick(await this.request("GET", `${path}${sep3}per_page=100&page=${page}`));
|
|
3265
|
+
out.push(...items);
|
|
3266
|
+
if (items.length < 100 || total !== void 0 && out.length >= total) break;
|
|
3267
|
+
}
|
|
3268
|
+
return out;
|
|
3155
3269
|
}
|
|
3156
3270
|
async getRepo() {
|
|
3157
3271
|
const r = await this.request(
|
|
@@ -3534,6 +3648,14 @@ var GitHubClient = class _GitHubClient {
|
|
|
3534
3648
|
*/
|
|
3535
3649
|
async readFile(path, ref) {
|
|
3536
3650
|
if (this.token) return this.getFileText(path, ref);
|
|
3651
|
+
return this.readRawFile(path, ref);
|
|
3652
|
+
}
|
|
3653
|
+
/**
|
|
3654
|
+
* Text of a file from the raw host (raw.githubusercontent.com), sent without any header so the browser makes a
|
|
3655
|
+
* simple CORS request (no preflight) and nothing counts against the API limit. Works for public repositories only:
|
|
3656
|
+
* a private file answers 404 there, like a missing one. Undefined on 404.
|
|
3657
|
+
*/
|
|
3658
|
+
async readRawFile(path, ref) {
|
|
3537
3659
|
const url = `${(this.options.rawUrl ?? RAW).replace(/\/$/, "")}/${this.owner}/${this.name}/${encodeRef(ref)}/${encodeRef(path)}`;
|
|
3538
3660
|
let res;
|
|
3539
3661
|
try {
|
|
@@ -3545,6 +3667,76 @@ var GitHubClient = class _GitHubClient {
|
|
|
3545
3667
|
if (!res.ok) throw new GitHubError(res.status, friendlyMessage(res.status, ""), `raw ${path}@${ref}`);
|
|
3546
3668
|
return res.text();
|
|
3547
3669
|
}
|
|
3670
|
+
/**
|
|
3671
|
+
* 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
|
|
3672
|
+
* host cannot take the token without a preflight). Counts against the API limit; conditional when cached.
|
|
3673
|
+
* Undefined when the file does not exist.
|
|
3674
|
+
*/
|
|
3675
|
+
async getRawText(path, ref) {
|
|
3676
|
+
try {
|
|
3677
|
+
return await this.request("GET", `${this.repoPath}/contents/${encodeRef(path)}?ref=${encodeURIComponent(ref)}`, void 0, true, { accept: ACCEPT_RAW, text: true });
|
|
3678
|
+
} catch (err) {
|
|
3679
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
3680
|
+
throw err;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* 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
|
|
3685
|
+
* listed by a tree (the raw host refuses any request that carries a token). A blob sha names its content, so the
|
|
3686
|
+
* answer is cached for the client's life and never revalidated. Undefined on 404.
|
|
3687
|
+
*/
|
|
3688
|
+
async getBlobRaw(sha) {
|
|
3689
|
+
const cached = this.blobCache.get(sha);
|
|
3690
|
+
if (cached !== void 0) return cached;
|
|
3691
|
+
try {
|
|
3692
|
+
const text = await this.request("GET", `${this.repoPath}/git/blobs/${encodeURIComponent(sha)}`, void 0, true, { accept: ACCEPT_RAW, text: true, etag: false });
|
|
3693
|
+
this.blobCache.set(sha, text);
|
|
3694
|
+
return text;
|
|
3695
|
+
} catch (err) {
|
|
3696
|
+
if (err instanceof GitHubError && err.status === 404) return void 0;
|
|
3697
|
+
throw err;
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
// --- GitHub App installations (user-to-server tokens) ---------------------------------------
|
|
3701
|
+
/**
|
|
3702
|
+
* Installations of the signing-in GitHub App that the signed-in user can reach (GET /user/installations, every
|
|
3703
|
+
* page). Needs a user-to-server token of that same App: a personal or OAuth token is refused (403), and each App
|
|
3704
|
+
* sees only its own installations. GitHub lists only installations on accounts and repositories the user has
|
|
3705
|
+
* explicit permission on: a public repository where the user is not a collaborator is never listed, installed or
|
|
3706
|
+
* not. (GET /repos/{o}/{r}/installation would say, but it needs the App's private key, which Bridge never holds.)
|
|
3707
|
+
*/
|
|
3708
|
+
async listUserInstallations() {
|
|
3709
|
+
this.assertToken("Reading where Bridge is installed");
|
|
3710
|
+
const list2 = await this.paged("/user/installations", (p) => ({ items: p.installations ?? [], total: p.total_count }));
|
|
3711
|
+
return list2.map((i) => ({
|
|
3712
|
+
id: i.id,
|
|
3713
|
+
account: i.account?.login ? { login: i.account.login, type: i.account.type ?? "User" } : null,
|
|
3714
|
+
repositorySelection: i.repository_selection ?? "selected",
|
|
3715
|
+
appSlug: i.app_slug
|
|
3716
|
+
}));
|
|
3717
|
+
}
|
|
3718
|
+
/** Repositories of one installation the signed-in user can reach (GET /user/installations/{id}/repositories, every page). */
|
|
3719
|
+
async listInstallationRepos(installationId) {
|
|
3720
|
+
this.assertToken("Reading where Bridge is installed");
|
|
3721
|
+
const list2 = await this.paged(`/user/installations/${encodeURIComponent(String(installationId))}/repositories`, (p) => ({
|
|
3722
|
+
items: p.repositories ?? [],
|
|
3723
|
+
total: p.total_count
|
|
3724
|
+
}));
|
|
3725
|
+
return list2.map((r) => ({ owner: r.owner?.login ?? r.full_name.split("/")[0], name: r.name, fullName: r.full_name, private: Boolean(r.private) }));
|
|
3726
|
+
}
|
|
3727
|
+
/**
|
|
3728
|
+
* The App installation that gives the signed-in user this repository (this client's by default): its id, or null
|
|
3729
|
+
* when the App is not installed there or the user cannot reach it. One call when the installation covers all of
|
|
3730
|
+
* the owner's repositories, two (paged) when it covers selected ones.
|
|
3731
|
+
*/
|
|
3732
|
+
async findInstallation(owner = this.owner, repo = this.name) {
|
|
3733
|
+
const found = installationFor(await this.listUserInstallations(), owner);
|
|
3734
|
+
if (found.reach === "no") return null;
|
|
3735
|
+
if (found.reach === "yes") return { id: found.id, selection: "all" };
|
|
3736
|
+
const repos = await this.listInstallationRepos(found.id);
|
|
3737
|
+
const hit = repos.some((r) => r.owner.toLowerCase() === owner.toLowerCase() && r.name.toLowerCase() === repo.toLowerCase());
|
|
3738
|
+
return hit ? { id: found.id, selection: "selected" } : null;
|
|
3739
|
+
}
|
|
3548
3740
|
/** The repository's default branch. */
|
|
3549
3741
|
async getDefaultBranch() {
|
|
3550
3742
|
return (await this.getRepo()).defaultBranch;
|
|
@@ -3922,14 +4114,101 @@ var SUGGESTION_APPLY_EXIT = {
|
|
|
3922
4114
|
refused: 1
|
|
3923
4115
|
};
|
|
3924
4116
|
|
|
4117
|
+
// ../core/src/node/fs-source.ts
|
|
4118
|
+
import { execFileSync } from "node:child_process";
|
|
4119
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
|
|
4120
|
+
import { join as join2 } from "node:path";
|
|
4121
|
+
|
|
4122
|
+
// ../core/src/repo-source.ts
|
|
4123
|
+
function normalizeRepoPath(path) {
|
|
4124
|
+
return path.replace(/\\/g, "/").split("/").filter((s) => s && s !== ".").join("/");
|
|
4125
|
+
}
|
|
4126
|
+
var parentDir = (path) => {
|
|
4127
|
+
const p = normalizeRepoPath(path);
|
|
4128
|
+
const i = p.lastIndexOf("/");
|
|
4129
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
4130
|
+
};
|
|
4131
|
+
async function readRepoTexts(source, paths, concurrency = 4, onError = (_path, err) => {
|
|
4132
|
+
throw err;
|
|
4133
|
+
}, onProgress) {
|
|
4134
|
+
const unique = [...new Set(paths)];
|
|
4135
|
+
const out = /* @__PURE__ */ new Map();
|
|
4136
|
+
onProgress?.(0, unique.length);
|
|
4137
|
+
let next = 0;
|
|
4138
|
+
const worker = async () => {
|
|
4139
|
+
while (next < unique.length) {
|
|
4140
|
+
const path = unique[next++];
|
|
4141
|
+
let body;
|
|
4142
|
+
try {
|
|
4143
|
+
body = await source.text(path);
|
|
4144
|
+
} catch (err) {
|
|
4145
|
+
body = onError(path, err);
|
|
4146
|
+
}
|
|
4147
|
+
out.set(path, body);
|
|
4148
|
+
onProgress?.(out.size, unique.length);
|
|
4149
|
+
}
|
|
4150
|
+
};
|
|
4151
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, unique.length)) }, worker));
|
|
4152
|
+
return out;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
// ../core/src/node/fs-source.ts
|
|
4156
|
+
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
4157
|
+
function gitOut(root, args) {
|
|
4158
|
+
try {
|
|
4159
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
4160
|
+
} catch {
|
|
4161
|
+
return "";
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function localRepoRef(root) {
|
|
4165
|
+
const origin = /github\.com[:/]([\w.-]+)\/([\w.-]+?)(\.git)?$/.exec(gitOut(root, ["remote", "get-url", "origin"]));
|
|
4166
|
+
return { owner: origin?.[1] ?? "", repo: origin?.[2] ?? "", sha: gitOut(root, ["rev-parse", "HEAD"]) || "working-tree" };
|
|
4167
|
+
}
|
|
4168
|
+
function fsSource(root, ref = {}) {
|
|
4169
|
+
let resolved;
|
|
4170
|
+
const walk2 = (rel, out) => {
|
|
4171
|
+
for (const entry of readdirSync2(join2(root, rel), { withFileTypes: true })) {
|
|
4172
|
+
if (SKIP.has(entry.name)) continue;
|
|
4173
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
4174
|
+
if (entry.isDirectory()) walk2(path, out);
|
|
4175
|
+
else if (entry.isFile()) out.push(path);
|
|
4176
|
+
else if (entry.isSymbolicLink() && statSync2(join2(root, path), { throwIfNoEntry: false })?.isFile()) out.push(path);
|
|
4177
|
+
}
|
|
4178
|
+
};
|
|
4179
|
+
return {
|
|
4180
|
+
get ref() {
|
|
4181
|
+
resolved ??= { ...localRepoRef(root), ...Object.fromEntries(Object.entries(ref).filter(([, v]) => v !== void 0)) };
|
|
4182
|
+
return resolved;
|
|
4183
|
+
},
|
|
4184
|
+
async list(prefix = "") {
|
|
4185
|
+
const rel = normalizeRepoPath(prefix);
|
|
4186
|
+
const abs = join2(root, rel);
|
|
4187
|
+
if (!existsSync2(abs)) return [];
|
|
4188
|
+
if (!statSync2(abs).isDirectory()) return [rel];
|
|
4189
|
+
const out = [];
|
|
4190
|
+
walk2(rel, out);
|
|
4191
|
+
return out.sort();
|
|
4192
|
+
},
|
|
4193
|
+
async text(path) {
|
|
4194
|
+
const abs = join2(root, normalizeRepoPath(path));
|
|
4195
|
+
try {
|
|
4196
|
+
return statSync2(abs).isFile() ? readFileSync2(abs, "utf8") : null;
|
|
4197
|
+
} catch {
|
|
4198
|
+
return null;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
|
|
3925
4204
|
// src/index.ts
|
|
3926
4205
|
import { fileURLToPath } from "node:url";
|
|
3927
4206
|
|
|
3928
4207
|
// ../visual/src/run.ts
|
|
3929
|
-
import { existsSync as
|
|
4208
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
3930
4209
|
import { mkdir as mkdir3, readFile as readFile3, rm as rm2, unlink, writeFile as writeFile3 } from "node:fs/promises";
|
|
3931
4210
|
import { tmpdir } from "node:os";
|
|
3932
|
-
import { join as
|
|
4211
|
+
import { join as join5, relative as relative3, resolve as resolve2 } from "node:path";
|
|
3933
4212
|
|
|
3934
4213
|
// ../core/src/protection.ts
|
|
3935
4214
|
var PROTECTION_PARTS = ["review", "checks", "history", "deletion"];
|
|
@@ -4251,6 +4530,894 @@ function planReferences(file) {
|
|
|
4251
4530
|
);
|
|
4252
4531
|
}
|
|
4253
4532
|
|
|
4533
|
+
// ../core/src/storybook.ts
|
|
4534
|
+
var StorybookIndexError = class extends Error {
|
|
4535
|
+
constructor(code, message, status) {
|
|
4536
|
+
super(message);
|
|
4537
|
+
this.code = code;
|
|
4538
|
+
this.status = status;
|
|
4539
|
+
this.name = "StorybookIndexError";
|
|
4540
|
+
}
|
|
4541
|
+
code;
|
|
4542
|
+
status;
|
|
4543
|
+
};
|
|
4544
|
+
var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
4545
|
+
var str = (v) => typeof v === "string" && v.trim() ? v : void 0;
|
|
4546
|
+
function entryFrom(key, raw, v3) {
|
|
4547
|
+
if (!isObj2(raw)) return null;
|
|
4548
|
+
const id = str(raw.id) ?? key;
|
|
4549
|
+
const title = str(raw.title) ?? str(raw.kind);
|
|
4550
|
+
const name = str(raw.name) ?? str(raw.story);
|
|
4551
|
+
if (!id || !title || !name) return null;
|
|
4552
|
+
const params = isObj2(raw.parameters) ? raw.parameters : {};
|
|
4553
|
+
const type = raw.type === "docs" || raw.type === "story" ? raw.type : v3 && params.docsOnly === true ? "docs" : "story";
|
|
4554
|
+
const e = {
|
|
4555
|
+
id,
|
|
4556
|
+
title,
|
|
4557
|
+
name,
|
|
4558
|
+
type,
|
|
4559
|
+
importPath: str(raw.importPath) ?? str(params.fileName) ?? "",
|
|
4560
|
+
tags: Array.isArray(raw.tags) ? raw.tags.filter((t) => typeof t === "string") : []
|
|
4561
|
+
};
|
|
4562
|
+
const subtype = str(raw.subtype);
|
|
4563
|
+
if (subtype) e.subtype = subtype;
|
|
4564
|
+
const exportName2 = str(raw.exportName);
|
|
4565
|
+
if (exportName2) e.exportName = exportName2;
|
|
4566
|
+
const componentPath = str(raw.componentPath);
|
|
4567
|
+
if (componentPath) e.componentPath = componentPath;
|
|
4568
|
+
return e;
|
|
4569
|
+
}
|
|
4570
|
+
function groupStorybookComponents(entries) {
|
|
4571
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
4572
|
+
for (const e of entries) {
|
|
4573
|
+
let c = byTitle.get(e.title);
|
|
4574
|
+
if (!c) {
|
|
4575
|
+
const parts = e.title.split("/").map((p) => p.trim()).filter(Boolean);
|
|
4576
|
+
c = { title: e.title, name: parts[parts.length - 1] ?? e.title, group: parts.slice(0, -1), stories: [], docs: [], tags: [] };
|
|
4577
|
+
byTitle.set(e.title, c);
|
|
4578
|
+
}
|
|
4579
|
+
if (e.type === "docs") c.docs.push(e);
|
|
4580
|
+
else if (e.subtype !== "test") c.stories.push(e);
|
|
4581
|
+
for (const t of e.tags) if (!c.tags.includes(t)) c.tags.push(t);
|
|
4582
|
+
}
|
|
4583
|
+
const out = [...byTitle.values()].filter((c) => c.stories.length || c.docs.length);
|
|
4584
|
+
for (const c of out) c.tags.sort();
|
|
4585
|
+
return out.sort((a, b) => a.title.localeCompare(b.title));
|
|
4586
|
+
}
|
|
4587
|
+
function parseStorybookIndex(json, source) {
|
|
4588
|
+
if (!isObj2(json)) throw new StorybookIndexError("not-json", "The Storybook index is not a JSON object.");
|
|
4589
|
+
if (typeof json.loginUrl === "string" && !("entries" in json) && !("stories" in json)) {
|
|
4590
|
+
throw new StorybookIndexError("private", "This Storybook is private: it asks for a sign-in before showing its index.");
|
|
4591
|
+
}
|
|
4592
|
+
const map = isObj2(json.entries) ? json.entries : isObj2(json.stories) ? json.stories : null;
|
|
4593
|
+
if (!map) throw new StorybookIndexError("empty", "This file has no Storybook entries.");
|
|
4594
|
+
const v3 = !isObj2(json.entries);
|
|
4595
|
+
const entries = Object.entries(map).map(([k, raw]) => entryFrom(k, raw, v3)).filter((e) => e !== null);
|
|
4596
|
+
if (!entries.length) throw new StorybookIndexError("empty", "This Storybook index lists no stories or docs.");
|
|
4597
|
+
const version = typeof json.v === "number" ? json.v : v3 ? 3 : 4;
|
|
4598
|
+
return { version, source: source ?? (v3 ? "stories.json" : "index.json"), entries, components: groupStorybookComponents(entries) };
|
|
4599
|
+
}
|
|
4600
|
+
var pairs = (o) => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}:${encodeURIComponent(v)}`).join(";");
|
|
4601
|
+
function storybookIframeUrl(base, id, o = {}) {
|
|
4602
|
+
let url = `${base.replace(/\/+$/, "")}/iframe.html?id=${encodeURIComponent(id)}&viewMode=${o.viewMode ?? "story"}`;
|
|
4603
|
+
if (o.globals && Object.keys(o.globals).length) url += `&globals=${pairs(o.globals)}`;
|
|
4604
|
+
if (o.args && Object.keys(o.args).length) url += `&args=${pairs(o.args)}`;
|
|
4605
|
+
return url;
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
// ../core/src/token-rows.ts
|
|
4609
|
+
var FALLBACK_BRANDS = ["bridge", "ocean"];
|
|
4610
|
+
var FALLBACK_THEMES = ["light", "dark"];
|
|
4611
|
+
var FALLBACK_DENSITIES = ["comfortable", "compact"];
|
|
4612
|
+
function tokenRowAxes(config, readText = () => void 0) {
|
|
4613
|
+
const brandAxis = config.axes.brand;
|
|
4614
|
+
const brands = brandAxis?.values.length ? [...brandAxis.values] : [...FALLBACK_BRANDS];
|
|
4615
|
+
const defaultBrand = brandAxis?.default ?? brands[0];
|
|
4616
|
+
const themes = config.modes.themes.length ? [...config.modes.themes] : [...FALLBACK_THEMES];
|
|
4617
|
+
const densities = config.modes.densities.length ? [...config.modes.densities] : [...FALLBACK_DENSITIES];
|
|
4618
|
+
const files = tokenRowFiles(config);
|
|
4619
|
+
const brandName = (b) => {
|
|
4620
|
+
try {
|
|
4621
|
+
const text = readText(files.brandFile(b));
|
|
4622
|
+
const name = text ? JSON.parse(text).$extensions?.["dev.bridge.brand"]?.name : void 0;
|
|
4623
|
+
if (typeof name === "string" && name) return name;
|
|
4624
|
+
} catch {
|
|
4625
|
+
}
|
|
4626
|
+
return b.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4627
|
+
};
|
|
4628
|
+
return {
|
|
4629
|
+
brands,
|
|
4630
|
+
defaultBrand,
|
|
4631
|
+
brandLabels: Object.fromEntries(brands.map((b) => [b, brandName(b)])),
|
|
4632
|
+
themes,
|
|
4633
|
+
densities,
|
|
4634
|
+
defaultDensity: config.modes.defaults.density ?? densities[0]
|
|
4635
|
+
};
|
|
4636
|
+
}
|
|
4637
|
+
function tokenRowFiles(config) {
|
|
4638
|
+
const t = config.tokens;
|
|
4639
|
+
const at = (rel) => tokensPath(config, rel);
|
|
4640
|
+
return {
|
|
4641
|
+
primitivesDir: at(t.primitives),
|
|
4642
|
+
semanticDir: at(t.semantic),
|
|
4643
|
+
brandsDir: at(t.brands),
|
|
4644
|
+
densityDir: at(t.density),
|
|
4645
|
+
componentsDir: at(t.components),
|
|
4646
|
+
baseFile: at(`${t.semantic}/${t.base}.tokens.json`),
|
|
4647
|
+
semanticFile: (theme) => at(`${t.semantic}/${theme}.tokens.json`),
|
|
4648
|
+
brandFile: (brand2) => at(`${t.brands}/${brand2}.tokens.json`),
|
|
4649
|
+
overrideFile: (brand2, theme) => at(`${t.brands}/${brand2}.${theme}.tokens.json`),
|
|
4650
|
+
densityFile: (density) => at(`${t.density}/${density}.tokens.json`)
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
function flattenTokenFile(node, file, path = [], inheritedType = "") {
|
|
4654
|
+
const type = node.$type ?? inheritedType;
|
|
4655
|
+
if ("$value" in node) {
|
|
4656
|
+
return [
|
|
4657
|
+
{
|
|
4658
|
+
path,
|
|
4659
|
+
type,
|
|
4660
|
+
value: node.$value,
|
|
4661
|
+
description: node.$description,
|
|
4662
|
+
deprecated: node.$deprecated,
|
|
4663
|
+
file
|
|
4664
|
+
}
|
|
4665
|
+
];
|
|
4666
|
+
}
|
|
4667
|
+
const out = [];
|
|
4668
|
+
for (const [key, child] of Object.entries(node)) {
|
|
4669
|
+
if (key.startsWith("$") || typeof child !== "object" || child === null) continue;
|
|
4670
|
+
out.push(...flattenTokenFile(child, file, [...path, key], type));
|
|
4671
|
+
}
|
|
4672
|
+
return out;
|
|
4673
|
+
}
|
|
4674
|
+
var toHex22 = (n) => Math.round(n * 255).toString(16).padStart(2, "0");
|
|
4675
|
+
function colorCss(c) {
|
|
4676
|
+
if (typeof c === "string") return { css: c, hex: /^#[0-9a-f]{6}$/i.test(c) ? c.toLowerCase() : void 0 };
|
|
4677
|
+
const v = c;
|
|
4678
|
+
const [r = 0, g = 0, b = 0] = v.components;
|
|
4679
|
+
const hex = (v.hex ?? `#${[r, g, b].map(toHex22).join("")}`).toLowerCase();
|
|
4680
|
+
const alpha = v.alpha ?? 1;
|
|
4681
|
+
if (alpha < 1) return { css: `rgb(${[r, g, b].map((x) => Math.round(x * 255)).join(" ")} / ${alpha})`, hex };
|
|
4682
|
+
return { css: hex, hex };
|
|
4683
|
+
}
|
|
4684
|
+
var dimCss = (d) => typeof d === "object" && d !== null ? `${d.value}${d.unit}` : String(d);
|
|
4685
|
+
function shadowCss(s) {
|
|
4686
|
+
if (typeof s === "string") return s;
|
|
4687
|
+
const layers = Array.isArray(s) ? s : [s];
|
|
4688
|
+
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(", ");
|
|
4689
|
+
}
|
|
4690
|
+
function tokenValueCss(type, value) {
|
|
4691
|
+
switch (type) {
|
|
4692
|
+
case "color":
|
|
4693
|
+
return colorCss(value);
|
|
4694
|
+
case "dimension":
|
|
4695
|
+
case "duration":
|
|
4696
|
+
return { css: dimCss(value) };
|
|
4697
|
+
case "shadow":
|
|
4698
|
+
return { css: shadowCss(value) };
|
|
4699
|
+
case "cubicBezier":
|
|
4700
|
+
return { css: Array.isArray(value) ? `cubic-bezier(${value.join(", ")})` : String(value) };
|
|
4701
|
+
case "fontFamily":
|
|
4702
|
+
return { css: Array.isArray(value) ? value.map((n) => /\s/.test(n) ? `"${n}"` : n).join(", ") : String(value) };
|
|
4703
|
+
default:
|
|
4704
|
+
return { css: String(value) };
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
var tokenRowCssVar = (segments) => `--${segments.join("-")}`;
|
|
4708
|
+
var tokenRowFigmaName = (segments) => segments.join("/");
|
|
4709
|
+
var isAlias2 = (v) => typeof v === "string" && /^\{[^}]+\}$/.test(v);
|
|
4710
|
+
function buildTokenRows(config, readText, listFiles) {
|
|
4711
|
+
const axes = tokenRowAxes(config, readText);
|
|
4712
|
+
const { brands: BRANDS2, defaultBrand: DEFAULT_BRAND, themes: MODES, densities: DENSITIES, defaultDensity: DEFAULT_DENSITY } = axes;
|
|
4713
|
+
const files = tokenRowFiles(config);
|
|
4714
|
+
const REF_THEME = MODES[0];
|
|
4715
|
+
const jsonFiles = (dir) => listFiles(dir).filter((f) => f.endsWith(".tokens.json")).sort();
|
|
4716
|
+
const flatCache = /* @__PURE__ */ new Map();
|
|
4717
|
+
const flatFile = (f) => {
|
|
4718
|
+
if (!flatCache.has(f)) {
|
|
4719
|
+
const text = readText(f);
|
|
4720
|
+
flatCache.set(f, text === void 0 ? [] : flattenTokenFile(JSON.parse(text), f));
|
|
4721
|
+
}
|
|
4722
|
+
return flatCache.get(f);
|
|
4723
|
+
};
|
|
4724
|
+
const byPathOf = (tokens2) => new Map(tokens2.map((t) => [t.path.join("."), t]));
|
|
4725
|
+
const primitiveFiles = jsonFiles(files.primitivesDir);
|
|
4726
|
+
const componentFiles = jsonFiles(files.componentsDir);
|
|
4727
|
+
const sources = [
|
|
4728
|
+
...primitiveFiles,
|
|
4729
|
+
...jsonFiles(files.brandsDir),
|
|
4730
|
+
...MODES.map(files.semanticFile),
|
|
4731
|
+
files.baseFile,
|
|
4732
|
+
...jsonFiles(files.densityDir),
|
|
4733
|
+
...componentFiles
|
|
4734
|
+
];
|
|
4735
|
+
const primitives = primitiveFiles.flatMap((f) => flatFile(f)).map((t) => {
|
|
4736
|
+
const { css, hex } = tokenValueCss(t.type, t.value);
|
|
4737
|
+
return {
|
|
4738
|
+
path: t.path.join("."),
|
|
4739
|
+
segments: t.path,
|
|
4740
|
+
type: t.type,
|
|
4741
|
+
css,
|
|
4742
|
+
hex,
|
|
4743
|
+
cssVar: tokenRowCssVar(t.path),
|
|
4744
|
+
figma: tokenRowFigmaName(t.path),
|
|
4745
|
+
description: t.description,
|
|
4746
|
+
deprecated: t.deprecated,
|
|
4747
|
+
raw: t.value,
|
|
4748
|
+
file: t.file
|
|
4749
|
+
};
|
|
4750
|
+
});
|
|
4751
|
+
const primByPath = new Map(primitives.map((p) => [p.path, p]));
|
|
4752
|
+
const brandRaw = Object.fromEntries(BRANDS2.map((b) => [b, flatFile(files.brandFile(b))]));
|
|
4753
|
+
const themeRaw = Object.fromEntries(MODES.map((m) => [m, flatFile(files.semanticFile(m))]));
|
|
4754
|
+
const baseRaw = flatFile(files.baseFile);
|
|
4755
|
+
const densityRaw = Object.fromEntries(DENSITIES.map((d) => [d, flatFile(files.densityFile(d))]));
|
|
4756
|
+
const componentRaw = componentFiles.flatMap((f) => flatFile(f));
|
|
4757
|
+
const layerCache = /* @__PURE__ */ new Map();
|
|
4758
|
+
const layers = (b, m, d) => {
|
|
4759
|
+
const k = `${b}.${m}.${d}`;
|
|
4760
|
+
if (!layerCache.has(k))
|
|
4761
|
+
layerCache.set(k, [
|
|
4762
|
+
byPathOf(flatFile(files.overrideFile(b, m))),
|
|
4763
|
+
byPathOf(themeRaw[m] ?? []),
|
|
4764
|
+
byPathOf(baseRaw),
|
|
4765
|
+
byPathOf(densityRaw[d] ?? []),
|
|
4766
|
+
byPathOf(componentRaw),
|
|
4767
|
+
byPathOf(brandRaw[b] ?? [])
|
|
4768
|
+
]);
|
|
4769
|
+
return layerCache.get(k);
|
|
4770
|
+
};
|
|
4771
|
+
const resolve7 = (b, m, d, value, type) => {
|
|
4772
|
+
const chain = [];
|
|
4773
|
+
let current = value;
|
|
4774
|
+
let currentType = type;
|
|
4775
|
+
let guard = 0;
|
|
4776
|
+
while (isAlias2(current) && guard++ < 30) {
|
|
4777
|
+
const target = current.slice(1, -1);
|
|
4778
|
+
chain.push(target);
|
|
4779
|
+
const prim = primByPath.get(target);
|
|
4780
|
+
if (prim) return { alias: chain[0] ?? null, chain, css: prim.css, hex: prim.hex };
|
|
4781
|
+
const hit = layers(b, m, d).map((l) => l.get(target)).find(Boolean);
|
|
4782
|
+
if (!hit) throw new Error(`tokens: alias {${target}} does not resolve in ${b} ${m} ${d}`);
|
|
4783
|
+
current = hit.value;
|
|
4784
|
+
currentType = hit.type || currentType;
|
|
4785
|
+
}
|
|
4786
|
+
const { css, hex } = tokenValueCss(currentType, current);
|
|
4787
|
+
return { alias: chain[0] ?? null, chain, css, hex };
|
|
4788
|
+
};
|
|
4789
|
+
const same2 = (a, c) => a.css === c.css && a.chain.join() === c.chain.join();
|
|
4790
|
+
const build = (t, tier, valueFor) => {
|
|
4791
|
+
const at = (b, m, d = DEFAULT_DENSITY) => {
|
|
4792
|
+
const raw = valueFor(b, m, d) ?? t;
|
|
4793
|
+
return resolve7(b, m, d, raw.value, raw.type || t.type);
|
|
4794
|
+
};
|
|
4795
|
+
const brands = Object.fromEntries(BRANDS2.map((b) => [b, Object.fromEntries(MODES.map((m) => [m, at(b, m)]))]));
|
|
4796
|
+
return {
|
|
4797
|
+
path: t.path.join("."),
|
|
4798
|
+
segments: t.path,
|
|
4799
|
+
type: t.type,
|
|
4800
|
+
cssVar: tokenRowCssVar(t.path),
|
|
4801
|
+
figma: tokenRowFigmaName(t.path),
|
|
4802
|
+
description: t.description,
|
|
4803
|
+
group: t.path[0] === "color" ? t.path.slice(0, 2).join(".") : t.path[0],
|
|
4804
|
+
deprecated: t.deprecated,
|
|
4805
|
+
tier,
|
|
4806
|
+
file: t.file,
|
|
4807
|
+
modes: brands[DEFAULT_BRAND],
|
|
4808
|
+
brands,
|
|
4809
|
+
densities: tier === "density" ? Object.fromEntries(DENSITIES.map((d) => [d, at(DEFAULT_BRAND, REF_THEME, d)])) : void 0,
|
|
4810
|
+
brandSpecific: BRANDS2.some((b) => MODES.some((m) => !same2(brands[b][m], brands[DEFAULT_BRAND][m])))
|
|
4811
|
+
};
|
|
4812
|
+
};
|
|
4813
|
+
const brand2 = (brandRaw[DEFAULT_BRAND] ?? []).map((t) => build(t, "brand", (b) => byPathOf(brandRaw[b] ?? []).get(t.path.join("."))));
|
|
4814
|
+
const themedBy = Object.fromEntries(MODES.map((m) => [m, byPathOf(themeRaw[m] ?? [])]));
|
|
4815
|
+
const themed = (themeRaw[REF_THEME] ?? []).map((t) => {
|
|
4816
|
+
const path = t.path.join(".");
|
|
4817
|
+
const others = MODES.slice(1).map((m) => {
|
|
4818
|
+
const hit = themedBy[m].get(path);
|
|
4819
|
+
if (!hit) throw new Error(`tokens: semantic token ${path} is missing in ${files.semanticFile(m)}`);
|
|
4820
|
+
return hit;
|
|
4821
|
+
});
|
|
4822
|
+
const s = build(t, "semantic", (b, m) => byPathOf(flatFile(files.overrideFile(b, m))).get(path) ?? (m === REF_THEME ? t : themedBy[m].get(path)));
|
|
4823
|
+
s.description = [t, ...others].map((x) => x.description).find((x) => x !== void 0);
|
|
4824
|
+
s.deprecated = [t, ...others].map((x) => x.deprecated).find((x) => x !== void 0);
|
|
4825
|
+
s.file = MODES.map(files.semanticFile).join(", ");
|
|
4826
|
+
return s;
|
|
4827
|
+
});
|
|
4828
|
+
const base = baseRaw.map((t) => build(t, "semantic", () => t));
|
|
4829
|
+
const semantic = [...themed, ...base];
|
|
4830
|
+
const density = (densityRaw[DEFAULT_DENSITY] ?? []).map((t) => build(t, "density", (_b, _m, d) => byPathOf(densityRaw[d] ?? []).get(t.path.join("."))));
|
|
4831
|
+
const component = componentRaw.map((t) => build(t, "component", () => t));
|
|
4832
|
+
return { ...axes, primitives, semantic, brand: brand2, density, component, all: [...brand2, ...semantic, ...density, ...component], sources };
|
|
4833
|
+
}
|
|
4834
|
+
function tokenRowIndex(rows) {
|
|
4835
|
+
const map = /* @__PURE__ */ new Map();
|
|
4836
|
+
for (const p of rows.primitives) map.set(p.cssVar, { path: p.path, kind: "primitive", type: p.type });
|
|
4837
|
+
for (const s of rows.all) map.set(s.cssVar, { path: s.path, kind: s.tier, type: s.type });
|
|
4838
|
+
return map;
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
// ../core/src/contrast-model.ts
|
|
4842
|
+
var CONTRAST_THRESHOLDS = { text: { aa: 4.5, aaa: 7 }, ui: { aa: 3, aaa: null } };
|
|
4843
|
+
var channel = (c) => {
|
|
4844
|
+
const s = c / 255;
|
|
4845
|
+
return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
4846
|
+
};
|
|
4847
|
+
function hexLuminance(hex) {
|
|
4848
|
+
const n = parseInt(hex.slice(1), 16);
|
|
4849
|
+
return 0.2126 * channel(n >> 16 & 255) + 0.7152 * channel(n >> 8 & 255) + 0.0722 * channel(n & 255);
|
|
4850
|
+
}
|
|
4851
|
+
function hexContrastRatio(a, b) {
|
|
4852
|
+
const [hi, lo] = [hexLuminance(a), hexLuminance(b)].sort((x, y) => y - x);
|
|
4853
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
4854
|
+
}
|
|
4855
|
+
function parseContrastSpecs(text) {
|
|
4856
|
+
if (!text) return [];
|
|
4857
|
+
const data = JSON.parse(text);
|
|
4858
|
+
return (data.pairs ?? []).map((p) => {
|
|
4859
|
+
const [fg, bg, min] = Array.isArray(p) ? p : [p.fg, p.bg, p.min];
|
|
4860
|
+
return { fg, bg, min, usage: min >= CONTRAST_THRESHOLDS.text.aa ? "text" : "ui" };
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
function evaluateContrastPairs(specs, rows) {
|
|
4864
|
+
const byPath = new Map(rows.all.map((s) => [s.path, s]));
|
|
4865
|
+
return specs.map((spec) => {
|
|
4866
|
+
const fgToken = byPath.get(spec.fg);
|
|
4867
|
+
const bgToken = byPath.get(spec.bg);
|
|
4868
|
+
if (!fgToken || !bgToken) throw new Error(`contrast: pair ${spec.fg} on ${spec.bg} references a token that does not exist`);
|
|
4869
|
+
const t = CONTRAST_THRESHOLDS[spec.usage];
|
|
4870
|
+
const result = (b, m) => {
|
|
4871
|
+
const fgHex = fgToken.brands[b][m].hex;
|
|
4872
|
+
const bgHex = bgToken.brands[b][m].hex;
|
|
4873
|
+
const r = hexContrastRatio(fgHex, bgHex);
|
|
4874
|
+
return { fgHex, bgHex, ratio: Math.floor(r * 100) / 100, aa: r >= spec.min, aaa: t.aaa === null ? null : r >= t.aaa };
|
|
4875
|
+
};
|
|
4876
|
+
const brands = Object.fromEntries(rows.brands.map((b) => [b, Object.fromEntries(rows.themes.map((m) => [m, result(b, m)]))]));
|
|
4877
|
+
return { ...spec, fgToken, bgToken, modes: brands[rows.defaultBrand], brands };
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
// ../core/src/component-scan.ts
|
|
4882
|
+
var componentSlug = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
4883
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4884
|
+
function parseComponentFamilies(indexSrc, componentsRel = "components") {
|
|
4885
|
+
const out = [];
|
|
4886
|
+
const exportRe = new RegExp(`export \\* from '\\./${escapeRe(componentsRel)}/(\\w+)'`);
|
|
4887
|
+
for (const line2 of indexSrc.split("\n")) {
|
|
4888
|
+
const head = line2.match(/^\/\/\s*([A-Z][\w ]+)$/);
|
|
4889
|
+
if (head) out.push({ name: head[1].trim(), members: [] });
|
|
4890
|
+
const exp = line2.match(exportRe);
|
|
4891
|
+
if (exp && out.length) out[out.length - 1].members.push(exp[1]);
|
|
4892
|
+
}
|
|
4893
|
+
return out.filter((f) => f.members.length);
|
|
4894
|
+
}
|
|
4895
|
+
var NATIVE = {
|
|
4896
|
+
HTMLButtonElement: "button",
|
|
4897
|
+
HTMLInputElement: "input",
|
|
4898
|
+
HTMLSpanElement: "span",
|
|
4899
|
+
HTMLDivElement: "div",
|
|
4900
|
+
HTMLAnchorElement: "a"
|
|
4901
|
+
};
|
|
4902
|
+
function parseExtends(clause) {
|
|
4903
|
+
const el = clause.match(/<(HTML\w+Element)>/)?.[1];
|
|
4904
|
+
const omitted = [...(clause.match(/Omit<[^,]+,\s*([^>]+)>/)?.[1] ?? "").matchAll(/'([^']+)'/g)].map((m) => m[1]);
|
|
4905
|
+
return { inherits: el ? NATIVE[el] ?? el : void 0, omitted };
|
|
4906
|
+
}
|
|
4907
|
+
function parseComponentTsx(name, src) {
|
|
4908
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
4909
|
+
for (const m of src.matchAll(/export type (\w+)\s*=\s*([^;]+);/g)) aliases.set(m[1], m[2].trim());
|
|
4910
|
+
let props = [];
|
|
4911
|
+
let inherits;
|
|
4912
|
+
let omitted = [];
|
|
4913
|
+
const iface = src.match(new RegExp(`export interface ${name}Props(?:\\s+extends\\s+([^{]+))?\\s*\\{([\\s\\S]*?)\\n\\}`));
|
|
4914
|
+
if (iface) {
|
|
4915
|
+
({ inherits, omitted } = parseExtends(iface[1] ?? ""));
|
|
4916
|
+
for (const m of iface[2].matchAll(/(?:\/\*\*([\s\S]*?)\*\/\s*)?(\w+)(\?)?:\s*([^;]+);/g)) {
|
|
4917
|
+
const rawType = m[4].trim();
|
|
4918
|
+
props.push({
|
|
4919
|
+
name: m[2],
|
|
4920
|
+
type: aliases.get(rawType) ?? rawType,
|
|
4921
|
+
required: !m[3],
|
|
4922
|
+
description: m[1]?.replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim() || void 0
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
} else {
|
|
4926
|
+
const alias = aliases.get(`${name}Props`);
|
|
4927
|
+
if (alias) ({ inherits, omitted } = parseExtends(alias));
|
|
4928
|
+
}
|
|
4929
|
+
const sig = src.match(new RegExp(`function ${name}\\(\\s*\\{([^}]*)\\}`));
|
|
4930
|
+
if (sig) {
|
|
4931
|
+
for (const m of sig[1].matchAll(/(\w+)\s*=\s*('[^']*'|[\w.]+)/g)) {
|
|
4932
|
+
const p = props.find((x) => x.name === m[1]);
|
|
4933
|
+
if (p) p.defaultValue = m[2];
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
const docBefore = (re) => {
|
|
4937
|
+
const idx = src.search(re);
|
|
4938
|
+
if (idx < 0) return void 0;
|
|
4939
|
+
const before = src.slice(0, idx).trimEnd();
|
|
4940
|
+
if (!before.endsWith("*/")) return void 0;
|
|
4941
|
+
const start = before.lastIndexOf("/*", before.length - 2);
|
|
4942
|
+
if (start < 0 || before[start + 2] !== "*") return void 0;
|
|
4943
|
+
return before.slice(start + 3, -2).replace(/^\s*\*\s?/gm, "").replace(/\s+/g, " ").trim();
|
|
4944
|
+
};
|
|
4945
|
+
const description = docBefore(new RegExp(`export (?:function|const) ${name}\\b`));
|
|
4946
|
+
const subcomponents = [...src.matchAll(/export function (\w+)/g)].map((m) => m[1]).filter((n) => n !== name);
|
|
4947
|
+
props = props.sort((a, b) => Number(b.required) - Number(a.required));
|
|
4948
|
+
return { props, inherits, omitted, description, subcomponents };
|
|
4949
|
+
}
|
|
4950
|
+
var stateOf = (selector) => {
|
|
4951
|
+
const states = [
|
|
4952
|
+
[/:focus-visible/, "focus"],
|
|
4953
|
+
[/:hover/, "hover"],
|
|
4954
|
+
[/:disabled(?!,)|^[^(]*:disabled/, "disabled"],
|
|
4955
|
+
[/\[data-loading\]/, "loading"],
|
|
4956
|
+
[/::placeholder/, "placeholder"]
|
|
4957
|
+
];
|
|
4958
|
+
const hit = states.filter(([re]) => re.test(selector)).map(([, s]) => s);
|
|
4959
|
+
return hit.length ? `, ${hit.join(" + ")}` : "";
|
|
4960
|
+
};
|
|
4961
|
+
var BASE_UI_RUNTIME = /^--(_|accordion-panel-|collapsible-panel-|active-tab-|anchor-|available-|drawer-swipe-|toast-(frontmost-height|height|index|offset-y|swipe-)|transform-origin)/;
|
|
4962
|
+
function parseComponentCss(src, labels, describe) {
|
|
4963
|
+
const clean = src.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
4964
|
+
const rules = [];
|
|
4965
|
+
const walk2 = (text, prefix = "") => {
|
|
4966
|
+
let i = 0;
|
|
4967
|
+
let buf = "";
|
|
4968
|
+
while (i < text.length) {
|
|
4969
|
+
const ch = text[i];
|
|
4970
|
+
if (ch === "{") {
|
|
4971
|
+
const head = buf.trim();
|
|
4972
|
+
buf = "";
|
|
4973
|
+
let depth = 1;
|
|
4974
|
+
const start = ++i;
|
|
4975
|
+
while (i < text.length && depth > 0) {
|
|
4976
|
+
if (text[i] === "{") depth++;
|
|
4977
|
+
else if (text[i] === "}") depth--;
|
|
4978
|
+
i++;
|
|
4979
|
+
}
|
|
4980
|
+
const inner = text.slice(start, i - 1);
|
|
4981
|
+
if (head.startsWith("@keyframes")) continue;
|
|
4982
|
+
if (head.startsWith("@")) {
|
|
4983
|
+
walk2(inner, `${head} `);
|
|
4984
|
+
continue;
|
|
4985
|
+
}
|
|
4986
|
+
const names = [...new Set([...inner.matchAll(/var\((--[\w-]+)/g)].map((m) => m[1]))].filter((n) => !BASE_UI_RUNTIME.test(n));
|
|
4987
|
+
if (!names.length) continue;
|
|
4988
|
+
const className = head.match(/\.([\w-]+)/)?.[1] ?? head;
|
|
4989
|
+
rules.push({
|
|
4990
|
+
selector: prefix + head.replace(/\s+/g, " "),
|
|
4991
|
+
className,
|
|
4992
|
+
label: (labels[className] ?? className) + stateOf(head),
|
|
4993
|
+
vars: names.map((n) => {
|
|
4994
|
+
const d = describe(n);
|
|
4995
|
+
return { name: n, href: d?.href, known: !!d, path: d?.path, kind: d?.kind };
|
|
4996
|
+
})
|
|
4997
|
+
});
|
|
4998
|
+
} else {
|
|
4999
|
+
buf += ch;
|
|
5000
|
+
i++;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
};
|
|
5004
|
+
walk2(clean);
|
|
5005
|
+
return rules;
|
|
5006
|
+
}
|
|
5007
|
+
function parseComponentStories(src) {
|
|
5008
|
+
const title = src.match(/title:\s*'([^']+)'/)?.[1];
|
|
5009
|
+
if (!title) return [];
|
|
5010
|
+
const base = title.split("/").map((s) => componentSlug(s.trim()).replace(/\s+/g, "-")).join("-");
|
|
5011
|
+
return [...src.matchAll(/export const (\w+):\s*Story/g)].map((m) => ({ name: m[1], id: `${base}--${componentSlug(m[1])}` }));
|
|
5012
|
+
}
|
|
5013
|
+
function resolveInheritedTokens(list2) {
|
|
5014
|
+
const byName = new Map(list2.map((c) => [c.name, c]));
|
|
5015
|
+
for (const c of list2) {
|
|
5016
|
+
const seen = /* @__PURE__ */ new Set([c.name]);
|
|
5017
|
+
const queue = [...c.reuses ?? []];
|
|
5018
|
+
const inherited = [];
|
|
5019
|
+
const taken = new Set(c.tokenPaths);
|
|
5020
|
+
while (queue.length) {
|
|
5021
|
+
const from = byName.get(queue.shift());
|
|
5022
|
+
if (!from || seen.has(from.name)) continue;
|
|
5023
|
+
seen.add(from.name);
|
|
5024
|
+
const paths = from.tokenPaths.filter((p) => !taken.has(p));
|
|
5025
|
+
paths.forEach((p) => taken.add(p));
|
|
5026
|
+
inherited.push({ from: from.name, slug: from.slug, paths });
|
|
5027
|
+
queue.push(...from.reuses ?? []);
|
|
5028
|
+
}
|
|
5029
|
+
c.inherited = inherited;
|
|
5030
|
+
c.allTokenPaths = [...taken].sort();
|
|
5031
|
+
}
|
|
5032
|
+
for (const c of list2) delete c.reuses;
|
|
5033
|
+
}
|
|
5034
|
+
function scanComponents(input) {
|
|
5035
|
+
const { dir, readText, describeVar } = input;
|
|
5036
|
+
const docsSuffix = input.docsSuffix ?? ".docs.json";
|
|
5037
|
+
const styleSuffix = input.styleSuffix ?? ".module.css";
|
|
5038
|
+
const fileSet = new Set(input.files);
|
|
5039
|
+
const has8 = (p) => fileSet.has(p);
|
|
5040
|
+
const fam = input.indexText === void 0 ? [] : parseComponentFamilies(input.indexText, input.indexRel ?? "components");
|
|
5041
|
+
const familyOf = (name) => fam.find((f) => f.members.includes(name))?.name;
|
|
5042
|
+
const names = [
|
|
5043
|
+
...new Set(
|
|
5044
|
+
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])
|
|
5045
|
+
)
|
|
5046
|
+
].sort();
|
|
5047
|
+
const reuseRe = new RegExp(`from\\s+'\\.\\.\\/(\\w+)\\/(\\w+)${escapeRe(styleSuffix)}'`, "g");
|
|
5048
|
+
const result = names.map((name) => {
|
|
5049
|
+
const folder = `${dir}/${name}`;
|
|
5050
|
+
const files = { tsx: `${folder}/${name}.tsx`, css: `${folder}/${name}${styleSuffix}`, stories: `${folder}/${name}.stories.tsx`, docs: `${folder}/${name}${docsSuffix}` };
|
|
5051
|
+
let docs = null;
|
|
5052
|
+
if (has8(files.docs)) {
|
|
5053
|
+
try {
|
|
5054
|
+
docs = JSON.parse(readText(files.docs) ?? "");
|
|
5055
|
+
} catch (err) {
|
|
5056
|
+
input.problems?.push(`${files.docs}: ${err.message}`);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
const tsxSrc = readText(files.tsx) ?? "";
|
|
5060
|
+
const tsx = parseComponentTsx(name, tsxSrc);
|
|
5061
|
+
const reuses = [...tsxSrc.matchAll(reuseRe)].filter((m) => m[1] === m[2] && m[1] !== name).map((m) => m[1]);
|
|
5062
|
+
const cssText = has8(files.css) ? readText(files.css) : void 0;
|
|
5063
|
+
const rules = cssText === void 0 ? [] : parseComponentCss(cssText, docs?.anatomy ?? {}, describeVar);
|
|
5064
|
+
const used = [...new Set(rules.flatMap((r) => r.vars.map((v) => v.name)))].sort();
|
|
5065
|
+
const storiesText = has8(files.stories) ? readText(files.stories) : void 0;
|
|
5066
|
+
return {
|
|
5067
|
+
name,
|
|
5068
|
+
slug: componentSlug(name),
|
|
5069
|
+
dir: folder,
|
|
5070
|
+
files,
|
|
5071
|
+
...tsx,
|
|
5072
|
+
rules,
|
|
5073
|
+
tokensUsed: used.map((n) => {
|
|
5074
|
+
const d = describeVar(n);
|
|
5075
|
+
return { name: n, href: d?.href, known: !!d, kind: d?.kind };
|
|
5076
|
+
}),
|
|
5077
|
+
tokenPaths: [...new Set(used.flatMap((n) => describeVar(n)?.path ?? []))].sort(),
|
|
5078
|
+
inherited: [],
|
|
5079
|
+
allTokenPaths: [],
|
|
5080
|
+
reuses,
|
|
5081
|
+
stories: storiesText === void 0 ? [] : parseComponentStories(storiesText),
|
|
5082
|
+
docs,
|
|
5083
|
+
category: docs?.category ?? familyOf(name) ?? "Core"
|
|
5084
|
+
};
|
|
5085
|
+
});
|
|
5086
|
+
const order = (c) => {
|
|
5087
|
+
const i = fam.findIndex((f) => f.name === c.category);
|
|
5088
|
+
return i < 0 ? fam.length : i;
|
|
5089
|
+
};
|
|
5090
|
+
result.sort((a, b) => order(a) - order(b) || a.name.localeCompare(b.name));
|
|
5091
|
+
resolveInheritedTokens(result);
|
|
5092
|
+
return result;
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
// ../core/src/repo-model.ts
|
|
5096
|
+
var REPO_MODEL_DEFAULTS = {
|
|
5097
|
+
tokensRoot: "packages/tokens/src",
|
|
5098
|
+
componentsDir: "packages/react/src/components",
|
|
5099
|
+
componentSpecs: "packages/react/component-specs.json",
|
|
5100
|
+
/** Optional token usage notes (group -> sentence), relative to tokens.root, when tokens.usage is not set. */
|
|
5101
|
+
tokensUsage: "usage.json",
|
|
5102
|
+
repo: { owner: "peterbono", repo: "ai-design-system-demo", defaultBranch: "main" }
|
|
5103
|
+
};
|
|
5104
|
+
function siteRepoFromConfig(config, raw, fallback = REPO_MODEL_DEFAULTS.repo) {
|
|
5105
|
+
const legacy = raw && typeof raw === "object" ? raw : void 0;
|
|
5106
|
+
const g = config.governance;
|
|
5107
|
+
const legacyOwners = legacy?.governance?.owners === void 0 ? legacy?.owners : void 0;
|
|
5108
|
+
return {
|
|
5109
|
+
owner: typeof legacy?.owner === "string" && legacy.owner ? legacy.owner : config.repo?.owner ?? fallback.owner,
|
|
5110
|
+
repo: config.repo?.name ?? fallback.repo,
|
|
5111
|
+
defaultBranch: typeof legacy?.defaultBranch === "string" && legacy.defaultBranch ? legacy.defaultBranch : config.repo?.defaultBranch ?? fallback.defaultBranch,
|
|
5112
|
+
owners: legacyOwners ? { design: legacyOwners.design ?? [], tech: legacyOwners.tech ?? [] } : { design: [...g.owners.design], tech: [...g.owners.tech] },
|
|
5113
|
+
override: g.override ? { ...g.override } : { ...DEFAULT_OVERRIDE },
|
|
5114
|
+
rights: g.rights ?? null,
|
|
5115
|
+
soloOwnerCanApproveBoth: g.soloOwnerCanApproveBoth === true,
|
|
5116
|
+
visualImpact: { mode: config.visualImpact?.mode === "on-demand" ? "on-demand" : "auto" },
|
|
5117
|
+
tokensRoot: config.tokens.root,
|
|
5118
|
+
integrations: config.integrations ?? {},
|
|
5119
|
+
products: config.products.length ? config.products : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
5120
|
+
changes: config.changes ?? DEFAULT_CHANGES_MODE
|
|
5121
|
+
};
|
|
5122
|
+
}
|
|
5123
|
+
function withModelDefaults(raw) {
|
|
5124
|
+
const tokens2 = raw.tokens && typeof raw.tokens === "object" ? raw.tokens : {};
|
|
5125
|
+
return {
|
|
5126
|
+
...raw,
|
|
5127
|
+
tokens: { ...tokens2, root: tokens2.root ?? REPO_MODEL_DEFAULTS.tokensRoot },
|
|
5128
|
+
components: raw.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir }
|
|
5129
|
+
};
|
|
5130
|
+
}
|
|
5131
|
+
var defaultModelConfig = () => defineConfig(withModelDefaults({}));
|
|
5132
|
+
function parseModelConfig(text) {
|
|
5133
|
+
if (text === null) return { config: defaultModelConfig(), raw: void 0, problems: [] };
|
|
5134
|
+
let raw;
|
|
5135
|
+
try {
|
|
5136
|
+
raw = JSON.parse(text);
|
|
5137
|
+
} catch (err) {
|
|
5138
|
+
return { config: defaultModelConfig(), raw: void 0, problems: [`${CONFIG_FILE} is not valid JSON (${err.message})`] };
|
|
5139
|
+
}
|
|
5140
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: defaultModelConfig(), raw, problems: [`${CONFIG_FILE}: top level must be an object`] };
|
|
5141
|
+
try {
|
|
5142
|
+
return { config: loadConfig(withModelDefaults(raw)), raw, problems: [] };
|
|
5143
|
+
} catch (err) {
|
|
5144
|
+
const problems = err instanceof ConfigError ? err.problems.map((p) => `${CONFIG_FILE}: ${p}`) : [`${CONFIG_FILE}: ${err.message}`];
|
|
5145
|
+
return { config: defaultModelConfig(), raw, problems };
|
|
5146
|
+
}
|
|
5147
|
+
}
|
|
5148
|
+
var emptyRows = (config) => ({ ...tokenRowAxes(config), primitives: [], semantic: [], brand: [], density: [], component: [], all: [], sources: [] });
|
|
5149
|
+
async function loadRepoModel(source, options = {}) {
|
|
5150
|
+
const concurrency = options.concurrency ?? 8;
|
|
5151
|
+
const problems = [];
|
|
5152
|
+
const tooLarge = (path, err) => {
|
|
5153
|
+
if (err?.status !== 413) throw err;
|
|
5154
|
+
problems.push(`${path}: ${err.message}`);
|
|
5155
|
+
return null;
|
|
5156
|
+
};
|
|
5157
|
+
const configText = await source.text(CONFIG_FILE);
|
|
5158
|
+
const parsed = parseModelConfig(configText);
|
|
5159
|
+
const { config } = parsed;
|
|
5160
|
+
problems.push(...parsed.problems);
|
|
5161
|
+
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;
|
|
5162
|
+
const site = siteRepoFromConfig(config, parsed.raw, fallbackRepo);
|
|
5163
|
+
const root = config.tokens.root;
|
|
5164
|
+
const underRoot = await source.list(root === "." ? "" : root);
|
|
5165
|
+
const tokenRel = selectTokenFiles(config, underRoot);
|
|
5166
|
+
const contrastPath = config.contrast?.path;
|
|
5167
|
+
const usagePath = tokensPath(config, config.tokens.usage ?? REPO_MODEL_DEFAULTS.tokensUsage);
|
|
5168
|
+
const tokenPaths = tokenRel.map((f) => tokensPath(config, f));
|
|
5169
|
+
const tokenRead = [.../* @__PURE__ */ new Set([...tokenPaths, ...contrastPath ? [contrastPath] : [], usagePath])];
|
|
5170
|
+
const comps = config.components ?? { dir: REPO_MODEL_DEFAULTS.componentsDir, docsSuffix: ".docs.json", styleSuffix: ".module.css" };
|
|
5171
|
+
const specsPath = comps.specs ?? REPO_MODEL_DEFAULTS.componentSpecs;
|
|
5172
|
+
const indexPath = comps.index ?? (parentDir(comps.dir) ? `${parentDir(comps.dir)}/index.ts` : "index.ts");
|
|
5173
|
+
let compFiles = [];
|
|
5174
|
+
const codeRead = [specsPath];
|
|
5175
|
+
const docsRead = [];
|
|
5176
|
+
if (options.components !== false) {
|
|
5177
|
+
compFiles = await source.list(comps.dir);
|
|
5178
|
+
for (const f of compFiles) {
|
|
5179
|
+
const parts = f.slice(comps.dir.length + 1).split("/");
|
|
5180
|
+
if (parts.length !== 2) continue;
|
|
5181
|
+
const [n, file] = parts;
|
|
5182
|
+
if (file === `${n}${comps.docsSuffix}`) docsRead.push(f);
|
|
5183
|
+
else if ([`${n}.tsx`, `${n}${comps.styleSuffix}`, `${n}.stories.tsx`].includes(file)) codeRead.push(f);
|
|
5184
|
+
}
|
|
5185
|
+
codeRead.push(indexPath);
|
|
5186
|
+
}
|
|
5187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5188
|
+
const fresh = (paths) => paths.filter((p) => {
|
|
5189
|
+
if (seen.has(p)) return false;
|
|
5190
|
+
seen.add(p);
|
|
5191
|
+
return true;
|
|
5192
|
+
});
|
|
5193
|
+
const phases = [
|
|
5194
|
+
["tokens", fresh(tokenRead)],
|
|
5195
|
+
["components", fresh(codeRead)],
|
|
5196
|
+
["docs", fresh(docsRead)]
|
|
5197
|
+
];
|
|
5198
|
+
const filesTotal = phases.reduce((n, [, paths]) => n + paths.length, 0);
|
|
5199
|
+
let before = 0;
|
|
5200
|
+
const readPhase = async (phase, paths) => {
|
|
5201
|
+
const offset = before;
|
|
5202
|
+
const out = await readRepoTexts(source, paths, concurrency, tooLarge, (done, total) => options.onProgress?.({ phase, done, total, filesDone: offset + done, filesTotal }));
|
|
5203
|
+
before += paths.length;
|
|
5204
|
+
return out;
|
|
5205
|
+
};
|
|
5206
|
+
const bodies = await readPhase("tokens", phases[0][1]);
|
|
5207
|
+
const read = (p) => bodies.get(p) ?? void 0;
|
|
5208
|
+
const files = {};
|
|
5209
|
+
tokenRel.forEach((rel, i) => {
|
|
5210
|
+
const body = read(tokenPaths[i]);
|
|
5211
|
+
if (body !== void 0) files[rel] = body;
|
|
5212
|
+
});
|
|
5213
|
+
const listFiles = (dir) => underRoot.filter((p) => parentDir(p) === dir);
|
|
5214
|
+
let rows;
|
|
5215
|
+
try {
|
|
5216
|
+
rows = buildTokenRows(config, read, listFiles);
|
|
5217
|
+
} catch (err) {
|
|
5218
|
+
problems.push(err.message);
|
|
5219
|
+
rows = emptyRows(config);
|
|
5220
|
+
}
|
|
5221
|
+
let usage = null;
|
|
5222
|
+
const usageText = read(usagePath);
|
|
5223
|
+
if (usageText !== void 0) {
|
|
5224
|
+
try {
|
|
5225
|
+
usage = JSON.parse(usageText);
|
|
5226
|
+
} catch (err) {
|
|
5227
|
+
problems.push(`${usagePath}: ${err.message}`);
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
let specs = [];
|
|
5231
|
+
let pairs3 = [];
|
|
5232
|
+
try {
|
|
5233
|
+
specs = parseContrastSpecs(contrastPath ? read(contrastPath) : void 0);
|
|
5234
|
+
pairs3 = evaluateContrastPairs(specs, rows);
|
|
5235
|
+
} catch (err) {
|
|
5236
|
+
problems.push(err.message);
|
|
5237
|
+
}
|
|
5238
|
+
const index = tokenRowIndex(rows);
|
|
5239
|
+
const describeVar = (cssVar) => {
|
|
5240
|
+
const hit = index.get(cssVar);
|
|
5241
|
+
return hit ? { path: hit.path, kind: hit.kind, href: options.href?.(cssVar, hit.type, hit.kind, hit.path) } : void 0;
|
|
5242
|
+
};
|
|
5243
|
+
let components = null;
|
|
5244
|
+
const code = await readPhase("components", phases[1][1]);
|
|
5245
|
+
const docs = await readPhase("docs", phases[2][1]);
|
|
5246
|
+
const readMore = (p) => code.get(p) ?? docs.get(p) ?? bodies.get(p) ?? void 0;
|
|
5247
|
+
if (options.components !== false) {
|
|
5248
|
+
const indexText = readMore(indexPath);
|
|
5249
|
+
const indexDir = parentDir(indexPath);
|
|
5250
|
+
const indexRel = indexDir && comps.dir.startsWith(`${indexDir}/`) ? comps.dir.slice(indexDir.length + 1) : comps.dir;
|
|
5251
|
+
const list2 = scanComponents({ dir: comps.dir, files: compFiles, readText: readMore, indexText, indexRel, docsSuffix: comps.docsSuffix, styleSuffix: comps.styleSuffix, describeVar, problems });
|
|
5252
|
+
components = { dir: comps.dir, index: indexText === void 0 ? void 0 : indexPath, families: indexText === void 0 ? [] : parseComponentFamilies(indexText, indexRel), list: list2 };
|
|
5253
|
+
}
|
|
5254
|
+
const specsText = readMore(specsPath);
|
|
5255
|
+
let specsFile = null;
|
|
5256
|
+
let coverage = [];
|
|
5257
|
+
if (specsText !== void 0) {
|
|
5258
|
+
try {
|
|
5259
|
+
specsFile = parseComponentSpecs(specsText);
|
|
5260
|
+
} catch {
|
|
5261
|
+
}
|
|
5262
|
+
try {
|
|
5263
|
+
coverage = JSON.parse(specsText).coverage ?? [];
|
|
5264
|
+
} catch (err) {
|
|
5265
|
+
problems.push(`${specsPath}: ${err.message}`);
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
return {
|
|
5269
|
+
ref: source.ref,
|
|
5270
|
+
configText,
|
|
5271
|
+
config,
|
|
5272
|
+
site,
|
|
5273
|
+
problems,
|
|
5274
|
+
tokens: { root, files, rows, usage },
|
|
5275
|
+
contrast: { path: contrastPath, specs, pairs: pairs3 },
|
|
5276
|
+
components,
|
|
5277
|
+
specs: { path: specsPath, file: specsFile, coverage }
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
|
|
5281
|
+
// ../core/src/snapshot.ts
|
|
5282
|
+
var SNAPSHOT_SCHEMA_VERSION = 1;
|
|
5283
|
+
var SNAPSHOT_BRANCH = "bridge-data";
|
|
5284
|
+
var SNAPSHOT_FILE = "snapshot.json";
|
|
5285
|
+
var SNAPSHOT_AUDITS_DIR = "audits";
|
|
5286
|
+
var SNAPSHOT_MAX_BYTES = 5e6;
|
|
5287
|
+
function recordingSource(source) {
|
|
5288
|
+
const paths = /* @__PURE__ */ new Set();
|
|
5289
|
+
const files = /* @__PURE__ */ new Map();
|
|
5290
|
+
return {
|
|
5291
|
+
paths,
|
|
5292
|
+
files,
|
|
5293
|
+
source: {
|
|
5294
|
+
get ref() {
|
|
5295
|
+
return source.ref;
|
|
5296
|
+
},
|
|
5297
|
+
async list(prefix) {
|
|
5298
|
+
const out = await source.list(prefix);
|
|
5299
|
+
for (const p of out) paths.add(p);
|
|
5300
|
+
return out;
|
|
5301
|
+
},
|
|
5302
|
+
async text(path) {
|
|
5303
|
+
const body = await source.text(path);
|
|
5304
|
+
if (body !== null) files.set(normalizeRepoPath(path), body);
|
|
5305
|
+
return body;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
5310
|
+
function snapshotAudit(id, report) {
|
|
5311
|
+
return {
|
|
5312
|
+
id,
|
|
5313
|
+
path: `${SNAPSHOT_AUDITS_DIR}/${id}.json`,
|
|
5314
|
+
...report.label ? { label: report.label } : {},
|
|
5315
|
+
generatedAt: report.generatedAt,
|
|
5316
|
+
target: { path: report.target.path, files: report.target.files },
|
|
5317
|
+
metrics: report.metrics
|
|
5318
|
+
};
|
|
5319
|
+
}
|
|
5320
|
+
var auditId = (s) => s.toLowerCase().replace(/\.json$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "audit";
|
|
5321
|
+
var pairResults = (p) => {
|
|
5322
|
+
const brands = Object.values(p.brands);
|
|
5323
|
+
return brands.length ? brands.flatMap((b) => Object.values(b)) : Object.values(p.modes);
|
|
5324
|
+
};
|
|
5325
|
+
function snapshotSummary(model) {
|
|
5326
|
+
const results = model.contrast.pairs.flatMap((p) => pairResults(p).map((r) => r.ratio >= p.min));
|
|
5327
|
+
const coverage = model.specs.coverage;
|
|
5328
|
+
const visual = coverage.filter((c) => c.status !== "layout").length;
|
|
5329
|
+
const inFigma = coverage.filter((c) => c.status === "generated" || c.status === "drafted").length;
|
|
5330
|
+
return {
|
|
5331
|
+
tokens: model.tokens.rows.all.length + model.tokens.rows.primitives.length,
|
|
5332
|
+
components: model.components ? model.components.list.length : null,
|
|
5333
|
+
contrast: { pairs: model.contrast.pairs.length, checks: results.length, failing: results.filter((ok) => !ok).length },
|
|
5334
|
+
figma: coverage.length ? { inFigma, visual } : null
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
function buildSnapshot(input) {
|
|
5338
|
+
const { model } = input;
|
|
5339
|
+
const files = {};
|
|
5340
|
+
const entries = input.files instanceof Map ? [...input.files] : Object.entries(input.files);
|
|
5341
|
+
const specsPath = normalizeRepoPath(model.specs.path);
|
|
5342
|
+
for (const [k, v] of entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
5343
|
+
const p = normalizeRepoPath(k);
|
|
5344
|
+
if (p !== specsPath) files[p] = v;
|
|
5345
|
+
}
|
|
5346
|
+
const hasSpecs = entries.some(([k]) => normalizeRepoPath(k) === specsPath);
|
|
5347
|
+
return {
|
|
5348
|
+
kind: "bridge-snapshot",
|
|
5349
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
5350
|
+
tool: { name: "bridge", version: input.version },
|
|
5351
|
+
generatedAt: input.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5352
|
+
repo: { owner: model.ref.owner || model.site.owner, name: model.ref.repo || model.site.repo, branch: input.branch ?? model.site.defaultBranch },
|
|
5353
|
+
sourceSha: model.ref.sha,
|
|
5354
|
+
...input.committedAt ? { committedAt: input.committedAt } : {},
|
|
5355
|
+
paths: [...new Set([...input.paths].map(normalizeRepoPath))].filter((p) => p !== specsPath).sort(),
|
|
5356
|
+
files,
|
|
5357
|
+
figma: hasSpecs ? { path: specsPath, ...model.specs.file?.generated ? { generated: model.specs.file.generated } : {}, coverage: model.specs.coverage } : null,
|
|
5358
|
+
summary: snapshotSummary(model),
|
|
5359
|
+
contrast: { ...model.contrast.path ? { path: model.contrast.path } : {}, pairs: model.contrast.pairs.map(({ fgToken: _fg, bgToken: _bg, ...rest }) => rest) },
|
|
5360
|
+
audits: input.audits ?? [],
|
|
5361
|
+
problems: [...model.problems]
|
|
5362
|
+
};
|
|
5363
|
+
}
|
|
5364
|
+
var SNAPSHOT_LLMS_FILE = "llms.txt";
|
|
5365
|
+
var LLMS_MAX_TOKENS = 600;
|
|
5366
|
+
var line = (text) => typeof text === "string" ? text.replace(/\s+/g, " ").trim() : "";
|
|
5367
|
+
var strings = (list2) => Array.isArray(list2) ? list2.map(line).filter(Boolean) : [];
|
|
5368
|
+
function snapshotLlmsTxt(model, snapshot) {
|
|
5369
|
+
const { owner, name, branch } = snapshot.repo;
|
|
5370
|
+
const date2 = (snapshot.committedAt ?? snapshot.generatedAt).slice(0, 10);
|
|
5371
|
+
const rows = model.tokens.rows;
|
|
5372
|
+
const tokens2 = rows.all.filter((t) => !t.deprecated);
|
|
5373
|
+
const shown = tokens2.slice(0, LLMS_MAX_TOKENS);
|
|
5374
|
+
const comps = model.components?.list ?? [];
|
|
5375
|
+
const out = [
|
|
5376
|
+
`# ${owner}/${name} design system`,
|
|
5377
|
+
"",
|
|
5378
|
+
`> 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.`,
|
|
5379
|
+
"",
|
|
5380
|
+
`- Source: https://github.com/${owner}/${name}/tree/${snapshot.sourceSha}`,
|
|
5381
|
+
`- Every file Bridge read, as JSON: ${SNAPSHOT_FILE}, next to this file on the ${SNAPSHOT_BRANCH} branch.`,
|
|
5382
|
+
`- Themes: ${rows.themes.join(", ") || "none"}. Brands: ${rows.brands.join(", ") || "none"}. Densities: ${rows.densities.join(", ") || "none"}.`,
|
|
5383
|
+
"",
|
|
5384
|
+
"## Tokens",
|
|
5385
|
+
"",
|
|
5386
|
+
`${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})` : ""}.`,
|
|
5387
|
+
""
|
|
5388
|
+
];
|
|
5389
|
+
for (const t of shown) {
|
|
5390
|
+
const values = Object.entries(t.modes).map(([theme, v]) => `${theme} ${line(v.css)}`).join(", ");
|
|
5391
|
+
const note = line(t.description);
|
|
5392
|
+
out.push(`- \`${t.path}\` (\`${t.cssVar}\`, ${t.tier} ${t.type}): ${values || "no value"}${note ? `. ${note}` : ""}`);
|
|
5393
|
+
}
|
|
5394
|
+
if (tokens2.length > shown.length) out.push("", `${tokens2.length - shown.length} more tokens are in ${SNAPSHOT_FILE}.`);
|
|
5395
|
+
out.push("", "## Components", "");
|
|
5396
|
+
if (!comps.length) out.push("No components were read (components.dir in bridge.config.json names the folder).");
|
|
5397
|
+
for (const c of comps) {
|
|
5398
|
+
const d = c.docs;
|
|
5399
|
+
out.push(`### ${line(c.name)}`, "");
|
|
5400
|
+
const summary = line(d?.summary) || line(c.description);
|
|
5401
|
+
if (summary) out.push(summary, "");
|
|
5402
|
+
out.push(`- Family: ${line(c.category)}${c.docs?.status && c.docs.status !== "stable" ? `, ${c.docs.status}` : ""}`);
|
|
5403
|
+
if (c.allTokenPaths.length) out.push(`- Tokens: ${c.allTokenPaths.slice(0, 40).join(", ")}${c.allTokenPaths.length > 40 ? ", ..." : ""}`);
|
|
5404
|
+
for (const [label, list2] of [
|
|
5405
|
+
["Use it for", strings(d?.whenToUse)],
|
|
5406
|
+
["Not for", strings(d?.whenNotToUse)],
|
|
5407
|
+
["Accessibility", strings(d?.accessibility)]
|
|
5408
|
+
]) {
|
|
5409
|
+
if (list2.length) out.push(`- ${label}:`, ...list2.map((x) => ` - ${x}`));
|
|
5410
|
+
}
|
|
5411
|
+
const dos = Array.isArray(d?.dos) ? d.dos : [];
|
|
5412
|
+
const donts = Array.isArray(d?.donts) ? d.donts : [];
|
|
5413
|
+
for (const g of dos) if (line(g?.title)) out.push(`- Do: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
|
|
5414
|
+
for (const g of donts) if (line(g?.title)) out.push(`- Don't: ${line(g.title)}${line(g.text) ? `. ${line(g.text)}` : ""}`);
|
|
5415
|
+
out.push("");
|
|
5416
|
+
}
|
|
5417
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
|
|
5418
|
+
`;
|
|
5419
|
+
}
|
|
5420
|
+
|
|
4254
5421
|
// ../visual/src/capture.ts
|
|
4255
5422
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4256
5423
|
import { dirname as dirname2 } from "node:path";
|
|
@@ -4348,15 +5515,15 @@ async function shoot(page, r) {
|
|
|
4348
5515
|
}
|
|
4349
5516
|
async function switchStory(page, storyId2) {
|
|
4350
5517
|
const ok = await page.evaluate(
|
|
4351
|
-
(id) => new Promise((
|
|
5518
|
+
(id) => new Promise((resolve7) => {
|
|
4352
5519
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
4353
|
-
if (!ch) return
|
|
5520
|
+
if (!ch) return resolve7(false);
|
|
4354
5521
|
const events = ["storyRendered", "storyErrored", "storyThrewException", "storyMissing", "playFunctionThrewException"];
|
|
4355
5522
|
const timer = setTimeout(() => done(), 15e3);
|
|
4356
5523
|
function done() {
|
|
4357
5524
|
clearTimeout(timer);
|
|
4358
5525
|
for (const e of events) ch.off(e, done);
|
|
4359
|
-
|
|
5526
|
+
resolve7(true);
|
|
4360
5527
|
}
|
|
4361
5528
|
for (const e of events) ch.on(e, done);
|
|
4362
5529
|
window.scrollTo(0, 0);
|
|
@@ -4645,7 +5812,7 @@ function routeUrl(base, route, tenant, theme) {
|
|
|
4645
5812
|
return `${base.replace(/\/$/, "")}/?tenant=${encodeURIComponent(tenant)}&theme=${encodeURIComponent(theme)}&density=comfortable&lang=en#${route.pathByTenant?.[tenant] ?? route.path}`;
|
|
4646
5813
|
}
|
|
4647
5814
|
function storyUrl(base, id, brand2, theme) {
|
|
4648
|
-
return
|
|
5815
|
+
return storybookIframeUrl(base, id, { globals: { brand: brand2, theme, density: "comfortable" } });
|
|
4649
5816
|
}
|
|
4650
5817
|
var shotKey = (...parts) => parts.map((p) => p.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-|-$/g, "")).join("__");
|
|
4651
5818
|
|
|
@@ -4727,9 +5894,9 @@ function storiesFromSource(files) {
|
|
|
4727
5894
|
}
|
|
4728
5895
|
|
|
4729
5896
|
// ../visual/src/serve.ts
|
|
4730
|
-
import { createReadStream, existsSync as
|
|
5897
|
+
import { createReadStream, existsSync as existsSync3, statSync as statSync3 } from "node:fs";
|
|
4731
5898
|
import { createServer } from "node:http";
|
|
4732
|
-
import { extname, join as
|
|
5899
|
+
import { extname, join as join3, normalize, sep as sep2 } from "node:path";
|
|
4733
5900
|
var TYPES = {
|
|
4734
5901
|
".html": "text/html; charset=utf-8",
|
|
4735
5902
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -4753,50 +5920,50 @@ function serveDir(dir) {
|
|
|
4753
5920
|
const root = normalize(dir);
|
|
4754
5921
|
const server = createServer((req, res) => {
|
|
4755
5922
|
const path = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
|
|
4756
|
-
let file = normalize(
|
|
5923
|
+
let file = normalize(join3(root, path));
|
|
4757
5924
|
if (!file.startsWith(root + sep2) && file !== root) {
|
|
4758
5925
|
res.writeHead(403).end();
|
|
4759
5926
|
return;
|
|
4760
5927
|
}
|
|
4761
|
-
if (
|
|
4762
|
-
if (!
|
|
5928
|
+
if (existsSync3(file) && statSync3(file).isDirectory()) file = join3(file, "index.html");
|
|
5929
|
+
if (!existsSync3(file)) {
|
|
4763
5930
|
if (extname(path)) {
|
|
4764
5931
|
res.writeHead(404).end();
|
|
4765
5932
|
return;
|
|
4766
5933
|
}
|
|
4767
|
-
file =
|
|
5934
|
+
file = join3(root, "index.html");
|
|
4768
5935
|
}
|
|
4769
5936
|
res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream", "cache-control": "no-store" });
|
|
4770
5937
|
createReadStream(file).pipe(res);
|
|
4771
5938
|
});
|
|
4772
|
-
return new Promise((
|
|
5939
|
+
return new Promise((resolve7, reject) => {
|
|
4773
5940
|
server.once("error", reject);
|
|
4774
5941
|
server.listen(0, "127.0.0.1", () => {
|
|
4775
5942
|
const addr = server.address();
|
|
4776
5943
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
4777
|
-
|
|
5944
|
+
resolve7({ url: `http://127.0.0.1:${port}`, close: () => new Promise((r) => server.close(() => r())) });
|
|
4778
5945
|
});
|
|
4779
5946
|
});
|
|
4780
5947
|
}
|
|
4781
5948
|
|
|
4782
5949
|
// ../visual/src/workspace.ts
|
|
4783
5950
|
import { spawn } from "node:child_process";
|
|
4784
|
-
import { existsSync as
|
|
5951
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4785
5952
|
import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, realpath, rm, symlink } from "node:fs/promises";
|
|
4786
|
-
import { dirname as dirname3, join as
|
|
5953
|
+
import { dirname as dirname3, join as join4, relative as relative2 } from "node:path";
|
|
4787
5954
|
var PRIVATE = /* @__PURE__ */ new Set([".cache", ".vite", ".vite-temp", ".package-lock.json"]);
|
|
4788
5955
|
async function workspaceDirs(root) {
|
|
4789
|
-
const pkg = JSON.parse(await readFile2(
|
|
5956
|
+
const pkg = JSON.parse(await readFile2(join4(root, "package.json"), "utf8"));
|
|
4790
5957
|
const pats = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages ?? [];
|
|
4791
5958
|
const out = [];
|
|
4792
5959
|
for (const p of pats) {
|
|
4793
5960
|
if (!p.endsWith("/*")) {
|
|
4794
|
-
if (
|
|
5961
|
+
if (existsSync4(join4(root, p, "package.json"))) out.push(p);
|
|
4795
5962
|
continue;
|
|
4796
5963
|
}
|
|
4797
5964
|
const parent = p.slice(0, -2);
|
|
4798
|
-
if (!
|
|
4799
|
-
for (const d of await readdir(
|
|
5965
|
+
if (!existsSync4(join4(root, parent))) continue;
|
|
5966
|
+
for (const d of await readdir(join4(root, parent))) if (existsSync4(join4(root, parent, d, "package.json"))) out.push(`${parent}/${d}`);
|
|
4800
5967
|
}
|
|
4801
5968
|
return out;
|
|
4802
5969
|
}
|
|
@@ -4805,30 +5972,30 @@ async function link(target, at) {
|
|
|
4805
5972
|
await symlink(target, at);
|
|
4806
5973
|
}
|
|
4807
5974
|
async function linkNodeModules(src, dst) {
|
|
4808
|
-
const srcNm = await realpath(
|
|
4809
|
-
const dstNm =
|
|
4810
|
-
if (
|
|
5975
|
+
const srcNm = await realpath(join4(src, "node_modules"));
|
|
5976
|
+
const dstNm = join4(dst, "node_modules");
|
|
5977
|
+
if (existsSync4(dstNm)) return;
|
|
4811
5978
|
const dirs = await workspaceDirs(dst);
|
|
4812
5979
|
const names = /* @__PURE__ */ new Map();
|
|
4813
5980
|
for (const d of dirs) {
|
|
4814
|
-
const { name } = JSON.parse(await readFile2(
|
|
5981
|
+
const { name } = JSON.parse(await readFile2(join4(dst, d, "package.json"), "utf8"));
|
|
4815
5982
|
if (name) names.set(name, d);
|
|
4816
5983
|
}
|
|
4817
5984
|
await mkdir2(dstNm, { recursive: true });
|
|
4818
5985
|
for (const e of await readdir(srcNm)) {
|
|
4819
5986
|
if (PRIVATE.has(e)) continue;
|
|
4820
5987
|
if (e.startsWith("@")) {
|
|
4821
|
-
for (const c of await readdir(
|
|
4822
|
-
} else if (!names.has(e)) await link(
|
|
5988
|
+
for (const c of await readdir(join4(srcNm, e))) if (!names.has(`${e}/${c}`)) await link(join4(srcNm, e, c), join4(dstNm, e, c));
|
|
5989
|
+
} else if (!names.has(e)) await link(join4(srcNm, e), join4(dstNm, e));
|
|
4823
5990
|
}
|
|
4824
5991
|
for (const [name, d] of names) {
|
|
4825
|
-
const at =
|
|
5992
|
+
const at = join4(dstNm, name);
|
|
4826
5993
|
await rm(at, { force: true, recursive: false }).catch(() => void 0);
|
|
4827
|
-
await link(relative2(dirname3(at),
|
|
5994
|
+
await link(relative2(dirname3(at), join4(dst, d)), at);
|
|
4828
5995
|
}
|
|
4829
5996
|
for (const d of dirs) {
|
|
4830
|
-
const nested =
|
|
4831
|
-
if (
|
|
5997
|
+
const nested = join4(src, d, "node_modules");
|
|
5998
|
+
if (existsSync4(nested) && !existsSync4(join4(dst, d, "node_modules"))) await link(await realpath(nested), join4(dst, d, "node_modules"));
|
|
4832
5999
|
}
|
|
4833
6000
|
}
|
|
4834
6001
|
async function addWorktree(repo, sha, dir) {
|
|
@@ -4838,7 +6005,7 @@ async function addWorktree(repo, sha, dir) {
|
|
|
4838
6005
|
}
|
|
4839
6006
|
async function removeWorktree(repo, dir) {
|
|
4840
6007
|
try {
|
|
4841
|
-
await rm(
|
|
6008
|
+
await rm(join4(dir, "node_modules"), { recursive: true, force: true });
|
|
4842
6009
|
await git(repo, ["worktree", "remove", "--force", dir]);
|
|
4843
6010
|
} catch {
|
|
4844
6011
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -4846,8 +6013,8 @@ async function removeWorktree(repo, dir) {
|
|
|
4846
6013
|
await git(repo, ["worktree", "prune"]).catch(() => void 0);
|
|
4847
6014
|
}
|
|
4848
6015
|
function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
4849
|
-
return new Promise((
|
|
4850
|
-
const bin = [.../* @__PURE__ */ new Set([
|
|
6016
|
+
return new Promise((resolve7, reject) => {
|
|
6017
|
+
const bin = [.../* @__PURE__ */ new Set([join4(cwd, "node_modules", ".bin"), join4(root, "node_modules", ".bin")])].join(":");
|
|
4851
6018
|
const child = spawn(cmd, args, {
|
|
4852
6019
|
cwd,
|
|
4853
6020
|
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, NODE_OPTIONS: process.env.NODE_OPTIONS ?? "--max-old-space-size=3072", STORYBOOK_DISABLE_TELEMETRY: "1", ...env },
|
|
@@ -4859,7 +6026,7 @@ function run(cmd, args, cwd, log, root = cwd, env = {}) {
|
|
|
4859
6026
|
child.stderr.on("data", keep);
|
|
4860
6027
|
child.on("error", reject);
|
|
4861
6028
|
child.on("close", (code) => {
|
|
4862
|
-
if (code === 0)
|
|
6029
|
+
if (code === 0) resolve7();
|
|
4863
6030
|
else {
|
|
4864
6031
|
log(tail);
|
|
4865
6032
|
reject(new Error(`${cmd} ${args.join(" ")} failed in ${cwd} (exit ${code})`));
|
|
@@ -4875,17 +6042,17 @@ var DEFAULT_BUILD = {
|
|
|
4875
6042
|
var isDir = async (p) => (await lstat(p).catch(() => null))?.isDirectory() ?? false;
|
|
4876
6043
|
async function buildRef(o) {
|
|
4877
6044
|
const cfg = o.config ?? DEFAULT_BUILD;
|
|
4878
|
-
const out =
|
|
4879
|
-
const sbOut =
|
|
4880
|
-
const appOut =
|
|
4881
|
-
const needSb = o.plan.storybook && !
|
|
4882
|
-
const needApp = o.plan.app && !
|
|
6045
|
+
const out = join4(o.cache, o.sha);
|
|
6046
|
+
const sbOut = join4(out, "storybook");
|
|
6047
|
+
const appOut = join4(out, "app");
|
|
6048
|
+
const needSb = o.plan.storybook && !existsSync4(join4(sbOut, "index.json"));
|
|
6049
|
+
const needApp = o.plan.app && !existsSync4(join4(appOut, "index.html"));
|
|
4883
6050
|
const built = { ...o.plan.storybook ? { storybook: sbOut } : {}, ...o.plan.app ? { app: appOut } : {} };
|
|
4884
6051
|
if (!needSb && !needApp) {
|
|
4885
6052
|
o.log(` ${o.sha.slice(0, 7)}: builds cached`);
|
|
4886
6053
|
return built;
|
|
4887
6054
|
}
|
|
4888
|
-
const dir =
|
|
6055
|
+
const dir = join4(o.work, `wt-${o.sha.slice(0, 12)}`);
|
|
4889
6056
|
o.log(` ${o.sha.slice(0, 7)}: worktree ${dir}`);
|
|
4890
6057
|
await addWorktree(o.repo, o.sha, dir);
|
|
4891
6058
|
try {
|
|
@@ -4897,15 +6064,15 @@ async function buildRef(o) {
|
|
|
4897
6064
|
await rm(sbOut, { recursive: true, force: true });
|
|
4898
6065
|
await run("storybook", ["build", "-c", cfg.storybookConfigDir, "-o", sbOut, "--quiet", "--disable-telemetry"], dir, o.log);
|
|
4899
6066
|
}
|
|
4900
|
-
if (needApp && await isDir(
|
|
6067
|
+
if (needApp && await isDir(join4(dir, cfg.appDir))) {
|
|
4901
6068
|
o.log(` ${o.sha.slice(0, 7)}: app build`);
|
|
4902
6069
|
await rm(appOut, { recursive: true, force: true });
|
|
4903
|
-
await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"],
|
|
6070
|
+
await run("vite", ["build", "--outDir", appOut, "--emptyOutDir", "--logLevel", "warn"], join4(dir, cfg.appDir), o.log, dir);
|
|
4904
6071
|
}
|
|
4905
6072
|
} finally {
|
|
4906
6073
|
if (!o.keep) await removeWorktree(o.repo, dir);
|
|
4907
6074
|
}
|
|
4908
|
-
if (o.plan.app && !
|
|
6075
|
+
if (o.plan.app && !existsSync4(join4(appOut, "index.html"))) delete built.app;
|
|
4909
6076
|
return built;
|
|
4910
6077
|
}
|
|
4911
6078
|
|
|
@@ -4913,7 +6080,7 @@ async function buildRef(o) {
|
|
|
4913
6080
|
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')`;
|
|
4914
6081
|
var APP_READY = `document.querySelector('#root')?.childElementCount > 0 && !document.querySelector('[aria-busy="true"]')`;
|
|
4915
6082
|
async function readConfig(root, sha) {
|
|
4916
|
-
const text = await showFile(root, sha, "bridge.config.json") ?? (
|
|
6083
|
+
const text = await showFile(root, sha, "bridge.config.json") ?? (existsSync5(join5(root, "bridge.config.json")) ? await readFile3(join5(root, "bridge.config.json"), "utf8") : null);
|
|
4917
6084
|
if (!text) throw new Error("bridge.config.json not found: run `bridge init` first.");
|
|
4918
6085
|
return loadConfig(text);
|
|
4919
6086
|
}
|
|
@@ -4936,8 +6103,8 @@ function changedTokens(before, after) {
|
|
|
4936
6103
|
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));
|
|
4937
6104
|
}
|
|
4938
6105
|
async function readIndexJson(dir) {
|
|
4939
|
-
const
|
|
4940
|
-
return
|
|
6106
|
+
const index = parseStorybookIndex(JSON.parse(await readFile3(join5(dir, "index.json"), "utf8")), "index.json");
|
|
6107
|
+
return index.entries.filter((e) => e.type === "story").map(({ id, title, name, importPath }) => ({ id, title, name, importPath }));
|
|
4941
6108
|
}
|
|
4942
6109
|
async function planImpact(o) {
|
|
4943
6110
|
const root = resolve2(o.root);
|
|
@@ -4978,7 +6145,7 @@ async function runImpact(o) {
|
|
|
4978
6145
|
const log = o.log ?? ((m) => process.stderr.write(`${m}
|
|
4979
6146
|
`));
|
|
4980
6147
|
const root = resolve2(o.root);
|
|
4981
|
-
const outDir = resolve2(o.out ??
|
|
6148
|
+
const outDir = resolve2(o.out ?? join5(root, "visual-impact"));
|
|
4982
6149
|
const threshold = o.threshold ?? 0.03;
|
|
4983
6150
|
const only = o.only?.length ? o.only : ["stories", "app"];
|
|
4984
6151
|
const vp = { stories: o.viewport?.stories ?? { width: 1024, height: 768 }, app: o.viewport?.app ?? { width: 1280, height: 800 } };
|
|
@@ -5005,12 +6172,12 @@ async function runImpact(o) {
|
|
|
5005
6172
|
shots: shots2
|
|
5006
6173
|
};
|
|
5007
6174
|
await mkdir3(outDir, { recursive: true });
|
|
5008
|
-
const manifestPath =
|
|
6175
|
+
const manifestPath = join5(outDir, "manifest.json");
|
|
5009
6176
|
await writeFile3(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
5010
6177
|
`);
|
|
5011
6178
|
let commentPath;
|
|
5012
6179
|
if (o.imageBase !== void 0 || o.artifactUrl) {
|
|
5013
|
-
commentPath =
|
|
6180
|
+
commentPath = join5(outDir, "comment.md");
|
|
5014
6181
|
await writeFile3(commentPath, renderComment(manifest, { ...o.imageBase ? { imageBase: o.imageBase } : {}, ...o.artifactUrl ? { artifactUrl: o.artifactUrl } : {} }));
|
|
5015
6182
|
}
|
|
5016
6183
|
return { manifest, outDir, manifestPath, ...commentPath ? { commentPath } : {}, summary: summaryLine(manifest) };
|
|
@@ -5022,8 +6189,8 @@ async function runImpact(o) {
|
|
|
5022
6189
|
log("Nothing visual to capture.");
|
|
5023
6190
|
return finish([]);
|
|
5024
6191
|
}
|
|
5025
|
-
const cache2 = resolve2(o.cacheDir ??
|
|
5026
|
-
const work = resolve2(o.workDir ??
|
|
6192
|
+
const cache2 = resolve2(o.cacheDir ?? join5(root, "node_modules", ".cache", "bridge-visual"));
|
|
6193
|
+
const work = resolve2(o.workDir ?? join5(tmpdir(), `bridge-visual-${process.pid}`));
|
|
5027
6194
|
await mkdir3(work, { recursive: true });
|
|
5028
6195
|
const builds = { base: {}, head: {} };
|
|
5029
6196
|
try {
|
|
@@ -5052,7 +6219,7 @@ async function runImpact(o) {
|
|
|
5052
6219
|
}
|
|
5053
6220
|
const byId = /* @__PURE__ */ new Map();
|
|
5054
6221
|
if (builds.head.storybook) for (const s of await readIndexJson(builds.head.storybook)) byId.set(s.id, s);
|
|
5055
|
-
if (builds.base.storybook &&
|
|
6222
|
+
if (builds.base.storybook && existsSync5(join5(builds.base.storybook, "index.json"))) {
|
|
5056
6223
|
for (const s of await readIndexJson(builds.base.storybook)) if (!byId.has(s.id)) byId.set(s.id, s);
|
|
5057
6224
|
}
|
|
5058
6225
|
const jobs = [];
|
|
@@ -5067,18 +6234,18 @@ async function runImpact(o) {
|
|
|
5067
6234
|
for (const theme of THEMES)
|
|
5068
6235
|
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 });
|
|
5069
6236
|
await rm2(outDir, { recursive: true, force: true });
|
|
5070
|
-
for (const d of ["before", "after", "diff", "composite"]) await mkdir3(
|
|
6237
|
+
for (const d of ["before", "after", "diff", "composite"]) await mkdir3(join5(outDir, d), { recursive: true });
|
|
5071
6238
|
const errors = /* @__PURE__ */ new Map();
|
|
5072
6239
|
for (const side of ["base", "head"]) {
|
|
5073
6240
|
const b = builds[side];
|
|
5074
6241
|
const served = [];
|
|
5075
6242
|
const browser = await launch();
|
|
5076
6243
|
try {
|
|
5077
|
-
const sb = b.storybook &&
|
|
5078
|
-
const app = b.app &&
|
|
6244
|
+
const sb = b.storybook && existsSync5(join5(b.storybook, "index.json")) ? await serveDir(b.storybook) : null;
|
|
6245
|
+
const app = b.app && existsSync5(join5(b.app, "index.html")) ? await serveDir(b.app) : null;
|
|
5079
6246
|
if (sb) served.push(sb);
|
|
5080
6247
|
if (app) served.push(app);
|
|
5081
|
-
const dir =
|
|
6248
|
+
const dir = join5(outDir, side === "base" ? "before" : "after");
|
|
5082
6249
|
const storyJobs = jobs.filter((j) => j.kind === "story");
|
|
5083
6250
|
if (sb && storyJobs.length) {
|
|
5084
6251
|
log(`Capturing ${storyJobs.length} story shots at ${side}`);
|
|
@@ -5093,7 +6260,7 @@ async function runImpact(o) {
|
|
|
5093
6260
|
if (!known.has(j.storyId)) continue;
|
|
5094
6261
|
const err = await shoot(page, {
|
|
5095
6262
|
url: storyUrl(sb.url, j.storyId, j.brand, j.theme),
|
|
5096
|
-
file:
|
|
6263
|
+
file: join5(dir, `${j.key}.png`),
|
|
5097
6264
|
fullPage: true,
|
|
5098
6265
|
readyFn: STORY_READY,
|
|
5099
6266
|
// Loading stories (Spinner, Skeleton) stay aria-busy on purpose: do not wait for them.
|
|
@@ -5119,7 +6286,7 @@ async function runImpact(o) {
|
|
|
5119
6286
|
const ctx = await newContext(browser, vp.app);
|
|
5120
6287
|
const page = await newPage(ctx);
|
|
5121
6288
|
for (const j of routeJobs.filter((x) => x.tenant.id === t.id && x.theme === theme)) {
|
|
5122
|
-
const err = await shoot(page, { url: routeUrl(app.url, j.route, t.id, theme), file:
|
|
6289
|
+
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 ?? []] });
|
|
5123
6290
|
if (err) errors.set(`${side}:${j.key}`, err);
|
|
5124
6291
|
}
|
|
5125
6292
|
await ctx.close();
|
|
@@ -5133,10 +6300,10 @@ async function runImpact(o) {
|
|
|
5133
6300
|
log("Diffing");
|
|
5134
6301
|
const shots = [];
|
|
5135
6302
|
for (const j of jobs) {
|
|
5136
|
-
const bf =
|
|
5137
|
-
const af =
|
|
5138
|
-
const hasB =
|
|
5139
|
-
const hasA =
|
|
6303
|
+
const bf = join5(outDir, "before", `${j.key}.png`);
|
|
6304
|
+
const af = join5(outDir, "after", `${j.key}.png`);
|
|
6305
|
+
const hasB = existsSync5(bf);
|
|
6306
|
+
const hasA = existsSync5(af);
|
|
5140
6307
|
const base = {
|
|
5141
6308
|
key: j.key,
|
|
5142
6309
|
kind: j.kind,
|
|
@@ -5176,7 +6343,7 @@ async function runImpact(o) {
|
|
|
5176
6343
|
if (changed || o.keepUnchanged) Object.assign(images, { before: `before/${j.key}.png`, after: `after/${j.key}.png` });
|
|
5177
6344
|
else await Promise.all([unlink(bf), unlink(af)]);
|
|
5178
6345
|
if (d.diff) {
|
|
5179
|
-
await writeFile3(
|
|
6346
|
+
await writeFile3(join5(outDir, "diff", `${j.key}.png`), encodePng(d.diff));
|
|
5180
6347
|
images.diff = `diff/${j.key}.png`;
|
|
5181
6348
|
}
|
|
5182
6349
|
shots.push({
|
|
@@ -5201,13 +6368,13 @@ async function runImpact(o) {
|
|
|
5201
6368
|
for (const s of toComposite) {
|
|
5202
6369
|
const w = Math.max(s.size.before?.width ?? 0, s.size.after?.width ?? 0);
|
|
5203
6370
|
const h = Math.max(s.size.before?.height ?? 0, s.size.after?.height ?? 0);
|
|
5204
|
-
const file =
|
|
6371
|
+
const file = join5(outDir, "composite", `${s.key}.png`);
|
|
5205
6372
|
await composite(page, {
|
|
5206
6373
|
title: s.label,
|
|
5207
6374
|
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}`,
|
|
5208
|
-
before: s.images.before ?
|
|
5209
|
-
after: s.images.after ?
|
|
5210
|
-
diff: s.images.diff ?
|
|
6375
|
+
before: s.images.before ? join5(outDir, s.images.before) : null,
|
|
6376
|
+
after: s.images.after ? join5(outDir, s.images.after) : null,
|
|
6377
|
+
diff: s.images.diff ? join5(outDir, s.images.diff) : null,
|
|
5211
6378
|
width: w,
|
|
5212
6379
|
height: h,
|
|
5213
6380
|
boxes: s.boxes,
|
|
@@ -5226,16 +6393,16 @@ async function runImpact(o) {
|
|
|
5226
6393
|
}
|
|
5227
6394
|
|
|
5228
6395
|
// ../visual/src/references.ts
|
|
5229
|
-
import { existsSync as
|
|
6396
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
5230
6397
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
5231
|
-
import { dirname as dirname4, join as
|
|
6398
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
5232
6399
|
async function captureFigmaReferences(o) {
|
|
5233
6400
|
const log = o.log ?? (() => void 0);
|
|
5234
6401
|
const scale = o.scale ?? 2;
|
|
5235
6402
|
const brand2 = o.brand ?? "bridge";
|
|
5236
6403
|
const theme = o.theme ?? "light";
|
|
5237
|
-
if (!
|
|
5238
|
-
const index = JSON.parse(await readFile4(
|
|
6404
|
+
if (!existsSync6(join6(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
|
|
6405
|
+
const index = JSON.parse(await readFile4(join6(o.storybook, "index.json"), "utf8"));
|
|
5239
6406
|
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).`);
|
|
5240
6407
|
const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
|
|
5241
6408
|
const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
|
|
@@ -5270,12 +6437,12 @@ async function captureFigmaReferences(o) {
|
|
|
5270
6437
|
}
|
|
5271
6438
|
const plan = JSON.stringify(j.plan.render);
|
|
5272
6439
|
await page.evaluate(
|
|
5273
|
-
({ id, plan: plan2 }) => new Promise((
|
|
6440
|
+
({ id, plan: plan2 }) => new Promise((resolve7) => {
|
|
5274
6441
|
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
5275
6442
|
const done = () => {
|
|
5276
6443
|
clearTimeout(timer);
|
|
5277
6444
|
ch.off("storyRendered", done);
|
|
5278
|
-
|
|
6445
|
+
resolve7();
|
|
5279
6446
|
};
|
|
5280
6447
|
const timer = setTimeout(done, 1e4);
|
|
5281
6448
|
ch.on("storyRendered", done);
|
|
@@ -5298,7 +6465,7 @@ async function captureFigmaReferences(o) {
|
|
|
5298
6465
|
continue;
|
|
5299
6466
|
}
|
|
5300
6467
|
const file = referenceImagePath(j.component, j.key);
|
|
5301
|
-
const path =
|
|
6468
|
+
const path = join6(o.out, file);
|
|
5302
6469
|
await mkdir4(dirname4(path), { recursive: true });
|
|
5303
6470
|
await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
|
|
5304
6471
|
const box = await target.boundingBox();
|
|
@@ -5311,7 +6478,7 @@ async function captureFigmaReferences(o) {
|
|
|
5311
6478
|
await served.close();
|
|
5312
6479
|
}
|
|
5313
6480
|
await mkdir4(o.out, { recursive: true });
|
|
5314
|
-
const manifestPath =
|
|
6481
|
+
const manifestPath = join6(o.out, "manifest.json");
|
|
5315
6482
|
await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
5316
6483
|
`);
|
|
5317
6484
|
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
@@ -5319,10 +6486,10 @@ async function captureFigmaReferences(o) {
|
|
|
5319
6486
|
}
|
|
5320
6487
|
|
|
5321
6488
|
// src/index.ts
|
|
5322
|
-
import { execFileSync as
|
|
6489
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
5323
6490
|
|
|
5324
6491
|
// src/request.ts
|
|
5325
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
6492
|
+
import { execFileSync as execFileSync2, spawnSync } from "node:child_process";
|
|
5326
6493
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
5327
6494
|
import { BridgeContext as BridgeContext2, requests as requests2 } from "@bridge-ds/mcp";
|
|
5328
6495
|
|
|
@@ -5438,7 +6605,7 @@ var defaultIO = () => ({
|
|
|
5438
6605
|
env: process.env,
|
|
5439
6606
|
ghToken() {
|
|
5440
6607
|
try {
|
|
5441
|
-
return
|
|
6608
|
+
return execFileSync2("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 }).trim() || void 0;
|
|
5442
6609
|
} catch {
|
|
5443
6610
|
return void 0;
|
|
5444
6611
|
}
|
|
@@ -5824,6 +6991,7 @@ ${r.summary}`);
|
|
|
5824
6991
|
Version ${r.version}${r.edited ? ` (${r.edited})` : ""}`);
|
|
5825
6992
|
io.print(`Approved by: ${r.approvals.approvedBy.map((l) => `@${l}`).join(", ") || "nobody yet"}`);
|
|
5826
6993
|
if (r.approvals.missing.length) io.print(`Missing: ${r.approvals.missing.join(" and ")}`);
|
|
6994
|
+
if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
|
|
5827
6995
|
if (r.approvals.changesRequestedBy.length) io.print(`Changes requested by: ${r.approvals.changesRequestedBy.map((l) => `@${l}`).join(", ")}`);
|
|
5828
6996
|
for (const p of r.linkedPrs ?? []) io.print(`Pull request: #${p.number} ${p.merged ? "merged" : p.state} ${p.url}`);
|
|
5829
6997
|
io.print(`
|
|
@@ -5960,7 +7128,7 @@ function number(a, at, usage) {
|
|
|
5960
7128
|
if (!raw) throw new RequestUsageError(`Usage: ${usage}`);
|
|
5961
7129
|
return branches.branchNumber(raw);
|
|
5962
7130
|
}
|
|
5963
|
-
function
|
|
7131
|
+
function pairs2(list2, what) {
|
|
5964
7132
|
const out = {};
|
|
5965
7133
|
for (const s of list2 ?? []) {
|
|
5966
7134
|
const i = s.lastIndexOf("=");
|
|
@@ -5993,6 +7161,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
5993
7161
|
return out(r, () => {
|
|
5994
7162
|
io.print(`#${r.pr} ${r.title} (${r.product.name})`);
|
|
5995
7163
|
io.print(`${r.status.label}. ${r.approvalNeeded} needed; approved by ${r.approvals.approvedBy.map((x) => `@${x}`).join(", ") || "nobody yet"}.`);
|
|
7164
|
+
if (r.approvals.soloOwner) io.print(r.approvals.soloOwner);
|
|
5996
7165
|
if (r.merge.blockedBy?.length) for (const x of r.merge.blockedBy) io.print(` ${x}`);
|
|
5997
7166
|
io.print(`Next: ${r.next}`);
|
|
5998
7167
|
io.print(r.url);
|
|
@@ -6049,7 +7218,7 @@ async function runBranch(a, root, io = defaultIO()) {
|
|
|
6049
7218
|
}
|
|
6050
7219
|
case "update": {
|
|
6051
7220
|
const n = number(a, 2, "bridge branch update <n> [--choice key=mine|theirs] [--dry-run]");
|
|
6052
|
-
const choices =
|
|
7221
|
+
const choices = pairs2(a.flags.choice, "choice");
|
|
6053
7222
|
for (const [k, v] of Object.entries(choices)) if (v !== "mine" && v !== "theirs") throw new RequestUsageError(`--choice ${k}= takes mine or theirs, got "${v}".`);
|
|
6054
7223
|
const r = await branches.updateFromMain(ctx, { pr: n, ...Object.keys(choices).length ? { choices } : {}, dryRun: has4(a, "dry-run") });
|
|
6055
7224
|
return out(r, () => {
|
|
@@ -6067,7 +7236,7 @@ Share with a product team: ${r.share.install ? r.share.install : r.share.sentenc
|
|
|
6067
7236
|
}
|
|
6068
7237
|
case "combine": {
|
|
6069
7238
|
const prs = a._.slice(2);
|
|
6070
|
-
const picks = Object.fromEntries(Object.entries(
|
|
7239
|
+
const picks = Object.fromEntries(Object.entries(pairs2(a.flags.pick, "pick")).map(([k, v]) => [k, branches.branchNumber(v)]));
|
|
6071
7240
|
const r = await branches.combineBranches(ctx, { prs, ...Object.keys(picks).length ? { picks } : {} });
|
|
6072
7241
|
return out(r, () => {
|
|
6073
7242
|
io.print(`${r.tokens} tokens change together.`);
|
|
@@ -6098,6 +7267,7 @@ This folder: ${r2.current.name}. Pick another with --product <key>.`);
|
|
|
6098
7267
|
if (json) return io.print(JSON.stringify(r2, null, 2)), 0;
|
|
6099
7268
|
io.print(`Design approval: ${r2.roles.designApproval.join(", ") || "any reviewer"}`);
|
|
6100
7269
|
io.print(`Tech approval: ${r2.roles.techApproval.join(", ") || "any reviewer"}`);
|
|
7270
|
+
if (r2.roles.soloOwner) io.print(r2.roles.soloOwner);
|
|
6101
7271
|
io.print(`Merge: ${r2.roles.ship}`);
|
|
6102
7272
|
io.print(`Owner override: ${r2.ownerOverride.on ? `${r2.ownerOverride.who.join(", ")}. ${r2.ownerOverride.reason}` : r2.ownerOverride.reason}`);
|
|
6103
7273
|
io.print(`Protection: ${r2.protection}`);
|
|
@@ -6232,7 +7402,7 @@ ${r.fixes.sample.map((s) => ` ${s}`).join("\n")}`);
|
|
|
6232
7402
|
}
|
|
6233
7403
|
|
|
6234
7404
|
// src/figma.ts
|
|
6235
|
-
import { readFileSync as
|
|
7405
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
6236
7406
|
import { resolve as resolve4 } from "node:path";
|
|
6237
7407
|
import { BridgeContext as BridgeContext7, branches as branches3, figma } from "@bridge-ds/mcp";
|
|
6238
7408
|
var FIGMA_HELP = `bridge figma: component changes sent from Figma
|
|
@@ -6299,7 +7469,7 @@ ${r.brief}`);
|
|
|
6299
7469
|
if (!file) throw new RequestUsageError("Usage: bridge figma push <diff.json> [--dry-run]");
|
|
6300
7470
|
let diff2;
|
|
6301
7471
|
try {
|
|
6302
|
-
diff2 = JSON.parse(
|
|
7472
|
+
diff2 = JSON.parse(readFileSync3(resolve4(root, file), "utf8"));
|
|
6303
7473
|
} catch (err) {
|
|
6304
7474
|
throw new RequestUsageError(`${file} could not be read as JSON: ${err.message}`);
|
|
6305
7475
|
}
|
|
@@ -6319,6 +7489,94 @@ ${r.brief}`);
|
|
|
6319
7489
|
}
|
|
6320
7490
|
}
|
|
6321
7491
|
|
|
7492
|
+
// src/snapshot.ts
|
|
7493
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
7494
|
+
import { existsSync as existsSync7, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7495
|
+
import { basename, join as join7, relative as relative4, resolve as resolve5 } from "node:path";
|
|
7496
|
+
var git2 = (root, args) => {
|
|
7497
|
+
try {
|
|
7498
|
+
return execFileSync3("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7499
|
+
} catch {
|
|
7500
|
+
return "";
|
|
7501
|
+
}
|
|
7502
|
+
};
|
|
7503
|
+
var isReport = (x) => {
|
|
7504
|
+
const r = x;
|
|
7505
|
+
return !!r && typeof r === "object" && r.schemaVersion === 1 && typeof r.generatedAt === "string" && !!r.metrics && Array.isArray(r.files);
|
|
7506
|
+
};
|
|
7507
|
+
function reportFiles(root, entry) {
|
|
7508
|
+
const abs = resolve5(root, entry);
|
|
7509
|
+
if (!existsSync7(abs)) throw new Error(`Audit report not found: ${entry}`);
|
|
7510
|
+
if (!statSync4(abs).isDirectory()) return [abs];
|
|
7511
|
+
return readdirSync3(abs).filter((n) => n.endsWith(".json")).sort().map((n) => join7(abs, n));
|
|
7512
|
+
}
|
|
7513
|
+
async function writeSnapshot(opts) {
|
|
7514
|
+
const rec = recordingSource(fsSource(opts.root));
|
|
7515
|
+
const model = await loadRepoModel(rec.source);
|
|
7516
|
+
const audits = /* @__PURE__ */ new Map();
|
|
7517
|
+
for (const entry of opts.auditReports ?? []) {
|
|
7518
|
+
for (const file of reportFiles(opts.root, entry)) {
|
|
7519
|
+
let data;
|
|
7520
|
+
try {
|
|
7521
|
+
data = JSON.parse(readFileSync4(file, "utf8"));
|
|
7522
|
+
} catch (err) {
|
|
7523
|
+
throw new Error(`${relative4(opts.root, file)} is not valid JSON (${err.message})`);
|
|
7524
|
+
}
|
|
7525
|
+
if (!isReport(data)) continue;
|
|
7526
|
+
const id = auditId(basename(file));
|
|
7527
|
+
audits.set(id, { summary: snapshotAudit(id, data), report: data });
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
for (const spec of opts.audit ?? []) {
|
|
7531
|
+
const [path, label] = spec.split(/=(.*)/s, 2);
|
|
7532
|
+
const abs = resolve5(opts.root, path);
|
|
7533
|
+
if (!existsSync7(abs)) throw new Error(`No such file or folder to audit: ${path}`);
|
|
7534
|
+
const report = runAudit(abs, { ds: opts.root, version: opts.version, ...label ? { label } : {} });
|
|
7535
|
+
const id = auditId(label || relative4(opts.root, abs) || basename(abs));
|
|
7536
|
+
audits.set(id, { summary: snapshotAudit(id, report), report });
|
|
7537
|
+
}
|
|
7538
|
+
const snapshot = buildSnapshot({
|
|
7539
|
+
model,
|
|
7540
|
+
paths: rec.paths,
|
|
7541
|
+
files: rec.files,
|
|
7542
|
+
version: opts.version,
|
|
7543
|
+
...opts.branch ? { branch: opts.branch } : {},
|
|
7544
|
+
...git2(opts.root, ["log", "-1", "--format=%cI"]) ? { committedAt: git2(opts.root, ["log", "-1", "--format=%cI"]) } : {},
|
|
7545
|
+
...opts.generatedAt ? { generatedAt: opts.generatedAt } : {},
|
|
7546
|
+
audits: [...audits.values()].map((a) => a.summary)
|
|
7547
|
+
});
|
|
7548
|
+
const text = `${JSON.stringify(snapshot)}
|
|
7549
|
+
`;
|
|
7550
|
+
const bytes = Buffer.byteLength(text);
|
|
7551
|
+
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?`);
|
|
7552
|
+
rmSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true, force: true });
|
|
7553
|
+
mkdirSync(opts.out, { recursive: true });
|
|
7554
|
+
writeFileSync2(join7(opts.out, SNAPSHOT_FILE), text);
|
|
7555
|
+
writeFileSync2(join7(opts.out, SNAPSHOT_LLMS_FILE), snapshotLlmsTxt(model, snapshot));
|
|
7556
|
+
const written = [SNAPSHOT_FILE, SNAPSHOT_LLMS_FILE];
|
|
7557
|
+
if (audits.size) mkdirSync(join7(opts.out, SNAPSHOT_AUDITS_DIR), { recursive: true });
|
|
7558
|
+
for (const { summary, report } of audits.values()) {
|
|
7559
|
+
writeFileSync2(join7(opts.out, summary.path), `${JSON.stringify(report)}
|
|
7560
|
+
`);
|
|
7561
|
+
written.push(summary.path);
|
|
7562
|
+
}
|
|
7563
|
+
return { snapshot, written, bytes };
|
|
7564
|
+
}
|
|
7565
|
+
function snapshotLines(r, out) {
|
|
7566
|
+
const s = r.snapshot;
|
|
7567
|
+
const c = s.summary.contrast;
|
|
7568
|
+
const kb = Math.max(1, Math.round(r.bytes / 1024));
|
|
7569
|
+
return [
|
|
7570
|
+
`Wrote ${join7(out, SNAPSHOT_FILE)} (${kb} KB) for ${s.repo.owner}/${s.repo.name} at ${s.sourceSha.slice(0, 7)}.`,
|
|
7571
|
+
` tokens ${s.summary.tokens}`,
|
|
7572
|
+
` components ${s.summary.components ?? "not scanned"}`,
|
|
7573
|
+
` contrast ${c.checks} checks over ${c.pairs} pairs, ${c.failing} failing`,
|
|
7574
|
+
` figma ${s.summary.figma ? `${s.summary.figma.inFigma} of ${s.summary.figma.visual} visual components` : "no component-specs.json"}`,
|
|
7575
|
+
` audits ${s.audits.length ? s.audits.map((a) => a.path).join(", ") : "none (--audit <path> or --audit-report <file|dir>)"}`,
|
|
7576
|
+
...s.problems.map((p) => ` problem ${p}`)
|
|
7577
|
+
];
|
|
7578
|
+
}
|
|
7579
|
+
|
|
6322
7580
|
// src/protection.ts
|
|
6323
7581
|
var UNKNOWN = { kind: "unknown", missing: ["review", "checks", "history", "deletion"] };
|
|
6324
7582
|
async function protectionReport(opts) {
|
|
@@ -6411,6 +7669,13 @@ Usage:
|
|
|
6411
7669
|
Exit 0 ok, 1 under --fail-under or regressed, 2 setup error
|
|
6412
7670
|
bridge audit <path> --fix [--write] safe autofix: CSS values equal to a token become var(--token).
|
|
6413
7671
|
Dry run unless --write
|
|
7672
|
+
bridge snapshot [--out bridge-data] [--audit <path>[=label] ...] [--audit-report <file|dir> ...]
|
|
7673
|
+
[--branch main] [--json]
|
|
7674
|
+
the repo model of this checkout for hosted Bridge (Studio and Library):
|
|
7675
|
+
snapshot.json (token, contrast and component files, Figma coverage,
|
|
7676
|
+
contrast results) plus audits/<id>.json. --audit runs bridge audit on a
|
|
7677
|
+
product folder; --audit-report carries reports already written.
|
|
7678
|
+
The bridge-data workflow publishes the folder to the bridge-data branch
|
|
6414
7679
|
bridge docs draft <Component> [--json]
|
|
6415
7680
|
docs.json pre-fill from the code (props, tokens, anatomy, contrast,
|
|
6416
7681
|
changelog line, missing keys) and a prompt for Claude to write the
|
|
@@ -6499,9 +7764,9 @@ function printSent(r) {
|
|
|
6499
7764
|
if (r.hint) print(String(r.hint));
|
|
6500
7765
|
}
|
|
6501
7766
|
async function init(a, root) {
|
|
6502
|
-
const cfgPath =
|
|
7767
|
+
const cfgPath = join8(root, CONFIG_FILE2);
|
|
6503
7768
|
let config;
|
|
6504
|
-
if (
|
|
7769
|
+
if (existsSync8(cfgPath) && !has7(a, "force")) {
|
|
6505
7770
|
print(`${CONFIG_FILE2} exists, keeping it (--force to regenerate).`);
|
|
6506
7771
|
config = (await (await BridgeContext8.create({ root })).config()).config;
|
|
6507
7772
|
} else {
|
|
@@ -6515,11 +7780,11 @@ async function init(a, root) {
|
|
|
6515
7780
|
print("Check the layers order (lowest first) and the tiers; detection is a starting point.");
|
|
6516
7781
|
}
|
|
6517
7782
|
const serverJs = fileURLToPath(new URL("./index.js", import.meta.resolve("@bridge-ds/mcp")));
|
|
6518
|
-
const relServer =
|
|
7783
|
+
const relServer = relative5(root, serverJs);
|
|
6519
7784
|
const inRepo = !relServer.startsWith("..") && !isAbsolute(relServer);
|
|
6520
7785
|
const server = (dir) => inRepo ? { command: "node", args: [relServer, "--root", dir] } : { command: "npx", args: ["-y", MCP_PACKAGE, "--root", dir] };
|
|
6521
|
-
const mcpPath =
|
|
6522
|
-
const existing =
|
|
7786
|
+
const mcpPath = join8(root, ".mcp.json");
|
|
7787
|
+
const existing = existsSync8(mcpPath) ? JSON.parse(await readFile7(mcpPath, "utf8")) : {};
|
|
6523
7788
|
existing.mcpServers = { ...existing.mcpServers ?? {}, bridge: server(".") };
|
|
6524
7789
|
await writeFile5(mcpPath, `${JSON.stringify(existing, null, 2)}
|
|
6525
7790
|
`);
|
|
@@ -6551,8 +7816,8 @@ async function check(a, root) {
|
|
|
6551
7816
|
const tight = c.tightest?.[0];
|
|
6552
7817
|
if (tight) print(` tightest: ${tight.pair} [${tight.modes}] ${tight.ratio}:1 (min ${tight.min})`);
|
|
6553
7818
|
} else print(`contrast: ${c.message}`);
|
|
6554
|
-
for (const
|
|
6555
|
-
for (const
|
|
7819
|
+
for (const line2 of protection?.lines ?? []) print(line2);
|
|
7820
|
+
for (const line2 of roles?.lines ?? []) print(line2);
|
|
6556
7821
|
print(failed ? "FAIL" : "OK");
|
|
6557
7822
|
return failed ? 1 : 0;
|
|
6558
7823
|
}
|
|
@@ -6590,8 +7855,8 @@ async function diff(a, root) {
|
|
|
6590
7855
|
const before = await core.loadTokenSet(new GitRefSource(root, base), config);
|
|
6591
7856
|
const after = await ctx.tokens();
|
|
6592
7857
|
const changes = core.diff(before, after);
|
|
6593
|
-
const
|
|
6594
|
-
const delta2 =
|
|
7858
|
+
const pairs3 = await core.readContrastPairs(ctx.source, config).catch(() => []);
|
|
7859
|
+
const delta2 = pairs3.length ? core.contrastDelta(core.runContrast(before, pairs3, config.contrast?.modes), core.runContrast(after, pairs3, config.contrast?.modes)) : [];
|
|
6595
7860
|
const tier = core.classify(changes, [], config);
|
|
6596
7861
|
const imp = core.impact(before, after, changes, 30);
|
|
6597
7862
|
if (has7(a, "json")) {
|
|
@@ -6644,7 +7909,7 @@ async function propose(a, root) {
|
|
|
6644
7909
|
edits.push({ op: "set", path: s.slice(0, i), value: s.slice(i + 1), ...file ? { file } : {}, ...mode2 ? { mode: mode2 } : {} });
|
|
6645
7910
|
}
|
|
6646
7911
|
const editsFile = flag5(a, "edits");
|
|
6647
|
-
if (editsFile) edits.push(...JSON.parse(await readFile7(
|
|
7912
|
+
if (editsFile) edits.push(...JSON.parse(await readFile7(resolve6(editsFile), "utf8")));
|
|
6648
7913
|
if (!edits.length) throw new UsageError("Give at least one --set path=value or --edits file.json");
|
|
6649
7914
|
const pr = flag5(a, "pr");
|
|
6650
7915
|
const changes = changesFlag(a);
|
|
@@ -6730,19 +7995,19 @@ async function figmaReferences(a, root) {
|
|
|
6730
7995
|
let sha = flag5(a, "sha");
|
|
6731
7996
|
if (!sha) {
|
|
6732
7997
|
try {
|
|
6733
|
-
sha =
|
|
7998
|
+
sha = execFileSync4("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
|
6734
7999
|
} catch {
|
|
6735
8000
|
sha = "unknown";
|
|
6736
8001
|
}
|
|
6737
8002
|
}
|
|
6738
8003
|
const only = (flag5(a, "only") ?? "").split(",").filter(Boolean);
|
|
6739
8004
|
const r = await captureFigmaReferences({
|
|
6740
|
-
storybook:
|
|
6741
|
-
specs:
|
|
6742
|
-
out:
|
|
8005
|
+
storybook: resolve6(root, flag5(a, "storybook") ?? "storybook-static"),
|
|
8006
|
+
specs: resolve6(root, flag5(a, "specs") ?? "packages/react/component-specs.json"),
|
|
8007
|
+
out: resolve6(root, flag5(a, "out") ?? "figma-references"),
|
|
6743
8008
|
sha,
|
|
6744
8009
|
...only.length ? { only } : {},
|
|
6745
|
-
log: has7(a, "json") ? void 0 : (
|
|
8010
|
+
log: has7(a, "json") ? void 0 : (line2) => print(line2)
|
|
6746
8011
|
});
|
|
6747
8012
|
if (has7(a, "json")) print(JSON.stringify({ manifest: r.manifestPath, captured: r.captured, skipped: r.skipped }, null, 2));
|
|
6748
8013
|
return 0;
|
|
@@ -6762,10 +8027,10 @@ async function impact(a, root) {
|
|
|
6762
8027
|
root,
|
|
6763
8028
|
base: flag5(a, "base") ?? "main",
|
|
6764
8029
|
head: flag5(a, "head") ?? "HEAD",
|
|
6765
|
-
...out ? { out:
|
|
8030
|
+
...out ? { out: resolve6(out) } : {},
|
|
6766
8031
|
...only.length ? { only } : {},
|
|
6767
8032
|
...threshold !== void 0 ? { threshold } : {},
|
|
6768
|
-
...cacheDir ? { cacheDir:
|
|
8033
|
+
...cacheDir ? { cacheDir: resolve6(cacheDir) } : {},
|
|
6769
8034
|
...maxStories ? { maxStoriesPerComponent: maxStories } : {},
|
|
6770
8035
|
...imageBase !== void 0 ? { imageBase } : {},
|
|
6771
8036
|
...artifactUrl ? { artifactUrl } : {},
|
|
@@ -6857,8 +8122,8 @@ ${r.suggestion?.body ?? r.prBody}`);
|
|
|
6857
8122
|
async function audit(a, root) {
|
|
6858
8123
|
const target = a._[1];
|
|
6859
8124
|
if (!target) throw new UsageError("bridge audit <path> (a product folder or file that uses the design system)");
|
|
6860
|
-
if (!
|
|
6861
|
-
const dsRoot = flag5(a, "ds") ?
|
|
8125
|
+
if (!existsSync8(target)) throw new UsageError(`No such file or folder: ${target}`);
|
|
8126
|
+
const dsRoot = flag5(a, "ds") ? resolve6(flag5(a, "ds")) : existsSync8(join8(root, CONFIG_FILE2)) ? root : findDesignSystemRoot(root);
|
|
6862
8127
|
if (!dsRoot) throw new UsageError(`No ${CONFIG_FILE2} found: run bridge audit from the design system repo or pass --ds <dir>.`);
|
|
6863
8128
|
const ds = loadDesignSystem(dsRoot);
|
|
6864
8129
|
if (has7(a, "fix")) {
|
|
@@ -6872,12 +8137,12 @@ async function audit(a, root) {
|
|
|
6872
8137
|
const baselinePath = flag5(a, "baseline");
|
|
6873
8138
|
let baseline;
|
|
6874
8139
|
if (baselinePath) {
|
|
6875
|
-
if (!
|
|
8140
|
+
if (!existsSync8(baselinePath)) throw new UsageError(`Baseline not found: ${baselinePath}`);
|
|
6876
8141
|
baseline = JSON.parse(await readFile7(baselinePath, "utf8"));
|
|
6877
8142
|
}
|
|
6878
8143
|
const label = flag5(a, "label");
|
|
6879
8144
|
const allowlistPath = flag5(a, "allowlist");
|
|
6880
|
-
if (allowlistPath && !
|
|
8145
|
+
if (allowlistPath && !existsSync8(allowlistPath)) throw new UsageError(`Allowlist not found: ${allowlistPath}`);
|
|
6881
8146
|
let allowlist;
|
|
6882
8147
|
try {
|
|
6883
8148
|
allowlist = allowlistPath ? loadAllowlist(allowlistPath) : void 0;
|
|
@@ -6947,6 +8212,17 @@ async function audit(a, root) {
|
|
|
6947
8212
|
}
|
|
6948
8213
|
return code;
|
|
6949
8214
|
}
|
|
8215
|
+
async function snapshotCmd(a, root) {
|
|
8216
|
+
const out = resolve6(root, flag5(a, "out") ?? "bridge-data");
|
|
8217
|
+
const branch = flag5(a, "branch");
|
|
8218
|
+
const r = await writeSnapshot({ root, out, version: VERSION, audit: a.flags.audit ?? [], auditReports: a.flags["audit-report"] ?? [], ...branch ? { branch } : {} });
|
|
8219
|
+
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));
|
|
8220
|
+
else {
|
|
8221
|
+
const shown = relative5(process.cwd(), out);
|
|
8222
|
+
for (const line2 of snapshotLines(r, shown.startsWith("..") || isAbsolute(shown) ? out : shown || ".")) print(line2);
|
|
8223
|
+
}
|
|
8224
|
+
return 0;
|
|
8225
|
+
}
|
|
6950
8226
|
async function docsCmd(a, root) {
|
|
6951
8227
|
const [, sub, name] = a._;
|
|
6952
8228
|
if (sub !== "draft" || !name) throw new UsageError("Usage: bridge docs draft <Component> [--json]");
|
|
@@ -6983,7 +8259,7 @@ async function main() {
|
|
|
6983
8259
|
const argv = process.argv.slice(2);
|
|
6984
8260
|
const a = parse(argv);
|
|
6985
8261
|
const cmd = a._[0];
|
|
6986
|
-
const root =
|
|
8262
|
+
const root = resolve6(flag5(a, "root") ?? process.cwd());
|
|
6987
8263
|
if (cmd === "request") return runRequest(a, root);
|
|
6988
8264
|
if (cmd === "explore") return runExplore(a, root);
|
|
6989
8265
|
if (cmd === "branch") return runBranch(a, root);
|
|
@@ -7026,6 +8302,8 @@ async function main() {
|
|
|
7026
8302
|
return audit(a, root);
|
|
7027
8303
|
case "docs":
|
|
7028
8304
|
return docsCmd(a, root);
|
|
8305
|
+
case "snapshot":
|
|
8306
|
+
return snapshotCmd(a, root);
|
|
7029
8307
|
case "mcp":
|
|
7030
8308
|
await serveStdio(optionsFromArgv(argv.slice(1)));
|
|
7031
8309
|
return -1;
|