@bridge-ds/mcp 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-I3TL76GK.js → chunk-7HYTZVRW.js} +603 -158
- package/dist/{chunk-X455XY4E.js → chunk-K3HY24MZ.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/lib.js +2 -2
- package/dist/ops.d.ts +32 -17
- package/dist/{src-XSBCZV4W.js → src-V653Z7JC.js} +241 -5
- package/package.json +3 -3
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
unreadableTeams,
|
|
55
55
|
validateLayered,
|
|
56
56
|
whoCanShipSentence
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-K3HY24MZ.js";
|
|
58
58
|
|
|
59
59
|
// src/lib.ts
|
|
60
60
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -774,6 +774,12 @@ function classifyAuth(status, detail, opts = {}) {
|
|
|
774
774
|
}
|
|
775
775
|
return {};
|
|
776
776
|
}
|
|
777
|
+
function rateLimitMessage(x) {
|
|
778
|
+
const when = x.resetsAt ? ` It resets at ${x.resetsAt.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : "";
|
|
779
|
+
if (x.withToken) return `GitHub's hourly request limit for this token is used up.${when} Try again after that.`;
|
|
780
|
+
const hint = x.tokenHint ?? "add a GitHub token";
|
|
781
|
+
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.`;
|
|
782
|
+
}
|
|
777
783
|
var DEFAULT_RETRY = { attempts: 3, baseDelayMs: 600, sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
778
784
|
var isTransient = (status, detail) => status === 0 || status >= 500 || status === 429 || status === 403 && /secondary rate limit/i.test(detail);
|
|
779
785
|
var API = "https://api.github.com";
|
|
@@ -877,12 +883,8 @@ var GitHubClient = class _GitHubClient {
|
|
|
877
883
|
const remaining = res.headers?.get("x-ratelimit-remaining");
|
|
878
884
|
const reset = res.headers?.get("x-ratelimit-reset");
|
|
879
885
|
if ((res.status === 403 || res.status === 429) && remaining === "0") {
|
|
880
|
-
const
|
|
881
|
-
throw new GitHubError(
|
|
882
|
-
429,
|
|
883
|
-
`GitHub's hourly request limit for this token is used up.${when ? ` It resets at ${when.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}.` : ""} Try again after that.`,
|
|
884
|
-
detail
|
|
885
|
-
);
|
|
886
|
+
const resetsAt = reset ? new Date(Number(reset) * 1e3) : void 0;
|
|
887
|
+
throw new GitHubError(429, rateLimitMessage({ withToken: Boolean(this.token), resetsAt, tokenHint: this.options.tokenHint }), detail);
|
|
886
888
|
}
|
|
887
889
|
const info = classifyAuth(res.status, detail, {
|
|
888
890
|
path,
|
|
@@ -1853,10 +1855,10 @@ function planRestore(input) {
|
|
|
1853
1855
|
const from = input.label ? ` from ${input.label}` : "";
|
|
1854
1856
|
const planned = [{ key: keyOf(path, file), path, file, summary: `Set ${path} to ${show(input.value)} (${label})`, edit: { op: "set", path, file, value: input.value } }];
|
|
1855
1857
|
const title = `Restore ${path} (${label})${from}`;
|
|
1856
|
-
const
|
|
1858
|
+
const item2 = input.item;
|
|
1857
1859
|
const summary2 = [
|
|
1858
1860
|
`Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
|
|
1859
|
-
...
|
|
1861
|
+
...item2 ? ["", `That value came from ${item2.pr ? item2.pr.url ? `[#${item2.pr.number}](${item2.pr.url})` : `#${item2.pr.number}` : `commit ${item2.sha.slice(0, 7)}`} ("${item2.title}"), ${item2.date.slice(0, 10)}, by ${item2.author}.`] : []
|
|
1860
1862
|
].join("\n");
|
|
1861
1863
|
return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary: summary2, planned, conflicts: [], skipped: [] });
|
|
1862
1864
|
}
|
|
@@ -3046,6 +3048,8 @@ function explorationStatus(p) {
|
|
|
3046
3048
|
}
|
|
3047
3049
|
function promotionPlan(input) {
|
|
3048
3050
|
const exploration = parseExplorationBody(input.body);
|
|
3051
|
+
const jira = /^([A-Z][A-Z0-9_]*-\d+)\s+/.exec(input.title)?.[1] ?? null;
|
|
3052
|
+
const title = jira ? input.title.slice(jira.length).trim() : input.title;
|
|
3049
3053
|
const removeFiles = input.files.filter((f) => f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`) && f.status !== "removed").map((f) => f.path);
|
|
3050
3054
|
const files = input.files.filter((f) => !f.path.startsWith(`${EXPLORATION_NOTES_DIR}/`));
|
|
3051
3055
|
const risk = classifyRisk({ files, ...input.config ? { config: input.config } : {} });
|
|
@@ -3058,13 +3062,13 @@ function promotionPlan(input) {
|
|
|
3058
3062
|
const body = [
|
|
3059
3063
|
`**Promoted from an exploration on ${date}.** Review it like any change: the tier below comes from the files it touches.`,
|
|
3060
3064
|
"",
|
|
3061
|
-
kept || `## ${
|
|
3065
|
+
kept || `## ${title.replace(/^Exploration:\s*/i, "")}`,
|
|
3062
3066
|
"",
|
|
3063
3067
|
`**Risk tier:** ${risk.tier}.`,
|
|
3064
3068
|
...risk.reasons.slice(0, 8).map((r) => `- ${r}`),
|
|
3065
3069
|
...closes.length ? ["", ...closes.map((n) => `Closes #${n}`)] : []
|
|
3066
3070
|
].join("\n");
|
|
3067
|
-
const t =
|
|
3071
|
+
const t = title.replace(/^Exploration:\s*/i, "").trim() || exploration?.name || title;
|
|
3068
3072
|
return {
|
|
3069
3073
|
tier: risk.tier,
|
|
3070
3074
|
reasons: risk.reasons,
|
|
@@ -3072,7 +3076,7 @@ function promotionPlan(input) {
|
|
|
3072
3076
|
addLabels,
|
|
3073
3077
|
removeFiles,
|
|
3074
3078
|
body,
|
|
3075
|
-
title: t.charAt(0).toUpperCase()
|
|
3079
|
+
title: `${jira ? `${jira} ` : ""}${t.charAt(0).toUpperCase()}${t.slice(1)}`,
|
|
3076
3080
|
markReady: true
|
|
3077
3081
|
};
|
|
3078
3082
|
}
|
|
@@ -3298,29 +3302,29 @@ function sanitizeDraftText(raw) {
|
|
|
3298
3302
|
const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
|
|
3299
3303
|
return { title, summary: summary2 };
|
|
3300
3304
|
}
|
|
3301
|
-
function sanitizeDocsResult(
|
|
3302
|
-
const task =
|
|
3305
|
+
function sanitizeDocsResult(item2, r) {
|
|
3306
|
+
const task = item2.docs.task;
|
|
3303
3307
|
if (task.kind === "selection") {
|
|
3304
3308
|
const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
|
|
3305
3309
|
const replacement = sanitizeDocsReplacement(raw);
|
|
3306
|
-
if (!replacement) throw new Error(`Item "${
|
|
3310
|
+
if (!replacement) throw new Error(`Item "${item2.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item2.id}", "replacement": "..."}.`);
|
|
3307
3311
|
return { replacement };
|
|
3308
3312
|
}
|
|
3309
3313
|
try {
|
|
3310
3314
|
const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
|
|
3311
3315
|
return { docs: sections2, checked };
|
|
3312
3316
|
} catch {
|
|
3313
|
-
throw new Error(`Item "${
|
|
3317
|
+
throw new Error(`Item "${item2.id}" asks for the sections ${docsTaskKeys(task).join(", ")}: send {"id": "${item2.id}", "docs": {${task.keys.map((k) => `"${k}": ...`).join(", ")}}} in the shapes of its prompt.`);
|
|
3314
3318
|
}
|
|
3315
3319
|
}
|
|
3316
|
-
function sanitizeResultLines(
|
|
3320
|
+
function sanitizeResultLines(item2, raw) {
|
|
3317
3321
|
if (raw === void 0 || raw === null) return [];
|
|
3318
|
-
if (!Array.isArray(raw)) throw new Error(`lines for "${
|
|
3319
|
-
const known = new Set((
|
|
3322
|
+
if (!Array.isArray(raw)) throw new Error(`lines for "${item2.id}" must be a list of {"id", "line", "title"}.`);
|
|
3323
|
+
const known = new Set((item2.lines ?? []).map((l) => l.id));
|
|
3320
3324
|
const out = /* @__PURE__ */ new Map();
|
|
3321
3325
|
for (const l of raw) {
|
|
3322
3326
|
const id = typeof l?.id === "string" ? l.id : "";
|
|
3323
|
-
if (!known.has(id)) throw new Error(`Item "${
|
|
3327
|
+
if (!known.has(id)) throw new Error(`Item "${item2.id}" has no change line "${id}".${known.size ? ` Use the line ids from bridge_pending_drafts: ${[...known].join(", ")}.` : " It lists no lines: leave lines out."}`);
|
|
3324
3328
|
const line = sanitizeDraftLine(l.line);
|
|
3325
3329
|
const title = sanitizeDraftTitle(l.title);
|
|
3326
3330
|
if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
|
|
@@ -3337,13 +3341,13 @@ function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
|
|
|
3337
3341
|
const next = new Map((p.results ?? []).map((r) => [r.id, r]));
|
|
3338
3342
|
for (const r of results) {
|
|
3339
3343
|
if (!ids.has(r.id)) throw new Error(`Session ${p.sessionId} has no item "${r.id}". Use the ids from bridge_pending_drafts: ${[...ids].join(", ")}.`);
|
|
3340
|
-
const
|
|
3341
|
-
if (
|
|
3342
|
-
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(
|
|
3344
|
+
const item2 = p.items.find((i) => i.id === r.id);
|
|
3345
|
+
if (item2.scope === "docs") {
|
|
3346
|
+
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item2, r), by: "claude", at: at.toISOString() });
|
|
3343
3347
|
continue;
|
|
3344
3348
|
}
|
|
3345
3349
|
const clean = sanitizeDraftText(r);
|
|
3346
|
-
const lines = sanitizeResultLines(
|
|
3350
|
+
const lines = sanitizeResultLines(item2, r.lines);
|
|
3347
3351
|
if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
|
|
3348
3352
|
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
|
|
3349
3353
|
}
|
|
@@ -3433,7 +3437,7 @@ function explorationSummary(p, _owner, _repo, s) {
|
|
|
3433
3437
|
url: p.url,
|
|
3434
3438
|
branch: p.headRef,
|
|
3435
3439
|
slug: slug2,
|
|
3436
|
-
name: e?.name ?? p.title.replace(/^Exploration:\s*/i, ""),
|
|
3440
|
+
name: e?.name ?? p.title.replace(/^[A-Z][A-Z0-9_]*-\d+\s+/, "").replace(/^Exploration:\s*/i, ""),
|
|
3437
3441
|
purpose: e?.purpose ?? "",
|
|
3438
3442
|
...e?.useCase ? { useCase: e.useCase } : {},
|
|
3439
3443
|
status: explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body }),
|
|
@@ -3470,7 +3474,8 @@ Exploration: never merged as is. Promote it to review or close it.`,
|
|
|
3470
3474
|
files: [{ path: explorationNotePath(slug2), content: explorationNote(full) }]
|
|
3471
3475
|
});
|
|
3472
3476
|
const ctx = { owner: gh.owner, repo: gh.name, packages: s.packages, apps: s.apps, ...s.requests?.length ? { requests: s.requests } : {} };
|
|
3473
|
-
const
|
|
3477
|
+
const title = full.jira ? `${full.jira.trim().toUpperCase()} ${explorationPrTitle(full)}` : explorationPrTitle(full);
|
|
3478
|
+
const pr = await x.createPullRequest({ title, head: branch, base: s.base, body: explorationPrBody(full, ctx), draft: true });
|
|
3474
3479
|
await x.addLabels(pr.number, [EXPLORATION_LABEL]);
|
|
3475
3480
|
await x.updatePullRequest(pr.number, { body: explorationPrBody(full, { ...ctx, pr: pr.number }) }).catch(() => void 0);
|
|
3476
3481
|
return {
|
|
@@ -3731,6 +3736,10 @@ var EQUIVALENTS = [
|
|
|
3731
3736
|
[/^Badge$/, ["Badge"]],
|
|
3732
3737
|
[/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
|
|
3733
3738
|
[/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
|
|
3739
|
+
[/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
|
|
3740
|
+
[/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
|
|
3741
|
+
[/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
|
|
3742
|
+
[/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
|
|
3734
3743
|
[/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
|
|
3735
3744
|
[/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
|
|
3736
3745
|
[/^(LinearProgress|Progress)$/, ["Progress"]],
|
|
@@ -4277,6 +4286,26 @@ var THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|ex
|
|
|
4277
4286
|
var CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
|
|
4278
4287
|
var MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
|
|
4279
4288
|
var THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
|
|
4289
|
+
var DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
|
|
4290
|
+
function domRendererComponent(fn, components) {
|
|
4291
|
+
const m = DOM_RENDERER_RE.exec(fn);
|
|
4292
|
+
if (!m) return void 0;
|
|
4293
|
+
const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
|
|
4294
|
+
if (components.has(pascal2)) return pascal2;
|
|
4295
|
+
return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
|
|
4296
|
+
}
|
|
4297
|
+
var LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
|
|
4298
|
+
var LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
|
|
4299
|
+
var HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
|
|
4300
|
+
var HTML_ATTR_RE = /\s(data-tip)\s*=/g;
|
|
4301
|
+
function lookalikesInClassList(list, components) {
|
|
4302
|
+
const out = [];
|
|
4303
|
+
for (const token of list.split(/\s+/)) {
|
|
4304
|
+
const component = LOOKALIKE_CLASSES[token];
|
|
4305
|
+
if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
|
|
4306
|
+
}
|
|
4307
|
+
return out;
|
|
4308
|
+
}
|
|
4280
4309
|
function kindFor(path) {
|
|
4281
4310
|
return path.endsWith(".tsx") ? ts.ScriptKind.TSX : path.endsWith(".jsx") ? ts.ScriptKind.JSX : path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts") ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
|
|
4282
4311
|
}
|
|
@@ -4296,6 +4325,79 @@ function contextForDeclaration(name, typeText) {
|
|
|
4296
4325
|
if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
|
|
4297
4326
|
return null;
|
|
4298
4327
|
}
|
|
4328
|
+
function importsOf(path, text) {
|
|
4329
|
+
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
|
|
4330
|
+
const imports = /* @__PURE__ */ new Map();
|
|
4331
|
+
for (const node of sf.statements) {
|
|
4332
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
|
|
4333
|
+
const mod = node.moduleSpecifier.text;
|
|
4334
|
+
const c = node.importClause;
|
|
4335
|
+
if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
|
|
4336
|
+
if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
|
|
4337
|
+
if (c?.namedBindings && ts.isNamedImports(c.namedBindings)) for (const el of c.namedBindings.elements) imports.set(el.name.text, { module: mod, imported: (el.propertyName ?? el.name).text });
|
|
4338
|
+
}
|
|
4339
|
+
return imports;
|
|
4340
|
+
}
|
|
4341
|
+
function classifyTag(t, imports, ds) {
|
|
4342
|
+
const { name } = t;
|
|
4343
|
+
const at = { line: t.line, column: t.column };
|
|
4344
|
+
if (/^[a-z][\w-]*$/.test(name)) {
|
|
4345
|
+
if (!ds.nativeElements.includes(name)) return {};
|
|
4346
|
+
if (name === "input" && t.type === "hidden") return {};
|
|
4347
|
+
if (name === "a" && !t.hasHref) return {};
|
|
4348
|
+
if (t.inRenderProp) return {};
|
|
4349
|
+
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
4350
|
+
if (!suggestion) return {};
|
|
4351
|
+
return { component: { ...at, name, kind: "native", library: "native", suggestion } };
|
|
4352
|
+
}
|
|
4353
|
+
const root = name.split(".")[0];
|
|
4354
|
+
const imp = imports.get(root);
|
|
4355
|
+
const ns = imp?.imported === "*";
|
|
4356
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
4357
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
4358
|
+
if (imp) {
|
|
4359
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
4360
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
4361
|
+
if (kind === "ds") return { ds: leaf };
|
|
4362
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
4363
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
4364
|
+
if (t.inRenderProp) return {};
|
|
4365
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
4366
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
4367
|
+
}
|
|
4368
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
4369
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
4370
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
4371
|
+
}
|
|
4372
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
4373
|
+
return {};
|
|
4374
|
+
}
|
|
4375
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
4376
|
+
return {};
|
|
4377
|
+
}
|
|
4378
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
4379
|
+
const findings = [];
|
|
4380
|
+
let themeRefs = 0;
|
|
4381
|
+
for (const h of hits) {
|
|
4382
|
+
if (h.kind === "scale") {
|
|
4383
|
+
themeRefs++;
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
if (h.kind === "token") continue;
|
|
4387
|
+
let cat = h.category;
|
|
4388
|
+
const v = h.value;
|
|
4389
|
+
const isColor = Boolean(parseCssColor(v));
|
|
4390
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
4391
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
4392
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
4393
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
4394
|
+
if (cat === "color" && !isColor) continue;
|
|
4395
|
+
const suggestion = ds.suggest(cat, v);
|
|
4396
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
4397
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
4398
|
+
}
|
|
4399
|
+
return { findings, themeRefs };
|
|
4400
|
+
}
|
|
4299
4401
|
function scanCode(path, text, ds) {
|
|
4300
4402
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
4301
4403
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -4310,24 +4412,9 @@ function scanCode(path, text, ds) {
|
|
|
4310
4412
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
4311
4413
|
};
|
|
4312
4414
|
const addTailwind = (hits, offset) => {
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
continue;
|
|
4317
|
-
}
|
|
4318
|
-
if (h.kind === "token") continue;
|
|
4319
|
-
let cat = h.category;
|
|
4320
|
-
const v = h.value;
|
|
4321
|
-
const isColor = Boolean(parseCssColor(v));
|
|
4322
|
-
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
4323
|
-
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
4324
|
-
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
4325
|
-
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
4326
|
-
if (cat === "color" && !isColor) continue;
|
|
4327
|
-
const suggestion = ds.suggest(cat, v);
|
|
4328
|
-
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
4329
|
-
out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
4330
|
-
}
|
|
4415
|
+
const r = tailwindFindings(hits, offset, pos, ds);
|
|
4416
|
+
out.themeRefs += r.themeRefs;
|
|
4417
|
+
out.findings.push(...r.findings);
|
|
4331
4418
|
};
|
|
4332
4419
|
const styleValue = (key, node, ctx) => {
|
|
4333
4420
|
const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
|
|
@@ -4388,13 +4475,18 @@ function scanCode(path, text, ds) {
|
|
|
4388
4475
|
}
|
|
4389
4476
|
for (const a of scan.applies) addTailwind(a.hits, start + a.start);
|
|
4390
4477
|
};
|
|
4478
|
+
const lookalike = (list, offset) => {
|
|
4479
|
+
for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
4480
|
+
};
|
|
4391
4481
|
const classLiteral = (node) => {
|
|
4392
4482
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
4393
4483
|
handled.add(node);
|
|
4484
|
+
lookalike(node.text, node.getStart(sf) + 1);
|
|
4394
4485
|
addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
|
|
4395
4486
|
} else if (ts.isTemplateExpression(node)) {
|
|
4396
4487
|
for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
|
|
4397
4488
|
handled.add(p);
|
|
4489
|
+
lookalike(p.text, p.getStart(sf) + 1);
|
|
4398
4490
|
addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
|
|
4399
4491
|
}
|
|
4400
4492
|
node.templateSpans.forEach((s) => visit(s.expression, null, true));
|
|
@@ -4403,49 +4495,13 @@ function scanCode(path, text, ds) {
|
|
|
4403
4495
|
const jsxName = (n) => n.getText(sf);
|
|
4404
4496
|
const element = (node) => {
|
|
4405
4497
|
const name = jsxName(node.tagName);
|
|
4406
|
-
const
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
if (isRenderElement(node)) return;
|
|
4414
|
-
const suggestion = nativeEquivalent(name, type, ds.components);
|
|
4415
|
-
out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
|
|
4416
|
-
return;
|
|
4417
|
-
}
|
|
4418
|
-
const root = name.split(".")[0];
|
|
4419
|
-
const imp = imports.get(root);
|
|
4420
|
-
const ns = imp?.imported === "*";
|
|
4421
|
-
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
4422
|
-
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
4423
|
-
if (imp) {
|
|
4424
|
-
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
4425
|
-
if (isNonVisual(leaf.split(".").pop())) return;
|
|
4426
|
-
if (kind === "ds") {
|
|
4427
|
-
out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
|
|
4428
|
-
return;
|
|
4429
|
-
}
|
|
4430
|
-
if (kind === "charts") {
|
|
4431
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
|
|
4432
|
-
return;
|
|
4433
|
-
}
|
|
4434
|
-
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
4435
|
-
if (isRenderElement(node)) return;
|
|
4436
|
-
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
4437
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
4438
|
-
return;
|
|
4439
|
-
}
|
|
4440
|
-
if (kind === "foreign" || kind === "shadcn") {
|
|
4441
|
-
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
4442
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
4443
|
-
return;
|
|
4444
|
-
}
|
|
4445
|
-
if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
|
|
4446
|
-
return;
|
|
4447
|
-
}
|
|
4448
|
-
if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
|
|
4498
|
+
const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
|
|
4499
|
+
const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
|
|
4500
|
+
const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
|
|
4501
|
+
const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
|
|
4502
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
4503
|
+
if (r.component) out.components.push(r.component);
|
|
4504
|
+
if (r.localCandidate) pendingLocal.push(r.localCandidate);
|
|
4449
4505
|
};
|
|
4450
4506
|
const pendingLocal = [];
|
|
4451
4507
|
function visit(node, ctx, classCtx) {
|
|
@@ -4505,6 +4561,17 @@ function scanCode(path, text, ds) {
|
|
|
4505
4561
|
cssTemplate(node.template);
|
|
4506
4562
|
return;
|
|
4507
4563
|
}
|
|
4564
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
4565
|
+
const imp = imports.get(node.expression.text);
|
|
4566
|
+
if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
|
|
4567
|
+
const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
|
|
4568
|
+
if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
|
|
4572
|
+
visit(node.left, ctx, classCtx);
|
|
4573
|
+
return classLiteral(node.right);
|
|
4574
|
+
}
|
|
4508
4575
|
if (ts.isCallExpression(node)) {
|
|
4509
4576
|
const callee = node.expression.getText(sf);
|
|
4510
4577
|
if (CLASS_FNS.test(callee)) {
|
|
@@ -4549,6 +4616,14 @@ function scanCode(path, text, ds) {
|
|
|
4549
4616
|
const literal = (node) => {
|
|
4550
4617
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
4551
4618
|
const t = node.text;
|
|
4619
|
+
if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
|
|
4620
|
+
const base = node.getStart(sf) + 1;
|
|
4621
|
+
for (const m of t.matchAll(HTML_CLASS_RE)) for (const l of lookalikesInClassList(m[1], ds.components)) out.components.push({ ...pos(base + m.index), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
4622
|
+
for (const m of t.matchAll(HTML_ATTR_RE)) {
|
|
4623
|
+
const component = LOOKALIKE_ATTRS[m[1]];
|
|
4624
|
+
if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4552
4627
|
for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
4553
4628
|
if (ds.isDsVar(m[1])) out.tokenUsages++;
|
|
4554
4629
|
else out.themeRefs++;
|
|
@@ -4568,29 +4643,85 @@ function scanCode(path, text, ds) {
|
|
|
4568
4643
|
return out;
|
|
4569
4644
|
}
|
|
4570
4645
|
|
|
4571
|
-
// ../core/src/audit/
|
|
4572
|
-
var
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4646
|
+
// ../core/src/audit/ignore.ts
|
|
4647
|
+
var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
|
|
4648
|
+
function ignoreDirectives(text) {
|
|
4649
|
+
const at = lineCol(text);
|
|
4650
|
+
const out = [];
|
|
4651
|
+
for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
|
|
4652
|
+
const start = m.index;
|
|
4653
|
+
const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
|
|
4654
|
+
const open = opener?.[1];
|
|
4655
|
+
if (!open || open === "//" && opener[2].includes("\n")) continue;
|
|
4656
|
+
const from = start + m[0].length;
|
|
4657
|
+
let end;
|
|
4658
|
+
if (open === "//") {
|
|
4659
|
+
end = text.indexOf("\n", from);
|
|
4660
|
+
if (end < 0) end = text.length;
|
|
4661
|
+
} else {
|
|
4662
|
+
end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
|
|
4663
|
+
if (end < 0) continue;
|
|
4664
|
+
}
|
|
4665
|
+
const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
|
|
4666
|
+
out.push({ line: at(start).line, target: at(end).line + 1, reason });
|
|
4667
|
+
}
|
|
4668
|
+
return out;
|
|
4583
4669
|
}
|
|
4584
|
-
var
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4670
|
+
var item = (x, reason, via) => "category" in x ? { line: x.line, column: x.column, kind: "value", what: x.value, reason, via } : { line: x.line, column: x.column, kind: "component", what: x.name, reason, via };
|
|
4671
|
+
function applyIgnoreComments(text, findings, components) {
|
|
4672
|
+
const errors = [];
|
|
4673
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
4674
|
+
for (const d of ignoreDirectives(text)) {
|
|
4675
|
+
if (!d.reason) {
|
|
4676
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
|
|
4680
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
reasons.set(d.target, d.reason);
|
|
4684
|
+
}
|
|
4685
|
+
const ignored = [];
|
|
4686
|
+
const keep = (xs) => xs.filter((x) => {
|
|
4687
|
+
const r = reasons.get(x.line);
|
|
4688
|
+
if (r === void 0) return true;
|
|
4689
|
+
ignored.push(item(x, r, "comment"));
|
|
4690
|
+
return false;
|
|
4691
|
+
});
|
|
4692
|
+
return { findings: keep(findings), components: keep(components), ignored, errors };
|
|
4693
|
+
}
|
|
4694
|
+
function applyAllowlist(entries, findings, components) {
|
|
4695
|
+
const used = /* @__PURE__ */ new Set();
|
|
4696
|
+
const ignored = [];
|
|
4697
|
+
const keep = (xs) => xs.filter((x) => {
|
|
4698
|
+
const what = "category" in x ? x.value : x.name;
|
|
4699
|
+
const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
|
|
4700
|
+
let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
|
|
4701
|
+
if (i < 0) i = entries.findIndex(fits);
|
|
4702
|
+
if (i < 0) return true;
|
|
4703
|
+
used.add(i);
|
|
4704
|
+
ignored.push(item(x, entries[i].reason, "allowlist"));
|
|
4705
|
+
return false;
|
|
4706
|
+
});
|
|
4707
|
+
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
4708
|
+
}
|
|
4709
|
+
function parseAllowlist(json) {
|
|
4710
|
+
const fail2 = (msg) => {
|
|
4711
|
+
throw new Error(`allowlist: ${msg}`);
|
|
4712
|
+
};
|
|
4713
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail2('expected { "entries": [...] }');
|
|
4714
|
+
return json.entries.map((e, i) => {
|
|
4715
|
+
const x = e;
|
|
4716
|
+
if (typeof x.file !== "string" || !x.file) fail2(`entry ${i} needs "file"`);
|
|
4717
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail2(`entry ${i} (${x.file}) needs a "line" number`);
|
|
4718
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail2(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
4719
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail2(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
4720
|
+
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
4721
|
+
});
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
// ../core/src/audit/style.ts
|
|
4594
4725
|
function isSafeFix(h, s) {
|
|
4595
4726
|
if (!s || s.level !== "exact") return false;
|
|
4596
4727
|
switch (h.category) {
|
|
@@ -4615,49 +4746,346 @@ function isSafeFix(h, s) {
|
|
|
4615
4746
|
return false;
|
|
4616
4747
|
}
|
|
4617
4748
|
}
|
|
4618
|
-
function
|
|
4619
|
-
const
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4749
|
+
function auditCssText(text, lang, ds, opts) {
|
|
4750
|
+
const base = opts.offset ?? 0;
|
|
4751
|
+
const source = opts.source ?? "css";
|
|
4752
|
+
const at = (o) => opts.pos(base + o);
|
|
4753
|
+
const scan = scanCssText(text, lang);
|
|
4754
|
+
const findings = [];
|
|
4755
|
+
let tokenUsages = 0;
|
|
4756
|
+
let themeRefs = 0;
|
|
4757
|
+
for (const d of scan.decls) {
|
|
4758
|
+
for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
|
|
4759
|
+
if (d.scan.themeRef) themeRefs++;
|
|
4760
|
+
else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
|
|
4761
|
+
const property = d.property.toLowerCase();
|
|
4762
|
+
for (const h of d.scan.hits) {
|
|
4763
|
+
const suggestion = ds.suggest(h.category, h.text, property);
|
|
4764
|
+
const autofix = source === "css" && isSafeFix(h, suggestion);
|
|
4765
|
+
findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
for (const a of scan.applies) {
|
|
4769
|
+
for (const h of a.hits) {
|
|
4770
|
+
if (h.kind === "scale") themeRefs++;
|
|
4771
|
+
else if (h.kind !== "token" && h.value) {
|
|
4772
|
+
const cat = parseCssColor(h.value) ? "color" : h.category === "color-or-size" ? "font-size" : h.category === "color-or-width" ? "border-width" : h.category === "color-or-shadow" ? "shadow" : h.category === "font" ? "font-family" : h.category;
|
|
4773
|
+
const suggestion = ds.suggest(cat, h.value);
|
|
4774
|
+
findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
|
|
4775
|
+
} else if (h.kind === "token" && h.value) {
|
|
4776
|
+
const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
|
|
4777
|
+
if (v && ds.isDsVar(v)) tokenUsages++;
|
|
4778
|
+
else themeRefs++;
|
|
4635
4779
|
}
|
|
4636
4780
|
}
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4781
|
+
}
|
|
4782
|
+
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
4783
|
+
return { findings, tokenUsages, themeRefs };
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
// ../core/src/audit/markup.ts
|
|
4787
|
+
var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
|
|
4788
|
+
function mask2(text, ranges, keepInside) {
|
|
4789
|
+
const inside = new Uint8Array(text.length);
|
|
4790
|
+
for (const r of ranges) inside.fill(1, r.start, r.end);
|
|
4791
|
+
let out = "";
|
|
4792
|
+
for (let i = 0; i < text.length; i++) {
|
|
4793
|
+
const c = text[i];
|
|
4794
|
+
out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
|
|
4795
|
+
}
|
|
4796
|
+
return out;
|
|
4797
|
+
}
|
|
4798
|
+
var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
|
|
4799
|
+
var isCodeScript = (attrs) => {
|
|
4800
|
+
const type = attrOf(attrs, "type")?.toLowerCase();
|
|
4801
|
+
return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
|
|
4802
|
+
};
|
|
4803
|
+
function markupBlocks(text, lang) {
|
|
4804
|
+
const nonTemplate = [];
|
|
4805
|
+
let frontmatter;
|
|
4806
|
+
let from = 0;
|
|
4807
|
+
if (lang === "astro") {
|
|
4808
|
+
const open = /^\s*---[^\S\n]*\n/.exec(text);
|
|
4809
|
+
if (open) {
|
|
4810
|
+
const close = /\n---[^\S\n]*(\n|$)/g;
|
|
4811
|
+
close.lastIndex = open[0].length - 1;
|
|
4812
|
+
const m = close.exec(text);
|
|
4813
|
+
if (m) {
|
|
4814
|
+
frontmatter = { start: open[0].length, end: m.index + 1 };
|
|
4815
|
+
from = m.index + m[0].length;
|
|
4816
|
+
nonTemplate.push({ start: 0, end: from });
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
const scripts = [];
|
|
4821
|
+
const styles = [];
|
|
4822
|
+
const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
4823
|
+
re.lastIndex = from;
|
|
4824
|
+
for (let m; m = re.exec(text); ) {
|
|
4825
|
+
const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
|
|
4826
|
+
const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
|
|
4827
|
+
(m[1].toLowerCase() === "script" ? scripts : styles).push(block);
|
|
4828
|
+
nonTemplate.push({ start: m.index, end: m.index + m[0].length });
|
|
4829
|
+
}
|
|
4830
|
+
for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
|
|
4831
|
+
return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
|
|
4832
|
+
}
|
|
4833
|
+
function closeBrace(s, i) {
|
|
4834
|
+
let depth = 0;
|
|
4835
|
+
for (let k = i; k < s.length; k++) {
|
|
4836
|
+
const c = s[k];
|
|
4837
|
+
if (c === "{") depth++;
|
|
4838
|
+
else if (c === "}") {
|
|
4839
|
+
if (--depth === 0) return k + 1;
|
|
4840
|
+
} else if (c === '"' || c === "'") {
|
|
4841
|
+
const e = s.indexOf(c, k + 1);
|
|
4842
|
+
if (e < 0) return s.length;
|
|
4843
|
+
k = e;
|
|
4844
|
+
} else if (c === "`") {
|
|
4845
|
+
for (k++; k < s.length && s[k] !== "`"; k++) {
|
|
4846
|
+
if (s[k] === "\\") k++;
|
|
4847
|
+
else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
return s.length;
|
|
4852
|
+
}
|
|
4853
|
+
function readTag(s, i, lang) {
|
|
4854
|
+
const attrs = [];
|
|
4855
|
+
let k = i;
|
|
4856
|
+
while (k < s.length) {
|
|
4857
|
+
while (k < s.length && /\s/.test(s[k])) k++;
|
|
4858
|
+
const c = s[k];
|
|
4859
|
+
if (c === void 0) break;
|
|
4860
|
+
if (c === ">") return { attrs, end: k + 1, selfClosing: false };
|
|
4861
|
+
if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
|
|
4862
|
+
if (c === "{") {
|
|
4863
|
+
const e = closeBrace(s, k);
|
|
4864
|
+
const inner = s.slice(k + 1, e - 1).trim();
|
|
4865
|
+
attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
|
|
4866
|
+
k = e;
|
|
4867
|
+
continue;
|
|
4868
|
+
}
|
|
4869
|
+
const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
|
|
4870
|
+
if (!nm) {
|
|
4871
|
+
k++;
|
|
4872
|
+
continue;
|
|
4873
|
+
}
|
|
4874
|
+
const name = nm[0];
|
|
4875
|
+
k += name.length;
|
|
4876
|
+
let j = k;
|
|
4877
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
4878
|
+
if (s[j] !== "=") {
|
|
4879
|
+
attrs.push({ name, kind: "bare", value: "", valueStart: k });
|
|
4880
|
+
continue;
|
|
4881
|
+
}
|
|
4882
|
+
j++;
|
|
4883
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
4884
|
+
const q = s[j];
|
|
4885
|
+
const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
|
|
4886
|
+
if (q === '"' || q === "'") {
|
|
4887
|
+
const e = s.indexOf(q, j + 1);
|
|
4888
|
+
const end = e < 0 ? s.length : e;
|
|
4889
|
+
attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
|
|
4890
|
+
k = end + 1;
|
|
4891
|
+
} else if (q === "{") {
|
|
4892
|
+
const e = closeBrace(s, j);
|
|
4893
|
+
attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
|
|
4894
|
+
k = e;
|
|
4895
|
+
} else {
|
|
4896
|
+
const m = /^[^\s>]+/.exec(s.slice(j));
|
|
4897
|
+
const v = m?.[0].replace(/\/$/, "") ?? "";
|
|
4898
|
+
attrs.push({ name, kind: "static", value: v, valueStart: j });
|
|
4899
|
+
k = j + v.length;
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
return { attrs, end: s.length, selfClosing: false };
|
|
4903
|
+
}
|
|
4904
|
+
var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
|
|
4905
|
+
var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
|
|
4906
|
+
var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
|
|
4907
|
+
function scanMarkup(path, text, lang, ds) {
|
|
4908
|
+
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
4909
|
+
const pos = lineCol(text);
|
|
4910
|
+
const blocks = markupBlocks(text, lang);
|
|
4911
|
+
const imports = /* @__PURE__ */ new Map();
|
|
4912
|
+
const merge = (c) => {
|
|
4913
|
+
out.findings.push(...c.findings);
|
|
4914
|
+
out.tokenUsages += c.tokenUsages;
|
|
4915
|
+
out.themeRefs += c.themeRefs;
|
|
4916
|
+
for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
|
|
4917
|
+
out.components.push(...c.components);
|
|
4918
|
+
out.definitions.push(...c.definitions);
|
|
4919
|
+
};
|
|
4920
|
+
const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
|
|
4921
|
+
for (const r of codeRanges) {
|
|
4922
|
+
const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
|
|
4923
|
+
const code2 = mask2(text, [r], true);
|
|
4924
|
+
const file = `${path}.${jsx ? "tsx" : "ts"}`;
|
|
4925
|
+
merge(scanCode(file, code2, ds));
|
|
4926
|
+
for (const [k, v] of importsOf(file, code2)) imports.set(k, v);
|
|
4927
|
+
}
|
|
4928
|
+
for (const b of blocks.styles) {
|
|
4929
|
+
const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
|
|
4930
|
+
const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
|
|
4931
|
+
out.findings.push(...r.findings);
|
|
4932
|
+
out.tokenUsages += r.tokenUsages;
|
|
4933
|
+
out.themeRefs += r.themeRefs;
|
|
4934
|
+
}
|
|
4935
|
+
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
4936
|
+
const { line, column } = pos(offset);
|
|
4937
|
+
const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
|
|
4938
|
+
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
4939
|
+
const c = scanCode(`${path}.tsx`, src, ds);
|
|
4940
|
+
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
4941
|
+
};
|
|
4942
|
+
const styleAttr = (a) => {
|
|
4943
|
+
if (a.kind === "static") {
|
|
4944
|
+
const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
|
|
4945
|
+
const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
|
|
4946
|
+
out.findings.push(...r.findings);
|
|
4947
|
+
out.tokenUsages += r.tokenUsages;
|
|
4948
|
+
out.themeRefs += r.themeRefs;
|
|
4949
|
+
return;
|
|
4950
|
+
}
|
|
4951
|
+
const str2 = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
4952
|
+
if (str2) {
|
|
4953
|
+
const body = str2[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
4954
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str2[1].length + 1, source: "style" });
|
|
4955
|
+
out.findings.push(...r.findings);
|
|
4956
|
+
out.tokenUsages += r.tokenUsages;
|
|
4957
|
+
out.themeRefs += r.themeRefs;
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
|
|
4961
|
+
};
|
|
4962
|
+
const tpl = mask2(text, blocks.nonTemplate, false);
|
|
4963
|
+
const pending = [];
|
|
4964
|
+
let noscript = 0;
|
|
4965
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
|
|
4966
|
+
for (let m; m = TAG.exec(tpl); ) {
|
|
4967
|
+
const raw = m[2];
|
|
4968
|
+
if (m[1]) {
|
|
4969
|
+
if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
|
|
4970
|
+
continue;
|
|
4971
|
+
}
|
|
4972
|
+
const tag = readTag(tpl, m.index + m[0].length, lang);
|
|
4973
|
+
TAG.lastIndex = tag.end;
|
|
4974
|
+
const lower = raw.toLowerCase();
|
|
4975
|
+
if (lower === "noscript" && !tag.selfClosing) {
|
|
4976
|
+
noscript++;
|
|
4977
|
+
continue;
|
|
4978
|
+
}
|
|
4979
|
+
const attrs = tag.attrs;
|
|
4980
|
+
const get = (n) => attrs.find((a) => attrName(a.name) === n);
|
|
4981
|
+
for (const a of attrs) {
|
|
4982
|
+
const n = attrName(a.name);
|
|
4983
|
+
if (n === "style") styleAttr(a);
|
|
4984
|
+
else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
|
|
4985
|
+
const prop = a.name.slice(6).split("|")[0];
|
|
4986
|
+
const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
|
|
4987
|
+
out.findings.push(...r2.findings);
|
|
4988
|
+
out.tokenUsages += r2.tokenUsages;
|
|
4989
|
+
out.themeRefs += r2.themeRefs;
|
|
4990
|
+
} else if (/^(class|className|class:list)$/.test(n)) {
|
|
4991
|
+
if (a.kind === "static") {
|
|
4992
|
+
for (const l of lookalikesInClassList(a.value, ds.components)) out.components.push({ ...pos(a.valueStart), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
4993
|
+
const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
|
|
4994
|
+
out.findings.push(...r2.findings);
|
|
4995
|
+
out.themeRefs += r2.themeRefs;
|
|
4996
|
+
} else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
|
|
4997
|
+
} else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
|
|
4998
|
+
out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
|
|
4999
|
+
} else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
|
|
5000
|
+
const suggestion = ds.suggest("color", a.value.trim(), n);
|
|
5001
|
+
out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
|
|
4649
5002
|
}
|
|
4650
5003
|
}
|
|
4651
|
-
|
|
4652
|
-
|
|
5004
|
+
let name = raw;
|
|
5005
|
+
if (raw.includes(":")) continue;
|
|
5006
|
+
if (raw.includes("-")) {
|
|
5007
|
+
if (!imports.has(pascal(raw))) continue;
|
|
5008
|
+
name = pascal(raw);
|
|
5009
|
+
}
|
|
5010
|
+
const isNative = /^[a-z]/.test(name) && !name.includes(".");
|
|
5011
|
+
if (isNative && noscript > 0) continue;
|
|
5012
|
+
const type = get("type");
|
|
5013
|
+
const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
|
|
5014
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
5015
|
+
if (r.component) out.components.push(r.component);
|
|
5016
|
+
if (r.localCandidate) pending.push(r.localCandidate);
|
|
5017
|
+
}
|
|
5018
|
+
const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
|
|
5019
|
+
if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
|
|
5020
|
+
const defined = new Set(out.definitions.map((d) => d.name));
|
|
5021
|
+
out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
|
|
5022
|
+
out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
5023
|
+
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
5024
|
+
return out;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
// ../core/src/audit/types.ts
|
|
5028
|
+
var AUDIT_SCHEMA_VERSION = 1;
|
|
5029
|
+
var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width", "shadow", "duration", "easing", "z-index", "font-family", "font-size", "font-weight", "line-height"];
|
|
5030
|
+
|
|
5031
|
+
// ../core/src/audit/report.ts
|
|
5032
|
+
var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
|
|
5033
|
+
var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
|
|
5034
|
+
var IGNORE_FILE = /\.d\.[cm]?ts$|\.(test|spec|stories|story)\.[cm]?[jt]sx?$|\.min\.(css|js)$|(^|\/)(vite|vitest|jest|webpack|rollup|eslint|postcss|tailwind|babel|next|astro|prettier|stylelint|tsup)\.config\.[cm]?[jt]s$/;
|
|
5035
|
+
function isAuditable(path) {
|
|
5036
|
+
if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
|
|
5037
|
+
if (IGNORE_FILE.test(path)) return false;
|
|
5038
|
+
return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
|
|
5039
|
+
}
|
|
5040
|
+
var langOf = (path) => {
|
|
5041
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
5042
|
+
if (ext === "css") return "css";
|
|
5043
|
+
if (ext === "scss" || ext === "sass") return "scss";
|
|
5044
|
+
if (ext === "less") return "less";
|
|
5045
|
+
if (ext === "tsx") return "tsx";
|
|
5046
|
+
if (ext === "jsx") return "jsx";
|
|
5047
|
+
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
5048
|
+
if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
|
|
5049
|
+
return "js";
|
|
5050
|
+
};
|
|
5051
|
+
function auditFile(path, text, ds) {
|
|
5052
|
+
const lang = langOf(path);
|
|
5053
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
5054
|
+
const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
|
|
5055
|
+
return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
|
|
4653
5056
|
}
|
|
4654
|
-
const c = scanCode(path, text, ds);
|
|
5057
|
+
const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
|
|
4655
5058
|
const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
|
|
4656
|
-
return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded:
|
|
5059
|
+
return withIgnores(text, { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded: 0, dsComponents: dsCount, otherComponents: 0, findings: c.findings, components: c.components, definitions: c.definitions, dsByName: c.ds });
|
|
5060
|
+
}
|
|
5061
|
+
function withIgnores(text, f) {
|
|
5062
|
+
const r = applyIgnoreComments(text, f.findings, f.components);
|
|
5063
|
+
return {
|
|
5064
|
+
...f,
|
|
5065
|
+
findings: r.findings,
|
|
5066
|
+
components: r.components,
|
|
5067
|
+
hardcoded: r.findings.length,
|
|
5068
|
+
otherComponents: r.components.length,
|
|
5069
|
+
...r.ignored.length ? { ignored: r.ignored } : {},
|
|
5070
|
+
...r.errors.length ? { ignoreErrors: r.errors } : {}
|
|
5071
|
+
};
|
|
4657
5072
|
}
|
|
4658
5073
|
var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
|
|
4659
5074
|
function buildAuditReport(input, ds) {
|
|
4660
|
-
const
|
|
5075
|
+
const allow = input.allowlist ?? [];
|
|
5076
|
+
const usedAllow = /* @__PURE__ */ new Set();
|
|
5077
|
+
const reports = input.files.map((f) => {
|
|
5078
|
+
const r = auditFile(f.path, f.text, ds);
|
|
5079
|
+
const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
|
|
5080
|
+
if (!idx.length) return r;
|
|
5081
|
+
const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
|
|
5082
|
+
a.used.forEach((k) => usedAllow.add(idx[k]));
|
|
5083
|
+
if (!a.ignored.length) return r;
|
|
5084
|
+
const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
|
|
5085
|
+
return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
|
|
5086
|
+
});
|
|
5087
|
+
const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
|
|
5088
|
+
const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
|
|
4661
5089
|
const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
|
|
4662
5090
|
const byCategory = {};
|
|
4663
5091
|
const fixes = /* @__PURE__ */ new Map();
|
|
@@ -4717,6 +5145,15 @@ function buildAuditReport(input, ds) {
|
|
|
4717
5145
|
topFiles,
|
|
4718
5146
|
topFixes,
|
|
4719
5147
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
5148
|
+
...ignoredItems.length || staleAllowlist.length ? {
|
|
5149
|
+
ignored: {
|
|
5150
|
+
total: ignoredItems.length,
|
|
5151
|
+
comment: ignoredItems.filter((x) => x.via === "comment").length,
|
|
5152
|
+
allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
|
|
5153
|
+
items: ignoredItems,
|
|
5154
|
+
staleAllowlist
|
|
5155
|
+
}
|
|
5156
|
+
} : {},
|
|
4720
5157
|
history: []
|
|
4721
5158
|
};
|
|
4722
5159
|
}
|
|
@@ -5030,6 +5467,9 @@ function collectAuditFiles(target, exclude = []) {
|
|
|
5030
5467
|
}
|
|
5031
5468
|
return { root: abs, files, skipped };
|
|
5032
5469
|
}
|
|
5470
|
+
function loadAllowlist(path) {
|
|
5471
|
+
return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
|
|
5472
|
+
}
|
|
5033
5473
|
|
|
5034
5474
|
// src/config.ts
|
|
5035
5475
|
var SCHEMA_URL = "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json";
|
|
@@ -5287,7 +5727,8 @@ function client(repo, o, rules) {
|
|
|
5287
5727
|
...o.apiUrl ? { apiUrl: o.apiUrl } : {},
|
|
5288
5728
|
...o.rawUrl ? { rawUrl: o.rawUrl } : {},
|
|
5289
5729
|
protectedBranches: rules.protectedBranches,
|
|
5290
|
-
branchPrefix: rules.branchPrefix
|
|
5730
|
+
branchPrefix: rules.branchPrefix,
|
|
5731
|
+
tokenHint: "set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN) in the environment of the Bridge MCP server, read access to this repository is enough"
|
|
5291
5732
|
});
|
|
5292
5733
|
}
|
|
5293
5734
|
var BridgeContext = class _BridgeContext {
|
|
@@ -6585,7 +7026,7 @@ async function visualImpact(ctx, a, run2) {
|
|
|
6585
7026
|
if (ctx.mode !== "local" || !ctx.root) throw new Error("Visual impact needs a local checkout (start Bridge with --root): it builds Storybook and the app at both refs.");
|
|
6586
7027
|
if (!ctx.isGit) throw new Error(`${ctx.root} is not a git repository.`);
|
|
6587
7028
|
const out = a.out ? resolve3(ctx.root, a.out) : join4(ctx.root, "visual-impact");
|
|
6588
|
-
const runImpact = run2 ?? (await import("./src-
|
|
7029
|
+
const runImpact = run2 ?? (await import("./src-V653Z7JC.js")).runImpact;
|
|
6589
7030
|
const r = await runImpact({
|
|
6590
7031
|
root: ctx.root,
|
|
6591
7032
|
base: a.base ?? "main",
|
|
@@ -6656,7 +7097,8 @@ async function auditOp(ctx, a) {
|
|
|
6656
7097
|
if (!existsSync2(target)) throw new Error(`No such file or folder: ${target}`);
|
|
6657
7098
|
const ds = await designSystemIndex(ctx);
|
|
6658
7099
|
const { files, skipped } = collectAuditFiles(target, config.audit?.exclude ?? []);
|
|
6659
|
-
|
|
7100
|
+
const allowlist = a.allowlist ? loadAllowlist(isAbsolute(a.allowlist) ? a.allowlist : resolve3(ctx.root, a.allowlist)) : void 0;
|
|
7101
|
+
let report = buildAuditReport({ files, skipped, target: a.path, ...a.label ? { label: a.label } : {}, ...allowlist ? { allowlist } : {} }, ds);
|
|
6660
7102
|
if (a.baseline) report = compareWithBaseline(report, JSON.parse(readFileSync2(isAbsolute(a.baseline) ? a.baseline : resolve3(ctx.root, a.baseline), "utf8")));
|
|
6661
7103
|
const limit = a.limit ?? 10;
|
|
6662
7104
|
const single = files.length === 1;
|
|
@@ -6673,6 +7115,8 @@ async function auditOp(ctx, a) {
|
|
|
6673
7115
|
native: report.components.native,
|
|
6674
7116
|
recreated: report.components.recreated
|
|
6675
7117
|
},
|
|
7118
|
+
...report.ignored ? { ignored: { total: report.ignored.total, comment: report.ignored.comment, allowlist: report.ignored.allowlist, items: report.ignored.items.slice(0, limit * 5), staleAllowlist: report.ignored.staleAllowlist } } : {},
|
|
7119
|
+
...report.files.some((f) => f.ignoreErrors?.length) ? { ignoreErrors: report.files.flatMap((f) => (f.ignoreErrors ?? []).map((e) => ({ file: f.path, ...e }))) } : {},
|
|
6676
7120
|
topFiles: report.topFiles.slice(0, limit),
|
|
6677
7121
|
topFixes: report.topFixes.slice(0, limit).map((f) => ({ value: f.value, category: f.category, count: f.count, ...f.suggestion ? { use: `var(${f.suggestion.cssVar})`, token: f.suggestion.token, tokenValue: f.suggestion.value, ...f.suggestion.mode ? { mode: f.suggestion.mode } : {}, confidence: f.suggestion.level } : { use: null } })),
|
|
6678
7122
|
...withFindings ? {
|
|
@@ -6807,19 +7251,19 @@ async function buildPlan(ctx, a, tool) {
|
|
|
6807
7251
|
file = v[v.length - 1].file;
|
|
6808
7252
|
}
|
|
6809
7253
|
let value = r.value;
|
|
6810
|
-
let
|
|
7254
|
+
let item2;
|
|
6811
7255
|
if (value === void 0) {
|
|
6812
7256
|
if (!r.entry) throw new Error("restore needs value, or entry (the history entry whose value to put back).");
|
|
6813
7257
|
const e = findEntry(report.entries, r.entry);
|
|
6814
7258
|
const items = tokenHistory([e], path, config).filter((i) => !file || i.file === file);
|
|
6815
7259
|
if (!items.length) throw new Error(`${e.id} did not change ${path}${file ? ` in ${file}` : ""}. bridge_get_token_history lists the changes of a token.`);
|
|
6816
7260
|
if (items.length > 1 && !file) throw new Error(`${e.id} changed ${path} in ${items.map((i) => i.file).join(", ")}. Pass mode (for example {"theme":"dark"}) or file.`);
|
|
6817
|
-
|
|
6818
|
-
file ??=
|
|
6819
|
-
value = r.side === "before" ?
|
|
6820
|
-
if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${
|
|
7261
|
+
item2 = items[0];
|
|
7262
|
+
file ??= item2.file;
|
|
7263
|
+
value = r.side === "before" ? item2.before : item2.after;
|
|
7264
|
+
if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${item2.kind}).`);
|
|
6821
7265
|
}
|
|
6822
|
-
return planRestore({ current, path, ...file ? { file } : {}, value, ...
|
|
7266
|
+
return planRestore({ current, path, ...file ? { file } : {}, value, ...item2 ? { item: item2, label: item2.pr ? `#${item2.pr.number}` : item2.entryId } : {}, pairs, tool });
|
|
6823
7267
|
}
|
|
6824
7268
|
const entry = findEntry(report.entries, a.target);
|
|
6825
7269
|
const later = report.entries.filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
|
|
@@ -7176,11 +7620,11 @@ async function pendingDrafts(ctx, a) {
|
|
|
7176
7620
|
const images = [];
|
|
7177
7621
|
if (a.images !== false) {
|
|
7178
7622
|
for (const s of sessions) {
|
|
7179
|
-
for (const
|
|
7180
|
-
for (const img of
|
|
7623
|
+
for (const item2 of s.items) {
|
|
7624
|
+
for (const img of item2.images ?? []) {
|
|
7181
7625
|
if (images.length >= IMAGES_PER_CALL || !img.path) continue;
|
|
7182
7626
|
const f = await gh.getFileBase64(img.path, REQUESTS_BRANCH).catch(() => void 0);
|
|
7183
|
-
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId:
|
|
7627
|
+
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId: item2.id, label: img.label, ...img.line ? { line: img.line } : {}, base64: f.base64 });
|
|
7184
7628
|
}
|
|
7185
7629
|
}
|
|
7186
7630
|
}
|
|
@@ -7212,7 +7656,7 @@ async function writeDrafts(ctx, a) {
|
|
|
7212
7656
|
}
|
|
7213
7657
|
|
|
7214
7658
|
// src/server.ts
|
|
7215
|
-
var VERSION = true ? "0.3.
|
|
7659
|
+
var VERSION = true ? "0.3.3" : "dev";
|
|
7216
7660
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
7217
7661
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
7218
7662
|
var run = (fn) => async (a) => {
|
|
@@ -7570,13 +8014,14 @@ function createServer(ctx) {
|
|
|
7570
8014
|
"bridge_audit",
|
|
7571
8015
|
{
|
|
7572
8016
|
title: "Audit design system adoption",
|
|
7573
|
-
description: "Measure how a product codebase uses this design system (local only).
|
|
8017
|
+
description: "Measure how a product codebase uses this design system (local only). Reads CSS, SCSS, Less, TS/JS, TSX/JSX, and Astro, Vue and Svelte files (template markup, frontmatter and <script> blocks, <style> blocks). Finds hardcoded colors, spacing, radii, shadows, durations, z-index and fonts in style sheets, style attributes, style/sx/styled objects, CSS-in-JS and Tailwind classes, each with the closest token (colors by deltaE across every mode) and a confidence; and in JSX and template markup, DS components versus foreign UI libraries (MUI, react-admin, Radix, shadcn/ui...), native elements a DS component should render, and re-created DS components. Hidden inputs and elements inside <noscript> do not count. A `bridge-audit-ignore-next-line: reason` comment on the line above leaves one exception out; an allowlist file lists known file:line exceptions; both need a reason and stay listed under `ignored`. Returns token and component adoption, counts by category, top files and top fixes; for a single file (or findings: true) the findings with line numbers.",
|
|
7574
8018
|
inputSchema: {
|
|
7575
8019
|
path: z.string().min(1).describe("Product folder or file, absolute or relative to the design system repo root."),
|
|
7576
8020
|
baseline: z.string().optional().describe("Earlier report from `bridge audit --json` to compare with (progress per metric and file)."),
|
|
7577
8021
|
label: z.string().optional(),
|
|
7578
8022
|
limit: z.number().int().min(1).max(50).optional().describe("Top files and fixes to return. Default 10."),
|
|
7579
|
-
findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file.")
|
|
8023
|
+
findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file."),
|
|
8024
|
+
allowlist: z.string().optional().describe('Allowlist file of known exceptions: { "entries": [{ "file", "line", "what"?, "reason" }] }, paths relative to the audited folder.')
|
|
7580
8025
|
},
|
|
7581
8026
|
annotations: RO
|
|
7582
8027
|
},
|