@bridge-ds/mcp 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -12
- package/dist/{chunk-GABRGCKY.js → chunk-MTQCDV65.js} +617 -153
- package/dist/explorations.d.ts +10 -1
- package/dist/index.js +1 -1
- package/dist/lib.js +1 -1
- package/dist/ops.d.ts +32 -17
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,28 +7,31 @@ propose token changes as reviewable branches and pull requests. It never writes
|
|
|
7
7
|
Free and open source (MIT). Local, no backend, no account: it reads files on your disk, or a GitHub repo
|
|
8
8
|
with your own token.
|
|
9
9
|
|
|
10
|
-
> On npm: `npx -y @bridge-ds/mcp` runs it, nothing to build. The local path form (`node packages/mcp/dist/index.js`)
|
|
11
|
-
> is for working on the server itself.
|
|
12
|
-
|
|
13
10
|
## Run it
|
|
14
11
|
|
|
12
|
+
From npm, nothing to build:
|
|
13
|
+
|
|
15
14
|
```sh
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
npx -y @bridge-ds/mcp --repo owner/name # GitHub repo, read-only
|
|
16
|
+
GITHUB_TOKEN=... npx -y @bridge-ds/mcp --repo owner/name # GitHub repo, can open PRs
|
|
17
|
+
npx -y @bridge-ds/mcp --root . # local folder (default: current directory)
|
|
18
|
+
npx -y @bridge-ds/mcp --help
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
Connect it to Claude Code:
|
|
23
22
|
|
|
24
23
|
```sh
|
|
25
|
-
claude mcp add bridge -- npx -y @bridge-ds/mcp --
|
|
26
|
-
#
|
|
24
|
+
claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo owner/name
|
|
25
|
+
# so Claude can open pull requests (a fine-grained token limited to that repo):
|
|
27
26
|
claude mcp add bridge -s user -e GITHUB_TOKEN=your-github-token -- npx -y @bridge-ds/mcp --repo owner/name
|
|
27
|
+
# or a local checkout:
|
|
28
|
+
claude mcp add bridge -- npx -y @bridge-ds/mcp --root /abs/path/to/your-ds
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
Or commit a project `.mcp.json
|
|
31
|
-
|
|
31
|
+
Or commit a project `.mcp.json`. Claude Desktop, other clients and the full guide: [docs/AI.md](../../docs/AI.md).
|
|
32
|
+
|
|
33
|
+
Working on the server itself: `npm run mcp:build` in a clone of this repo builds it to
|
|
34
|
+
`packages/mcp/dist/index.js`, which this repo's own `.mcp.json` runs.
|
|
32
35
|
|
|
33
36
|
## Modes
|
|
34
37
|
|
|
@@ -66,7 +69,7 @@ and the full guide: [docs/AI.md](../../docs/AI.md).
|
|
|
66
69
|
| `bridge_get_token_history` | Every change of one token, before and after per mode |
|
|
67
70
|
| `bridge_plan_revert` | Dry run of a revert (or a one-token restore) against the current base, conflicts shown |
|
|
68
71
|
| `bridge_propose_revert` | The revert as a local bridge/* branch or a draft PR labelled revert |
|
|
69
|
-
| `bridge_audit` | Adoption in a product folder or file (local):
|
|
72
|
+
| `bridge_audit` | Adoption in a product folder or file (local): CSS, SCSS, Less, TS/JS, TSX/JSX, Astro, Vue and Svelte. Token and component adoption, hardcoded values with the closest token, foreign and native components with the DS one to use, top files and fixes; `allowlist` and `bridge-audit-ignore-next-line: reason` comments leave known exceptions out, listed under `ignored` |
|
|
70
73
|
| `bridge_list_requests` | Component change requests (issues labeled `component-change`) with status and linked pull requests |
|
|
71
74
|
| `bridge_get_request` | One request with the component's files and a brief to implement it in a pull request that says "Closes #n" |
|
|
72
75
|
| `bridge_create_request` | File a request as an issue labeled `component-change` (token required; `dryRun` previews; `exploration` targets an exploration branch) |
|
|
@@ -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,
|
|
@@ -1039,6 +1041,25 @@ var GitHubClient = class _GitHubClient {
|
|
|
1039
1041
|
throw new GitHubError(422, `GitHub refused to mark #${n} ready for review${detail ? `: ${detail}` : "."} Use "Ready for review" on GitHub instead.`, detail);
|
|
1040
1042
|
}
|
|
1041
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Turns an open pull request back into a draft: the GraphQL mutation convertPullRequestToDraft (REST cannot set
|
|
1046
|
+
* `draft` either), same token. No-op when it is already a draft.
|
|
1047
|
+
*/
|
|
1048
|
+
async convertToDraft(n) {
|
|
1049
|
+
this.assertToken("Turning a pull request back into a draft");
|
|
1050
|
+
const pr = await this.request("GET", `${this.repoPath}/pulls/${n}`);
|
|
1051
|
+
if (pr.draft === true) return;
|
|
1052
|
+
if (pr.state && pr.state !== "open") throw new GitHubError(422, `Pull request #${n} is ${pr.state}: only an open one can go back to draft.`);
|
|
1053
|
+
if (!pr.node_id) throw new GitHubError(422, `Pull request #${n} has no node id: convert it to draft on GitHub.`);
|
|
1054
|
+
const r = await this.request("POST", "/graphql", {
|
|
1055
|
+
query: "mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { number isDraft } } }",
|
|
1056
|
+
variables: { id: pr.node_id }
|
|
1057
|
+
});
|
|
1058
|
+
if (r?.errors?.length) {
|
|
1059
|
+
const detail = r.errors.map((e) => e.message).filter(Boolean).join(" ");
|
|
1060
|
+
throw new GitHubError(422, `GitHub refused to turn #${n} back into a draft${detail ? `: ${detail}` : "."} Use "Convert to draft" on GitHub instead.`, detail);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1042
1063
|
async createPullRequest(input) {
|
|
1043
1064
|
this.assertWritableBranch(input.head, input.base);
|
|
1044
1065
|
const r = await this.request(
|
|
@@ -1834,10 +1855,10 @@ function planRestore(input) {
|
|
|
1834
1855
|
const from = input.label ? ` from ${input.label}` : "";
|
|
1835
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 } }];
|
|
1836
1857
|
const title = `Restore ${path} (${label})${from}`;
|
|
1837
|
-
const
|
|
1858
|
+
const item2 = input.item;
|
|
1838
1859
|
const summary2 = [
|
|
1839
1860
|
`Restores \`${path}\` in ${label} to ${show(input.value)}${from}.`,
|
|
1840
|
-
...
|
|
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}.`] : []
|
|
1841
1862
|
].join("\n");
|
|
1842
1863
|
return finishPlan({ ...input, kind: "restore", title, prTitle: title, summary: summary2, planned, conflicts: [], skipped: [] });
|
|
1843
1864
|
}
|
|
@@ -3000,7 +3021,7 @@ function explorationPrBody(e, ctx) {
|
|
|
3000
3021
|
`- **Promote:** in Bridge Studio (Changes, Explorations, Promote to review) or \`bridge explore promote ${slug2}\`. Bridge marks the pull request ready for review, removes the \`${EXPLORATION_LABEL}\` label and the exploration note, and adds the tier labels. Then it goes through the usual review.`,
|
|
3001
3022
|
"- **Close:** close this pull request. The branch stays for reference; the preview prerelease is removed.",
|
|
3002
3023
|
"",
|
|
3003
|
-
`<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {} }).replace(/-->/g, "--\\u003e")} -->`
|
|
3024
|
+
`<!-- ${MARK} ${JSON.stringify({ name: e.name.trim(), purpose: e.purpose.trim(), slug: slug2, ...e.useCase ? { useCase: e.useCase } : {}, ...experimental.length ? { experimental } : {}, ...e.author ? { author: e.author } : {}, ...e.startedAt ? { startedAt: e.startedAt } : {}, ...e.source ? { source: e.source } : {}, ...e.agent ? { agent: e.agent } : {} }).replace(/-->/g, "--\\u003e")} -->`
|
|
3004
3025
|
);
|
|
3005
3026
|
return lines.join("\n");
|
|
3006
3027
|
}
|
|
@@ -3279,29 +3300,29 @@ function sanitizeDraftText(raw) {
|
|
|
3279
3300
|
const summary2 = typeof raw.summary === "string" ? sentences(cleanDraftText(raw.summary)) : "";
|
|
3280
3301
|
return { title, summary: summary2 };
|
|
3281
3302
|
}
|
|
3282
|
-
function sanitizeDocsResult(
|
|
3283
|
-
const task =
|
|
3303
|
+
function sanitizeDocsResult(item2, r) {
|
|
3304
|
+
const task = item2.docs.task;
|
|
3284
3305
|
if (task.kind === "selection") {
|
|
3285
3306
|
const raw = r.replacement ?? (r.docs && typeof r.docs === "object" ? r.docs[task.key] : void 0);
|
|
3286
3307
|
const replacement = sanitizeDocsReplacement(raw);
|
|
3287
|
-
if (!replacement) throw new Error(`Item "${
|
|
3308
|
+
if (!replacement) throw new Error(`Item "${item2.id}" asks to rewrite a passage of ${task.key}: send {"id": "${item2.id}", "replacement": "..."}.`);
|
|
3288
3309
|
return { replacement };
|
|
3289
3310
|
}
|
|
3290
3311
|
try {
|
|
3291
3312
|
const { sections: sections2, checked } = docsSectionsAnswer(task, r.docs ?? {});
|
|
3292
3313
|
return { docs: sections2, checked };
|
|
3293
3314
|
} catch {
|
|
3294
|
-
throw new Error(`Item "${
|
|
3315
|
+
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.`);
|
|
3295
3316
|
}
|
|
3296
3317
|
}
|
|
3297
|
-
function sanitizeResultLines(
|
|
3318
|
+
function sanitizeResultLines(item2, raw) {
|
|
3298
3319
|
if (raw === void 0 || raw === null) return [];
|
|
3299
|
-
if (!Array.isArray(raw)) throw new Error(`lines for "${
|
|
3300
|
-
const known = new Set((
|
|
3320
|
+
if (!Array.isArray(raw)) throw new Error(`lines for "${item2.id}" must be a list of {"id", "line", "title"}.`);
|
|
3321
|
+
const known = new Set((item2.lines ?? []).map((l) => l.id));
|
|
3301
3322
|
const out = /* @__PURE__ */ new Map();
|
|
3302
3323
|
for (const l of raw) {
|
|
3303
3324
|
const id = typeof l?.id === "string" ? l.id : "";
|
|
3304
|
-
if (!known.has(id)) throw new Error(`Item "${
|
|
3325
|
+
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."}`);
|
|
3305
3326
|
const line = sanitizeDraftLine(l.line);
|
|
3306
3327
|
const title = sanitizeDraftTitle(l.title);
|
|
3307
3328
|
if (line || title) out.set(id, { id, line, ...title ? { title } : {} });
|
|
@@ -3318,13 +3339,13 @@ function mergeDraftResults(p, results, at = /* @__PURE__ */ new Date()) {
|
|
|
3318
3339
|
const next = new Map((p.results ?? []).map((r) => [r.id, r]));
|
|
3319
3340
|
for (const r of results) {
|
|
3320
3341
|
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(", ")}.`);
|
|
3321
|
-
const
|
|
3322
|
-
if (
|
|
3323
|
-
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(
|
|
3342
|
+
const item2 = p.items.find((i) => i.id === r.id);
|
|
3343
|
+
if (item2.scope === "docs") {
|
|
3344
|
+
next.set(r.id, { id: r.id, title: "", summary: "", ...sanitizeDocsResult(item2, r), by: "claude", at: at.toISOString() });
|
|
3324
3345
|
continue;
|
|
3325
3346
|
}
|
|
3326
3347
|
const clean = sanitizeDraftText(r);
|
|
3327
|
-
const lines = sanitizeResultLines(
|
|
3348
|
+
const lines = sanitizeResultLines(item2, r.lines);
|
|
3328
3349
|
if (!clean.title && !clean.summary && !lines.length) throw new Error(`The result for "${r.id}" has no title and no summary, and no line.`);
|
|
3329
3350
|
next.set(r.id, { id: r.id, title: clean.title, summary: clean.summary, ...lines.length ? { lines } : {}, by: "claude", at: at.toISOString() });
|
|
3330
3351
|
}
|
|
@@ -3420,6 +3441,8 @@ function explorationSummary(p, _owner, _repo, s) {
|
|
|
3420
3441
|
status: explorationStatus({ state: p.state, labels: p.labels, headRef: p.headRef, body: p.body }),
|
|
3421
3442
|
draft: p.draft,
|
|
3422
3443
|
author: e?.author ?? p.author,
|
|
3444
|
+
...e?.source ? { source: e.source } : {},
|
|
3445
|
+
...e?.agent ? { agent: e.agent } : {},
|
|
3423
3446
|
updatedAt: p.updatedAt,
|
|
3424
3447
|
headSha: p.headSha,
|
|
3425
3448
|
previews: previewLinks(s.apps, { branch: p.headRef, pr: p.number, sha: p.headSha })
|
|
@@ -3710,6 +3733,10 @@ var EQUIVALENTS = [
|
|
|
3710
3733
|
[/^Badge$/, ["Badge"]],
|
|
3711
3734
|
[/^(Chip|Tag|ChipField)$/, ["Tag", "Badge"]],
|
|
3712
3735
|
[/^(Alert|AlertTitle|Callout)$/, ["Alert"]],
|
|
3736
|
+
[/^(Banner|AnnouncementBar|TopBanner)$/, ["Banner", "Alert"]],
|
|
3737
|
+
[/^(CodeBlock|Code|CodeSnippet|Snippet|Prism|SyntaxHighlighter|CopyBlock)$/, ["CodeBlock"]],
|
|
3738
|
+
[/^(RadioCard|RadioCards|RadioTile|ChoiceCard|ChoiceCardGroup)$/, ["ChoiceCard", "RadioGroup"]],
|
|
3739
|
+
[/^(Checklist|ChecklistItem|StepList)$/, ["Checklist"]],
|
|
3713
3740
|
[/^(Snackbar|Toast|Toaster|Notification)$/, ["Toast"]],
|
|
3714
3741
|
[/^(CircularProgress|Spinner|Loading|LoadingIndicator)$/, ["Spinner", "Progress"]],
|
|
3715
3742
|
[/^(LinearProgress|Progress)$/, ["Progress"]],
|
|
@@ -4256,6 +4283,26 @@ var THEME_FNS = /^(createTheme|extendTheme|createMuiTheme|responsiveFontSizes|ex
|
|
|
4256
4283
|
var CSS_TAGS = /^(styled(\.\w+|\(.*\)(\.attrs\(.*\))?)|css|keyframes|createGlobalStyle|injectGlobal|tw\.\w+)$/s;
|
|
4257
4284
|
var MUI_SPACING_ATTR = /^([mp][trblxy]?|(margin|padding)(Top|Bottom|Left|Right|X|Y)?|gap|rowGap|columnGap|spacing)$/;
|
|
4258
4285
|
var THEME_REF_RE = /\btheme\.(palette|spacing|shape|typography|shadows|zIndex|transitions|vars)\b/g;
|
|
4286
|
+
var DOM_RENDERER_RE = /^([a-z][A-Za-z0-9]*?)(Html|HTML|HtmlAttributes|Attributes|ClassName)$/;
|
|
4287
|
+
function domRendererComponent(fn, components) {
|
|
4288
|
+
const m = DOM_RENDERER_RE.exec(fn);
|
|
4289
|
+
if (!m) return void 0;
|
|
4290
|
+
const pascal2 = m[1][0].toUpperCase() + m[1].slice(1);
|
|
4291
|
+
if (components.has(pascal2)) return pascal2;
|
|
4292
|
+
return [...components].filter((c) => pascal2.startsWith(c)).sort((a, b) => b.length - a.length)[0];
|
|
4293
|
+
}
|
|
4294
|
+
var LOOKALIKE_CLASSES = { pill: "Badge", badge: "Badge", chip: "Tag", tooltip: "Tooltip", tip: "Tooltip", "has-tip": "Tooltip" };
|
|
4295
|
+
var LOOKALIKE_ATTRS = { "data-tip": "Tooltip" };
|
|
4296
|
+
var HTML_CLASS_RE = /\bclass(?:Name)?\s*=\s*["']([^"'$]*)/g;
|
|
4297
|
+
var HTML_ATTR_RE = /\s(data-tip)\s*=/g;
|
|
4298
|
+
function lookalikesInClassList(list, components) {
|
|
4299
|
+
const out = [];
|
|
4300
|
+
for (const token of list.split(/\s+/)) {
|
|
4301
|
+
const component = LOOKALIKE_CLASSES[token];
|
|
4302
|
+
if (component && components.has(component) && !out.some((o) => o.component === component)) out.push({ token, component });
|
|
4303
|
+
}
|
|
4304
|
+
return out;
|
|
4305
|
+
}
|
|
4259
4306
|
function kindFor(path) {
|
|
4260
4307
|
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;
|
|
4261
4308
|
}
|
|
@@ -4275,6 +4322,78 @@ function contextForDeclaration(name, typeText) {
|
|
|
4275
4322
|
if (/^(styles?|sx|classes)$|Styles?$|Sx$/.test(name)) return /sx$/i.test(name) ? "sx" : "styled";
|
|
4276
4323
|
return null;
|
|
4277
4324
|
}
|
|
4325
|
+
function importsOf(path, text) {
|
|
4326
|
+
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false, kindFor(path));
|
|
4327
|
+
const imports = /* @__PURE__ */ new Map();
|
|
4328
|
+
for (const node of sf.statements) {
|
|
4329
|
+
if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier)) continue;
|
|
4330
|
+
const mod = node.moduleSpecifier.text;
|
|
4331
|
+
const c = node.importClause;
|
|
4332
|
+
if (c?.name) imports.set(c.name.text, { module: mod, imported: "default" });
|
|
4333
|
+
if (c?.namedBindings && ts.isNamespaceImport(c.namedBindings)) imports.set(c.namedBindings.name.text, { module: mod, imported: "*" });
|
|
4334
|
+
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 });
|
|
4335
|
+
}
|
|
4336
|
+
return imports;
|
|
4337
|
+
}
|
|
4338
|
+
function classifyTag(t, imports, ds) {
|
|
4339
|
+
const { name } = t;
|
|
4340
|
+
const at = { line: t.line, column: t.column };
|
|
4341
|
+
if (/^[a-z][\w-]*$/.test(name)) {
|
|
4342
|
+
if (!ds.nativeElements.includes(name)) return {};
|
|
4343
|
+
if (name === "input" && t.type === "hidden") return {};
|
|
4344
|
+
if (name === "a" && !t.hasHref) return {};
|
|
4345
|
+
if (t.inRenderProp) return {};
|
|
4346
|
+
const suggestion = nativeEquivalent(name, t.type, ds.components);
|
|
4347
|
+
return { component: { ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} } };
|
|
4348
|
+
}
|
|
4349
|
+
const root = name.split(".")[0];
|
|
4350
|
+
const imp = imports.get(root);
|
|
4351
|
+
const ns = imp?.imported === "*";
|
|
4352
|
+
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
4353
|
+
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
4354
|
+
if (imp) {
|
|
4355
|
+
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
4356
|
+
if (isNonVisual(leaf.split(".").pop())) return {};
|
|
4357
|
+
if (kind === "ds") return { ds: leaf };
|
|
4358
|
+
if (kind === "charts") return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) } };
|
|
4359
|
+
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
4360
|
+
if (t.inRenderProp) return {};
|
|
4361
|
+
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
4362
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
4363
|
+
}
|
|
4364
|
+
if (kind === "foreign" || kind === "shadcn") {
|
|
4365
|
+
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
4366
|
+
return { component: { ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} } };
|
|
4367
|
+
}
|
|
4368
|
+
if (kind === "local" && ds.components.has(leaf.split(".")[0])) return { component: { ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] } };
|
|
4369
|
+
return {};
|
|
4370
|
+
}
|
|
4371
|
+
if (ds.components.has(root)) return { localCandidate: { ...at, name, kind: "recreated", library: "local", suggestion: root } };
|
|
4372
|
+
return {};
|
|
4373
|
+
}
|
|
4374
|
+
function tailwindFindings(hits, offset, pos, ds) {
|
|
4375
|
+
const findings = [];
|
|
4376
|
+
let themeRefs = 0;
|
|
4377
|
+
for (const h of hits) {
|
|
4378
|
+
if (h.kind === "scale") {
|
|
4379
|
+
themeRefs++;
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
if (h.kind === "token") continue;
|
|
4383
|
+
let cat = h.category;
|
|
4384
|
+
const v = h.value;
|
|
4385
|
+
const isColor = Boolean(parseCssColor(v));
|
|
4386
|
+
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
4387
|
+
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
4388
|
+
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
4389
|
+
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
4390
|
+
if (cat === "color" && !isColor) continue;
|
|
4391
|
+
const suggestion = ds.suggest(cat, v);
|
|
4392
|
+
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
4393
|
+
findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
4394
|
+
}
|
|
4395
|
+
return { findings, themeRefs };
|
|
4396
|
+
}
|
|
4278
4397
|
function scanCode(path, text, ds) {
|
|
4279
4398
|
const sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kindFor(path));
|
|
4280
4399
|
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
@@ -4289,24 +4408,9 @@ function scanCode(path, text, ds) {
|
|
|
4289
4408
|
out.findings.push({ ...pos(offset), category: h.category, ...property ? { property } : {}, value: h.text, source, ...suggestion ? { suggestion } : {} });
|
|
4290
4409
|
};
|
|
4291
4410
|
const addTailwind = (hits, offset) => {
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
continue;
|
|
4296
|
-
}
|
|
4297
|
-
if (h.kind === "token") continue;
|
|
4298
|
-
let cat = h.category;
|
|
4299
|
-
const v = h.value;
|
|
4300
|
-
const isColor = Boolean(parseCssColor(v));
|
|
4301
|
-
if (cat === "color-or-size") cat = isColor ? "color" : "font-size";
|
|
4302
|
-
else if (cat === "color-or-width") cat = isColor ? "color" : "border-width";
|
|
4303
|
-
else if (cat === "color-or-shadow") cat = isColor ? "color" : "shadow";
|
|
4304
|
-
else if (cat === "font") cat = /^\d{3}$/.test(v) ? "font-weight" : "font-family";
|
|
4305
|
-
if (cat === "color" && !isColor) continue;
|
|
4306
|
-
const suggestion = ds.suggest(cat, v);
|
|
4307
|
-
const cap = h.kind === "palette" && suggestion && suggestion.confidence > 0.6 ? { ...suggestion, confidence: 0.6, level: "medium" } : suggestion;
|
|
4308
|
-
out.findings.push({ ...pos(offset + h.index), category: cat, value: h.cls, source: "tailwind", ...cap ? { suggestion: cap } : {} });
|
|
4309
|
-
}
|
|
4411
|
+
const r = tailwindFindings(hits, offset, pos, ds);
|
|
4412
|
+
out.themeRefs += r.themeRefs;
|
|
4413
|
+
out.findings.push(...r.findings);
|
|
4310
4414
|
};
|
|
4311
4415
|
const styleValue = (key, node, ctx) => {
|
|
4312
4416
|
const source = ctx === "sx" ? "sx" : ctx === "style" ? "style" : ctx === "theme" ? "theme" : "styled";
|
|
@@ -4367,13 +4471,18 @@ function scanCode(path, text, ds) {
|
|
|
4367
4471
|
}
|
|
4368
4472
|
for (const a of scan.applies) addTailwind(a.hits, start + a.start);
|
|
4369
4473
|
};
|
|
4474
|
+
const lookalike = (list, offset) => {
|
|
4475
|
+
for (const l of lookalikesInClassList(list, ds.components)) out.components.push({ ...pos(offset), name: `.${l.token}`, kind: "recreated", library: "local", suggestion: l.component });
|
|
4476
|
+
};
|
|
4370
4477
|
const classLiteral = (node) => {
|
|
4371
4478
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
4372
4479
|
handled.add(node);
|
|
4480
|
+
lookalike(node.text, node.getStart(sf) + 1);
|
|
4373
4481
|
addTailwind(scanClassString(node.text), node.getStart(sf) + 1);
|
|
4374
4482
|
} else if (ts.isTemplateExpression(node)) {
|
|
4375
4483
|
for (const p of [node.head, ...node.templateSpans.map((s) => s.literal)]) {
|
|
4376
4484
|
handled.add(p);
|
|
4485
|
+
lookalike(p.text, p.getStart(sf) + 1);
|
|
4377
4486
|
addTailwind(scanClassString(p.text), p.getStart(sf) + 1);
|
|
4378
4487
|
}
|
|
4379
4488
|
node.templateSpans.forEach((s) => visit(s.expression, null, true));
|
|
@@ -4382,49 +4491,13 @@ function scanCode(path, text, ds) {
|
|
|
4382
4491
|
const jsxName = (n) => n.getText(sf);
|
|
4383
4492
|
const element = (node) => {
|
|
4384
4493
|
const name = jsxName(node.tagName);
|
|
4385
|
-
const
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
if (isRenderElement(node)) return;
|
|
4393
|
-
const suggestion = nativeEquivalent(name, type, ds.components);
|
|
4394
|
-
out.components.push({ ...at, name, kind: "native", library: "native", ...suggestion ? { suggestion } : {} });
|
|
4395
|
-
return;
|
|
4396
|
-
}
|
|
4397
|
-
const root = name.split(".")[0];
|
|
4398
|
-
const imp = imports.get(root);
|
|
4399
|
-
const ns = imp?.imported === "*";
|
|
4400
|
-
const shown = imp && !ns && imp.imported !== "default" ? [imp.imported, ...name.split(".").slice(1)].join(".") : name;
|
|
4401
|
-
const leaf = ns && name.includes(".") ? name.split(".").slice(1).join(".") : shown;
|
|
4402
|
-
if (imp) {
|
|
4403
|
-
const kind = classifyModule(imp.module, ds.packages, ds.foreignLibraries);
|
|
4404
|
-
if (isNonVisual(leaf.split(".").pop())) return;
|
|
4405
|
-
if (kind === "ds") {
|
|
4406
|
-
out.ds[leaf] = (out.ds[leaf] ?? 0) + 1;
|
|
4407
|
-
return;
|
|
4408
|
-
}
|
|
4409
|
-
if (kind === "charts") {
|
|
4410
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind) });
|
|
4411
|
-
return;
|
|
4412
|
-
}
|
|
4413
|
-
if (kind === "other" && isRouterLink(imp.module, imp.imported === "default" ? "default" : leaf)) {
|
|
4414
|
-
if (isRenderElement(node)) return;
|
|
4415
|
-
const suggestion = ds.components.has("Link") ? "Link" : void 0;
|
|
4416
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
4417
|
-
return;
|
|
4418
|
-
}
|
|
4419
|
-
if (kind === "foreign" || kind === "shadcn") {
|
|
4420
|
-
const suggestion = (ns ? dsEquivalent(root, ds.components) : void 0) ?? dsEquivalent(leaf, ds.components);
|
|
4421
|
-
out.components.push({ ...at, name: shown, kind: "foreign", library: libraryLabel(imp.module, kind), ...suggestion ? { suggestion } : {} });
|
|
4422
|
-
return;
|
|
4423
|
-
}
|
|
4424
|
-
if (kind === "local" && ds.components.has(leaf.split(".")[0])) out.components.push({ ...at, name: leaf, kind: "recreated", library: "local", suggestion: leaf.split(".")[0] });
|
|
4425
|
-
return;
|
|
4426
|
-
}
|
|
4427
|
-
if (ds.components.has(root)) pendingLocal.push({ ...at, name, kind: "recreated", library: "local", suggestion: root });
|
|
4494
|
+
const typeAttr = node.attributes.properties.find((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "type");
|
|
4495
|
+
const type = typeAttr?.initializer && ts.isStringLiteral(typeAttr.initializer) ? typeAttr.initializer.text : void 0;
|
|
4496
|
+
const hasHref = node.attributes.properties.some((p) => ts.isJsxAttribute(p) && p.name.getText(sf) === "href");
|
|
4497
|
+
const r = classifyTag({ name, ...pos(node.getStart(sf)), ...type !== void 0 ? { type } : {}, hasHref, inRenderProp: isRenderElement(node) }, imports, ds);
|
|
4498
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
4499
|
+
if (r.component) out.components.push(r.component);
|
|
4500
|
+
if (r.localCandidate) pendingLocal.push(r.localCandidate);
|
|
4428
4501
|
};
|
|
4429
4502
|
const pendingLocal = [];
|
|
4430
4503
|
function visit(node, ctx, classCtx) {
|
|
@@ -4484,6 +4557,17 @@ function scanCode(path, text, ds) {
|
|
|
4484
4557
|
cssTemplate(node.template);
|
|
4485
4558
|
return;
|
|
4486
4559
|
}
|
|
4560
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
4561
|
+
const imp = imports.get(node.expression.text);
|
|
4562
|
+
if (imp && imp.imported !== "*" && classifyModule(imp.module, ds.packages, ds.foreignLibraries) === "ds") {
|
|
4563
|
+
const comp = domRendererComponent(imp.imported === "default" ? node.expression.text : imp.imported, ds.components);
|
|
4564
|
+
if (comp) out.ds[comp] = (out.ds[comp] ?? 0) + 1;
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isPropertyAccessExpression(node.left) && node.left.name.text === "className") {
|
|
4568
|
+
visit(node.left, ctx, classCtx);
|
|
4569
|
+
return classLiteral(node.right);
|
|
4570
|
+
}
|
|
4487
4571
|
if (ts.isCallExpression(node)) {
|
|
4488
4572
|
const callee = node.expression.getText(sf);
|
|
4489
4573
|
if (CLASS_FNS.test(callee)) {
|
|
@@ -4528,6 +4612,14 @@ function scanCode(path, text, ds) {
|
|
|
4528
4612
|
const literal = (node) => {
|
|
4529
4613
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node)) {
|
|
4530
4614
|
const t = node.text;
|
|
4615
|
+
if (!handled.has(node) && (t.includes("<") || ts.isTemplateMiddle(node) || ts.isTemplateTail(node))) {
|
|
4616
|
+
const base = node.getStart(sf) + 1;
|
|
4617
|
+
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 });
|
|
4618
|
+
for (const m of t.matchAll(HTML_ATTR_RE)) {
|
|
4619
|
+
const component = LOOKALIKE_ATTRS[m[1]];
|
|
4620
|
+
if (component && ds.components.has(component)) out.components.push({ ...pos(base + m.index + 1), name: `[${m[1]}]`, kind: "recreated", library: "local", suggestion: component });
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4531
4623
|
for (const m of t.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
4532
4624
|
if (ds.isDsVar(m[1])) out.tokenUsages++;
|
|
4533
4625
|
else out.themeRefs++;
|
|
@@ -4547,29 +4639,85 @@ function scanCode(path, text, ds) {
|
|
|
4547
4639
|
return out;
|
|
4548
4640
|
}
|
|
4549
4641
|
|
|
4550
|
-
// ../core/src/audit/
|
|
4551
|
-
var
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4642
|
+
// ../core/src/audit/ignore.ts
|
|
4643
|
+
var IGNORE_DIRECTIVE = "bridge-audit-ignore-next-line";
|
|
4644
|
+
function ignoreDirectives(text) {
|
|
4645
|
+
const at = lineCol(text);
|
|
4646
|
+
const out = [];
|
|
4647
|
+
for (const m of text.matchAll(new RegExp(`${IGNORE_DIRECTIVE}\\b`, "g"))) {
|
|
4648
|
+
const start = m.index;
|
|
4649
|
+
const opener = /(<!--|\/\*|\/\/)(\s*)$/.exec(text.slice(Math.max(0, start - 200), start));
|
|
4650
|
+
const open = opener?.[1];
|
|
4651
|
+
if (!open || open === "//" && opener[2].includes("\n")) continue;
|
|
4652
|
+
const from = start + m[0].length;
|
|
4653
|
+
let end;
|
|
4654
|
+
if (open === "//") {
|
|
4655
|
+
end = text.indexOf("\n", from);
|
|
4656
|
+
if (end < 0) end = text.length;
|
|
4657
|
+
} else {
|
|
4658
|
+
end = text.indexOf(open === "<!--" ? "-->" : "*/", from);
|
|
4659
|
+
if (end < 0) continue;
|
|
4660
|
+
}
|
|
4661
|
+
const reason = text.slice(from, end).replace(/^\s*:?/, "").replace(/\s+/g, " ").trim();
|
|
4662
|
+
out.push({ line: at(start).line, target: at(end).line + 1, reason });
|
|
4663
|
+
}
|
|
4664
|
+
return out;
|
|
4562
4665
|
}
|
|
4563
|
-
var
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4666
|
+
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 };
|
|
4667
|
+
function applyIgnoreComments(text, findings, components) {
|
|
4668
|
+
const errors = [];
|
|
4669
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
4670
|
+
for (const d of ignoreDirectives(text)) {
|
|
4671
|
+
if (!d.reason) {
|
|
4672
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} needs a reason after a colon (${IGNORE_DIRECTIVE}: why this one stays); nothing was ignored` });
|
|
4673
|
+
continue;
|
|
4674
|
+
}
|
|
4675
|
+
if (!findings.some((f) => f.line === d.target) && !components.some((c) => c.line === d.target)) {
|
|
4676
|
+
errors.push({ line: d.line, message: `${IGNORE_DIRECTIVE} ignores nothing: line ${d.target} has no finding; remove the comment` });
|
|
4677
|
+
continue;
|
|
4678
|
+
}
|
|
4679
|
+
reasons.set(d.target, d.reason);
|
|
4680
|
+
}
|
|
4681
|
+
const ignored = [];
|
|
4682
|
+
const keep = (xs) => xs.filter((x) => {
|
|
4683
|
+
const r = reasons.get(x.line);
|
|
4684
|
+
if (r === void 0) return true;
|
|
4685
|
+
ignored.push(item(x, r, "comment"));
|
|
4686
|
+
return false;
|
|
4687
|
+
});
|
|
4688
|
+
return { findings: keep(findings), components: keep(components), ignored, errors };
|
|
4689
|
+
}
|
|
4690
|
+
function applyAllowlist(entries, findings, components) {
|
|
4691
|
+
const used = /* @__PURE__ */ new Set();
|
|
4692
|
+
const ignored = [];
|
|
4693
|
+
const keep = (xs) => xs.filter((x) => {
|
|
4694
|
+
const what = "category" in x ? x.value : x.name;
|
|
4695
|
+
const fits = (e) => e.line === x.line && (e.what === void 0 || e.what === what);
|
|
4696
|
+
let i = entries.findIndex((e, k) => fits(e) && !used.has(k));
|
|
4697
|
+
if (i < 0) i = entries.findIndex(fits);
|
|
4698
|
+
if (i < 0) return true;
|
|
4699
|
+
used.add(i);
|
|
4700
|
+
ignored.push(item(x, entries[i].reason, "allowlist"));
|
|
4701
|
+
return false;
|
|
4702
|
+
});
|
|
4703
|
+
return { findings: keep(findings), components: keep(components), ignored, used };
|
|
4704
|
+
}
|
|
4705
|
+
function parseAllowlist(json) {
|
|
4706
|
+
const fail2 = (msg) => {
|
|
4707
|
+
throw new Error(`allowlist: ${msg}`);
|
|
4708
|
+
};
|
|
4709
|
+
if (!json || typeof json !== "object" || !Array.isArray(json.entries)) fail2('expected { "entries": [...] }');
|
|
4710
|
+
return json.entries.map((e, i) => {
|
|
4711
|
+
const x = e;
|
|
4712
|
+
if (typeof x.file !== "string" || !x.file) fail2(`entry ${i} needs "file"`);
|
|
4713
|
+
if (typeof x.line !== "number" || !Number.isInteger(x.line) || x.line < 1) fail2(`entry ${i} (${x.file}) needs a "line" number`);
|
|
4714
|
+
if (typeof x.reason !== "string" || !x.reason.trim()) fail2(`entry ${i} (${x.file}:${x.line}) needs a "reason"`);
|
|
4715
|
+
if (x.what !== void 0 && typeof x.what !== "string") fail2(`entry ${i} (${x.file}:${x.line}): "what" must be a string`);
|
|
4716
|
+
return { file: x.file, line: x.line, ...x.what !== void 0 ? { what: x.what } : {}, reason: x.reason.trim() };
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
// ../core/src/audit/style.ts
|
|
4573
4721
|
function isSafeFix(h, s) {
|
|
4574
4722
|
if (!s || s.level !== "exact") return false;
|
|
4575
4723
|
switch (h.category) {
|
|
@@ -4594,49 +4742,346 @@ function isSafeFix(h, s) {
|
|
|
4594
4742
|
return false;
|
|
4595
4743
|
}
|
|
4596
4744
|
}
|
|
4597
|
-
function
|
|
4598
|
-
const
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4745
|
+
function auditCssText(text, lang, ds, opts) {
|
|
4746
|
+
const base = opts.offset ?? 0;
|
|
4747
|
+
const source = opts.source ?? "css";
|
|
4748
|
+
const at = (o) => opts.pos(base + o);
|
|
4749
|
+
const scan = scanCssText(text, lang);
|
|
4750
|
+
const findings = [];
|
|
4751
|
+
let tokenUsages = 0;
|
|
4752
|
+
let themeRefs = 0;
|
|
4753
|
+
for (const d of scan.decls) {
|
|
4754
|
+
for (const v of d.scan.vars) ds.isDsVar(v) ? tokenUsages++ : themeRefs++;
|
|
4755
|
+
if (d.scan.themeRef) themeRefs++;
|
|
4756
|
+
else if (lang !== "css" && !d.property.startsWith("$")) themeRefs += (d.value.match(/\$[\w-]+/g) ?? []).length;
|
|
4757
|
+
const property = d.property.toLowerCase();
|
|
4758
|
+
for (const h of d.scan.hits) {
|
|
4759
|
+
const suggestion = ds.suggest(h.category, h.text, property);
|
|
4760
|
+
const autofix = source === "css" && isSafeFix(h, suggestion);
|
|
4761
|
+
findings.push({ ...at(d.valueStart + h.index), category: h.category, property, value: h.text, source, ...suggestion ? { suggestion } : {}, ...autofix ? { autofix: true } : {} });
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
for (const a of scan.applies) {
|
|
4765
|
+
for (const h of a.hits) {
|
|
4766
|
+
if (h.kind === "scale") themeRefs++;
|
|
4767
|
+
else if (h.kind !== "token" && h.value) {
|
|
4768
|
+
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;
|
|
4769
|
+
const suggestion = ds.suggest(cat, h.value);
|
|
4770
|
+
findings.push({ ...at(a.start + h.index), category: cat, value: h.cls, source: "tailwind", ...suggestion ? { suggestion } : {} });
|
|
4771
|
+
} else if (h.kind === "token" && h.value) {
|
|
4772
|
+
const v = /var\(\s*(--[\w-]+)/.exec(h.value)?.[1];
|
|
4773
|
+
if (v && ds.isDsVar(v)) tokenUsages++;
|
|
4774
|
+
else themeRefs++;
|
|
4614
4775
|
}
|
|
4615
4776
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4777
|
+
}
|
|
4778
|
+
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
4779
|
+
return { findings, tokenUsages, themeRefs };
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
// ../core/src/audit/markup.ts
|
|
4783
|
+
var MARKUP_EXTENSIONS = [".astro", ".vue", ".svelte"];
|
|
4784
|
+
function mask2(text, ranges, keepInside) {
|
|
4785
|
+
const inside = new Uint8Array(text.length);
|
|
4786
|
+
for (const r of ranges) inside.fill(1, r.start, r.end);
|
|
4787
|
+
let out = "";
|
|
4788
|
+
for (let i = 0; i < text.length; i++) {
|
|
4789
|
+
const c = text[i];
|
|
4790
|
+
out += c === "\n" || Boolean(inside[i]) === keepInside ? c : " ";
|
|
4791
|
+
}
|
|
4792
|
+
return out;
|
|
4793
|
+
}
|
|
4794
|
+
var attrOf = (attrs, name) => new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i").exec(attrs)?.slice(1).find((v) => v !== void 0);
|
|
4795
|
+
var isCodeScript = (attrs) => {
|
|
4796
|
+
const type = attrOf(attrs, "type")?.toLowerCase();
|
|
4797
|
+
return !type || ["module", "text/javascript", "application/javascript", "text/typescript", "ts", "text/babel"].includes(type);
|
|
4798
|
+
};
|
|
4799
|
+
function markupBlocks(text, lang) {
|
|
4800
|
+
const nonTemplate = [];
|
|
4801
|
+
let frontmatter;
|
|
4802
|
+
let from = 0;
|
|
4803
|
+
if (lang === "astro") {
|
|
4804
|
+
const open = /^\s*---[^\S\n]*\n/.exec(text);
|
|
4805
|
+
if (open) {
|
|
4806
|
+
const close = /\n---[^\S\n]*(\n|$)/g;
|
|
4807
|
+
close.lastIndex = open[0].length - 1;
|
|
4808
|
+
const m = close.exec(text);
|
|
4809
|
+
if (m) {
|
|
4810
|
+
frontmatter = { start: open[0].length, end: m.index + 1 };
|
|
4811
|
+
from = m.index + m[0].length;
|
|
4812
|
+
nonTemplate.push({ start: 0, end: from });
|
|
4628
4813
|
}
|
|
4629
4814
|
}
|
|
4630
|
-
findings.sort((x, y) => x.line - y.line || x.column - y.column);
|
|
4631
|
-
return { path, lang, tokenUsages, themeRefs, hardcoded: findings.length, dsComponents: 0, otherComponents: 0, findings, components: [], definitions: [], dsByName: {} };
|
|
4632
4815
|
}
|
|
4633
|
-
const
|
|
4816
|
+
const scripts = [];
|
|
4817
|
+
const styles = [];
|
|
4818
|
+
const re = /<(script|style)\b((?:[^>"']|"[^"]*"|'[^']*')*)>([\s\S]*?)<\/\1\s*>/gi;
|
|
4819
|
+
re.lastIndex = from;
|
|
4820
|
+
for (let m; m = re.exec(text); ) {
|
|
4821
|
+
const contentStart = m.index + m[0].indexOf(">", m[1].length + 1 + m[2].length) + 1;
|
|
4822
|
+
const block = { start: contentStart, end: contentStart + m[3].length, attrs: m[2] };
|
|
4823
|
+
(m[1].toLowerCase() === "script" ? scripts : styles).push(block);
|
|
4824
|
+
nonTemplate.push({ start: m.index, end: m.index + m[0].length });
|
|
4825
|
+
}
|
|
4826
|
+
for (const m of text.slice(from).matchAll(/<!--[\s\S]*?-->/g)) nonTemplate.push({ start: from + m.index, end: from + m.index + m[0].length });
|
|
4827
|
+
return { ...frontmatter ? { frontmatter } : {}, scripts, styles, nonTemplate };
|
|
4828
|
+
}
|
|
4829
|
+
function closeBrace(s, i) {
|
|
4830
|
+
let depth = 0;
|
|
4831
|
+
for (let k = i; k < s.length; k++) {
|
|
4832
|
+
const c = s[k];
|
|
4833
|
+
if (c === "{") depth++;
|
|
4834
|
+
else if (c === "}") {
|
|
4835
|
+
if (--depth === 0) return k + 1;
|
|
4836
|
+
} else if (c === '"' || c === "'") {
|
|
4837
|
+
const e = s.indexOf(c, k + 1);
|
|
4838
|
+
if (e < 0) return s.length;
|
|
4839
|
+
k = e;
|
|
4840
|
+
} else if (c === "`") {
|
|
4841
|
+
for (k++; k < s.length && s[k] !== "`"; k++) {
|
|
4842
|
+
if (s[k] === "\\") k++;
|
|
4843
|
+
else if (s[k] === "$" && s[k + 1] === "{") k = closeBrace(s, k + 1) - 1;
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
return s.length;
|
|
4848
|
+
}
|
|
4849
|
+
function readTag(s, i, lang) {
|
|
4850
|
+
const attrs = [];
|
|
4851
|
+
let k = i;
|
|
4852
|
+
while (k < s.length) {
|
|
4853
|
+
while (k < s.length && /\s/.test(s[k])) k++;
|
|
4854
|
+
const c = s[k];
|
|
4855
|
+
if (c === void 0) break;
|
|
4856
|
+
if (c === ">") return { attrs, end: k + 1, selfClosing: false };
|
|
4857
|
+
if (c === "/" && s[k + 1] === ">") return { attrs, end: k + 2, selfClosing: true };
|
|
4858
|
+
if (c === "{") {
|
|
4859
|
+
const e = closeBrace(s, k);
|
|
4860
|
+
const inner = s.slice(k + 1, e - 1).trim();
|
|
4861
|
+
attrs.push(inner.startsWith("...") ? { name: "", kind: "spread", value: inner, valueStart: k + 1 } : { name: inner, kind: "expr", value: inner, valueStart: k + 1 });
|
|
4862
|
+
k = e;
|
|
4863
|
+
continue;
|
|
4864
|
+
}
|
|
4865
|
+
const nm = /^[^\s=>/"'{]+/.exec(s.slice(k, k + 200));
|
|
4866
|
+
if (!nm) {
|
|
4867
|
+
k++;
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4870
|
+
const name = nm[0];
|
|
4871
|
+
k += name.length;
|
|
4872
|
+
let j = k;
|
|
4873
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
4874
|
+
if (s[j] !== "=") {
|
|
4875
|
+
attrs.push({ name, kind: "bare", value: "", valueStart: k });
|
|
4876
|
+
continue;
|
|
4877
|
+
}
|
|
4878
|
+
j++;
|
|
4879
|
+
while (j < s.length && /\s/.test(s[j])) j++;
|
|
4880
|
+
const q = s[j];
|
|
4881
|
+
const bound = lang === "vue" && /^(:|v-bind:|v-on:|@|v-)/.test(name);
|
|
4882
|
+
if (q === '"' || q === "'") {
|
|
4883
|
+
const e = s.indexOf(q, j + 1);
|
|
4884
|
+
const end = e < 0 ? s.length : e;
|
|
4885
|
+
attrs.push({ name, kind: bound ? "expr" : "static", value: s.slice(j + 1, end), valueStart: j + 1 });
|
|
4886
|
+
k = end + 1;
|
|
4887
|
+
} else if (q === "{") {
|
|
4888
|
+
const e = closeBrace(s, j);
|
|
4889
|
+
attrs.push({ name, kind: "expr", value: s.slice(j + 1, e - 1), valueStart: j + 1 });
|
|
4890
|
+
k = e;
|
|
4891
|
+
} else {
|
|
4892
|
+
const m = /^[^\s>]+/.exec(s.slice(j));
|
|
4893
|
+
const v = m?.[0].replace(/\/$/, "") ?? "";
|
|
4894
|
+
attrs.push({ name, kind: "static", value: v, valueStart: j });
|
|
4895
|
+
k = j + v.length;
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
return { attrs, end: s.length, selfClosing: false };
|
|
4899
|
+
}
|
|
4900
|
+
var attrName = (n) => n.replace(/^(v-bind:|:|bind:)/, "");
|
|
4901
|
+
var pascal = (kebab) => kebab.replace(/(^|-)([a-z0-9])/g, (_m, _d, c) => c.toUpperCase());
|
|
4902
|
+
var NO_COLOR_ATTR_TAGS = /* @__PURE__ */ new Set(["meta", "link"]);
|
|
4903
|
+
function scanMarkup(path, text, lang, ds) {
|
|
4904
|
+
const out = { findings: [], tokenUsages: 0, themeRefs: 0, ds: {}, components: [], definitions: [] };
|
|
4905
|
+
const pos = lineCol(text);
|
|
4906
|
+
const blocks = markupBlocks(text, lang);
|
|
4907
|
+
const imports = /* @__PURE__ */ new Map();
|
|
4908
|
+
const merge = (c) => {
|
|
4909
|
+
out.findings.push(...c.findings);
|
|
4910
|
+
out.tokenUsages += c.tokenUsages;
|
|
4911
|
+
out.themeRefs += c.themeRefs;
|
|
4912
|
+
for (const [n, k] of Object.entries(c.ds)) out.ds[n] = (out.ds[n] ?? 0) + k;
|
|
4913
|
+
out.components.push(...c.components);
|
|
4914
|
+
out.definitions.push(...c.definitions);
|
|
4915
|
+
};
|
|
4916
|
+
const codeRanges = [...blocks.frontmatter ? [{ ...blocks.frontmatter, attrs: "" }] : [], ...blocks.scripts.filter((b) => isCodeScript(b.attrs))];
|
|
4917
|
+
for (const r of codeRanges) {
|
|
4918
|
+
const jsx = /^(tsx|jsx)$/i.test(attrOf(r.attrs, "lang") ?? "");
|
|
4919
|
+
const code2 = mask2(text, [r], true);
|
|
4920
|
+
const file = `${path}.${jsx ? "tsx" : "ts"}`;
|
|
4921
|
+
merge(scanCode(file, code2, ds));
|
|
4922
|
+
for (const [k, v] of importsOf(file, code2)) imports.set(k, v);
|
|
4923
|
+
}
|
|
4924
|
+
for (const b of blocks.styles) {
|
|
4925
|
+
const l = (attrOf(b.attrs, "lang") ?? "css").toLowerCase();
|
|
4926
|
+
const r = auditCssText(text.slice(b.start, b.end), l === "scss" || l === "sass" ? "scss" : l === "less" ? "less" : "css", ds, { pos, offset: b.start });
|
|
4927
|
+
out.findings.push(...r.findings);
|
|
4928
|
+
out.tokenUsages += r.tokenUsages;
|
|
4929
|
+
out.themeRefs += r.themeRefs;
|
|
4930
|
+
}
|
|
4931
|
+
const scanExpr = (prefix, expr, offset, suffix, keepLookalikes = false) => {
|
|
4932
|
+
const { line, column } = pos(offset);
|
|
4933
|
+
const src = line >= 2 ? `${"\n".repeat(line - 2)}${prefix}
|
|
4934
|
+
${" ".repeat(column - 1)}${expr}${suffix}` : `${prefix}${expr}${suffix}`;
|
|
4935
|
+
const c = scanCode(`${path}.tsx`, src, ds);
|
|
4936
|
+
merge({ ...c, components: keepLookalikes ? c.components.filter((x) => x.kind === "recreated" && x.name.startsWith(".")) : [], definitions: [], ds: {} });
|
|
4937
|
+
};
|
|
4938
|
+
const styleAttr = (a) => {
|
|
4939
|
+
if (a.kind === "static") {
|
|
4940
|
+
const css = lang === "svelte" ? a.value.replace(/\{[^}]*\}/g, (m) => " ".repeat(m.length)) : a.value;
|
|
4941
|
+
const r = auditCssText(css, "css", ds, { pos, offset: a.valueStart, source: "style" });
|
|
4942
|
+
out.findings.push(...r.findings);
|
|
4943
|
+
out.tokenUsages += r.tokenUsages;
|
|
4944
|
+
out.themeRefs += r.themeRefs;
|
|
4945
|
+
return;
|
|
4946
|
+
}
|
|
4947
|
+
const str2 = /^(\s*)([`'"])([\s\S]*)\2\s*$/.exec(a.value);
|
|
4948
|
+
if (str2) {
|
|
4949
|
+
const body = str2[3].replace(/\$\{[^}]*\}/g, (m) => " ".repeat(m.length));
|
|
4950
|
+
const r = auditCssText(body, "css", ds, { pos, offset: a.valueStart + str2[1].length + 1, source: "style" });
|
|
4951
|
+
out.findings.push(...r.findings);
|
|
4952
|
+
out.tokenUsages += r.tokenUsages;
|
|
4953
|
+
out.themeRefs += r.themeRefs;
|
|
4954
|
+
return;
|
|
4955
|
+
}
|
|
4956
|
+
scanExpr("const style: CSSProperties =", a.value, a.valueStart, ";");
|
|
4957
|
+
};
|
|
4958
|
+
const tpl = mask2(text, blocks.nonTemplate, false);
|
|
4959
|
+
const pending = [];
|
|
4960
|
+
let noscript = 0;
|
|
4961
|
+
const TAG = /<(\/?)([A-Za-z][\w.:-]*)/g;
|
|
4962
|
+
for (let m; m = TAG.exec(tpl); ) {
|
|
4963
|
+
const raw = m[2];
|
|
4964
|
+
if (m[1]) {
|
|
4965
|
+
if (raw.toLowerCase() === "noscript") noscript = Math.max(0, noscript - 1);
|
|
4966
|
+
continue;
|
|
4967
|
+
}
|
|
4968
|
+
const tag = readTag(tpl, m.index + m[0].length, lang);
|
|
4969
|
+
TAG.lastIndex = tag.end;
|
|
4970
|
+
const lower = raw.toLowerCase();
|
|
4971
|
+
if (lower === "noscript" && !tag.selfClosing) {
|
|
4972
|
+
noscript++;
|
|
4973
|
+
continue;
|
|
4974
|
+
}
|
|
4975
|
+
const attrs = tag.attrs;
|
|
4976
|
+
const get = (n) => attrs.find((a) => attrName(a.name) === n);
|
|
4977
|
+
for (const a of attrs) {
|
|
4978
|
+
const n = attrName(a.name);
|
|
4979
|
+
if (n === "style") styleAttr(a);
|
|
4980
|
+
else if (lang === "svelte" && a.name.startsWith("style:") && a.kind === "static") {
|
|
4981
|
+
const prop = a.name.slice(6).split("|")[0];
|
|
4982
|
+
const r2 = auditCssText(`${prop}:${a.value}`, "css", ds, { pos, offset: a.valueStart - prop.length - 1, source: "style" });
|
|
4983
|
+
out.findings.push(...r2.findings);
|
|
4984
|
+
out.tokenUsages += r2.tokenUsages;
|
|
4985
|
+
out.themeRefs += r2.themeRefs;
|
|
4986
|
+
} else if (/^(class|className|class:list)$/.test(n)) {
|
|
4987
|
+
if (a.kind === "static") {
|
|
4988
|
+
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 });
|
|
4989
|
+
const r2 = tailwindFindings(scanClassString(a.value), a.valueStart, pos, ds);
|
|
4990
|
+
out.findings.push(...r2.findings);
|
|
4991
|
+
out.themeRefs += r2.themeRefs;
|
|
4992
|
+
} else if (a.kind === "expr") scanExpr("cn(", a.value, a.valueStart, ")", true);
|
|
4993
|
+
} else if (LOOKALIKE_ATTRS[n] && ds.components.has(LOOKALIKE_ATTRS[n])) {
|
|
4994
|
+
out.components.push({ ...pos(a.valueStart - a.name.length - 1), name: `[${n}]`, kind: "recreated", library: "local", suggestion: LOOKALIKE_ATTRS[n] });
|
|
4995
|
+
} else if (a.kind === "static" && !NO_COLOR_ATTR_TAGS.has(lower) && isColorLiteral(a.value)) {
|
|
4996
|
+
const suggestion = ds.suggest("color", a.value.trim(), n);
|
|
4997
|
+
out.findings.push({ ...pos(a.valueStart), category: "color", property: n, value: a.value.trim(), source: "jsx-attr", ...suggestion ? { suggestion } : {} });
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
let name = raw;
|
|
5001
|
+
if (raw.includes(":")) continue;
|
|
5002
|
+
if (raw.includes("-")) {
|
|
5003
|
+
if (!imports.has(pascal(raw))) continue;
|
|
5004
|
+
name = pascal(raw);
|
|
5005
|
+
}
|
|
5006
|
+
const isNative = /^[a-z]/.test(name) && !name.includes(".");
|
|
5007
|
+
if (isNative && noscript > 0) continue;
|
|
5008
|
+
const type = get("type");
|
|
5009
|
+
const r = classifyTag({ name, ...pos(m.index), ...type?.kind === "static" ? { type: type.value } : {}, hasHref: Boolean(get("href")) }, imports, ds);
|
|
5010
|
+
if (r.ds) out.ds[r.ds] = (out.ds[r.ds] ?? 0) + 1;
|
|
5011
|
+
if (r.component) out.components.push(r.component);
|
|
5012
|
+
if (r.localCandidate) pending.push(r.localCandidate);
|
|
5013
|
+
}
|
|
5014
|
+
const base = /([^/]+)\.(astro|vue|svelte)$/.exec(path)?.[1];
|
|
5015
|
+
if (base && /^[A-Z][A-Za-z0-9]*$/.test(base)) out.definitions.push({ name: base, line: 1 });
|
|
5016
|
+
const defined = new Set(out.definitions.map((d) => d.name));
|
|
5017
|
+
out.components.push(...pending.filter((f) => defined.has(f.suggestion)));
|
|
5018
|
+
out.findings.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
5019
|
+
out.components.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
5020
|
+
return out;
|
|
5021
|
+
}
|
|
5022
|
+
|
|
5023
|
+
// ../core/src/audit/types.ts
|
|
5024
|
+
var AUDIT_SCHEMA_VERSION = 1;
|
|
5025
|
+
var FINDING_CATEGORIES = ["color", "spacing", "size", "radius", "border-width", "shadow", "duration", "easing", "z-index", "font-family", "font-size", "font-weight", "line-height"];
|
|
5026
|
+
|
|
5027
|
+
// ../core/src/audit/report.ts
|
|
5028
|
+
var AUDIT_EXTENSIONS = [".css", ".scss", ".sass", ".less", ".tsx", ".jsx", ".ts", ".js", ".mjs", ".cjs", ".mts", ".cts", ...MARKUP_EXTENSIONS];
|
|
5029
|
+
var AUDIT_IGNORE_DIRS = ["node_modules", "dist", "build", "out", "coverage", ".git", ".next", ".nuxt", ".astro", ".turbo", ".cache", ".vercel", "storybook-static", "vendor", "public"];
|
|
5030
|
+
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$/;
|
|
5031
|
+
function isAuditable(path) {
|
|
5032
|
+
if (!AUDIT_EXTENSIONS.some((e) => path.endsWith(e))) return false;
|
|
5033
|
+
if (IGNORE_FILE.test(path)) return false;
|
|
5034
|
+
return !path.split("/").some((seg) => AUDIT_IGNORE_DIRS.includes(seg));
|
|
5035
|
+
}
|
|
5036
|
+
var langOf = (path) => {
|
|
5037
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
5038
|
+
if (ext === "css") return "css";
|
|
5039
|
+
if (ext === "scss" || ext === "sass") return "scss";
|
|
5040
|
+
if (ext === "less") return "less";
|
|
5041
|
+
if (ext === "tsx") return "tsx";
|
|
5042
|
+
if (ext === "jsx") return "jsx";
|
|
5043
|
+
if (/^[cm]?ts$/.test(ext)) return "ts";
|
|
5044
|
+
if (ext === "astro" || ext === "vue" || ext === "svelte") return ext;
|
|
5045
|
+
return "js";
|
|
5046
|
+
};
|
|
5047
|
+
function auditFile(path, text, ds) {
|
|
5048
|
+
const lang = langOf(path);
|
|
5049
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
5050
|
+
const r = auditCssText(text, lang, ds, { pos: lineCol(text) });
|
|
5051
|
+
return withIgnores(text, { path, lang, tokenUsages: r.tokenUsages, themeRefs: r.themeRefs, hardcoded: 0, dsComponents: 0, otherComponents: 0, findings: r.findings, components: [], definitions: [], dsByName: {} });
|
|
5052
|
+
}
|
|
5053
|
+
const c = lang === "astro" || lang === "vue" || lang === "svelte" ? scanMarkup(path, text, lang, ds) : scanCode(path, text, ds);
|
|
4634
5054
|
const dsCount = Object.values(c.ds).reduce((a, b) => a + b, 0);
|
|
4635
|
-
return { path, lang, tokenUsages: c.tokenUsages, themeRefs: c.themeRefs, hardcoded:
|
|
5055
|
+
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 });
|
|
5056
|
+
}
|
|
5057
|
+
function withIgnores(text, f) {
|
|
5058
|
+
const r = applyIgnoreComments(text, f.findings, f.components);
|
|
5059
|
+
return {
|
|
5060
|
+
...f,
|
|
5061
|
+
findings: r.findings,
|
|
5062
|
+
components: r.components,
|
|
5063
|
+
hardcoded: r.findings.length,
|
|
5064
|
+
otherComponents: r.components.length,
|
|
5065
|
+
...r.ignored.length ? { ignored: r.ignored } : {},
|
|
5066
|
+
...r.errors.length ? { ignoreErrors: r.errors } : {}
|
|
5067
|
+
};
|
|
4636
5068
|
}
|
|
4637
5069
|
var pct = (num2, den) => den > 0 ? Math.round(num2 / den * 1e3) / 10 : null;
|
|
4638
5070
|
function buildAuditReport(input, ds) {
|
|
4639
|
-
const
|
|
5071
|
+
const allow = input.allowlist ?? [];
|
|
5072
|
+
const usedAllow = /* @__PURE__ */ new Set();
|
|
5073
|
+
const reports = input.files.map((f) => {
|
|
5074
|
+
const r = auditFile(f.path, f.text, ds);
|
|
5075
|
+
const idx = allow.flatMap((e, i) => e.file === f.path ? [i] : []);
|
|
5076
|
+
if (!idx.length) return r;
|
|
5077
|
+
const a = applyAllowlist(idx.map((i) => allow[i]), r.findings, r.components);
|
|
5078
|
+
a.used.forEach((k) => usedAllow.add(idx[k]));
|
|
5079
|
+
if (!a.ignored.length) return r;
|
|
5080
|
+
const ignored = [...r.ignored ?? [], ...a.ignored].sort((x, y) => x.line - y.line || x.column - y.column);
|
|
5081
|
+
return { ...r, findings: a.findings, components: a.components, hardcoded: a.findings.length, otherComponents: a.components.length, ignored };
|
|
5082
|
+
});
|
|
5083
|
+
const ignoredItems = reports.flatMap((r) => (r.ignored ?? []).map((x) => ({ file: r.path, ...x })));
|
|
5084
|
+
const staleAllowlist = allow.filter((_e, i) => !usedAllow.has(i));
|
|
4640
5085
|
const files = reports.map(({ definitions: _d, dsByName: _n, ...r }) => r);
|
|
4641
5086
|
const byCategory = {};
|
|
4642
5087
|
const fixes = /* @__PURE__ */ new Map();
|
|
@@ -4696,6 +5141,15 @@ function buildAuditReport(input, ds) {
|
|
|
4696
5141
|
topFiles,
|
|
4697
5142
|
topFixes,
|
|
4698
5143
|
files: files.sort((a, b) => a.path.localeCompare(b.path)),
|
|
5144
|
+
...ignoredItems.length || staleAllowlist.length ? {
|
|
5145
|
+
ignored: {
|
|
5146
|
+
total: ignoredItems.length,
|
|
5147
|
+
comment: ignoredItems.filter((x) => x.via === "comment").length,
|
|
5148
|
+
allowlist: ignoredItems.filter((x) => x.via === "allowlist").length,
|
|
5149
|
+
items: ignoredItems,
|
|
5150
|
+
staleAllowlist
|
|
5151
|
+
}
|
|
5152
|
+
} : {},
|
|
4699
5153
|
history: []
|
|
4700
5154
|
};
|
|
4701
5155
|
}
|
|
@@ -5009,6 +5463,9 @@ function collectAuditFiles(target, exclude = []) {
|
|
|
5009
5463
|
}
|
|
5010
5464
|
return { root: abs, files, skipped };
|
|
5011
5465
|
}
|
|
5466
|
+
function loadAllowlist(path) {
|
|
5467
|
+
return parseAllowlist(JSON.parse(readFileSync(path, "utf8")));
|
|
5468
|
+
}
|
|
5012
5469
|
|
|
5013
5470
|
// src/config.ts
|
|
5014
5471
|
var SCHEMA_URL = "https://raw.githubusercontent.com/peterbono/ai-design-system-demo/main/packages/core/bridge.config.schema.json";
|
|
@@ -5266,7 +5723,8 @@ function client(repo, o, rules) {
|
|
|
5266
5723
|
...o.apiUrl ? { apiUrl: o.apiUrl } : {},
|
|
5267
5724
|
...o.rawUrl ? { rawUrl: o.rawUrl } : {},
|
|
5268
5725
|
protectedBranches: rules.protectedBranches,
|
|
5269
|
-
branchPrefix: rules.branchPrefix
|
|
5726
|
+
branchPrefix: rules.branchPrefix,
|
|
5727
|
+
tokenHint: "set GITHUB_TOKEN (or BRIDGE_GITHUB_TOKEN) in the environment of the Bridge MCP server, read access to this repository is enough"
|
|
5270
5728
|
});
|
|
5271
5729
|
}
|
|
5272
5730
|
var BridgeContext = class _BridgeContext {
|
|
@@ -5856,6 +6314,8 @@ async function startExploration(ctx, a) {
|
|
|
5856
6314
|
...a.useCase ? { useCase: a.useCase } : {},
|
|
5857
6315
|
...a.experimental?.length ? { experimental: a.experimental } : {},
|
|
5858
6316
|
...a.author ? { author: a.author } : {},
|
|
6317
|
+
...a.source ? { source: a.source } : {},
|
|
6318
|
+
...a.agent ? { agent: a.agent } : {},
|
|
5859
6319
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5860
6320
|
};
|
|
5861
6321
|
const gh = await ctx.github().catch(() => null);
|
|
@@ -6633,7 +7093,8 @@ async function auditOp(ctx, a) {
|
|
|
6633
7093
|
if (!existsSync2(target)) throw new Error(`No such file or folder: ${target}`);
|
|
6634
7094
|
const ds = await designSystemIndex(ctx);
|
|
6635
7095
|
const { files, skipped } = collectAuditFiles(target, config.audit?.exclude ?? []);
|
|
6636
|
-
|
|
7096
|
+
const allowlist = a.allowlist ? loadAllowlist(isAbsolute(a.allowlist) ? a.allowlist : resolve3(ctx.root, a.allowlist)) : void 0;
|
|
7097
|
+
let report = buildAuditReport({ files, skipped, target: a.path, ...a.label ? { label: a.label } : {}, ...allowlist ? { allowlist } : {} }, ds);
|
|
6637
7098
|
if (a.baseline) report = compareWithBaseline(report, JSON.parse(readFileSync2(isAbsolute(a.baseline) ? a.baseline : resolve3(ctx.root, a.baseline), "utf8")));
|
|
6638
7099
|
const limit = a.limit ?? 10;
|
|
6639
7100
|
const single = files.length === 1;
|
|
@@ -6650,6 +7111,8 @@ async function auditOp(ctx, a) {
|
|
|
6650
7111
|
native: report.components.native,
|
|
6651
7112
|
recreated: report.components.recreated
|
|
6652
7113
|
},
|
|
7114
|
+
...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 } } : {},
|
|
7115
|
+
...report.files.some((f) => f.ignoreErrors?.length) ? { ignoreErrors: report.files.flatMap((f) => (f.ignoreErrors ?? []).map((e) => ({ file: f.path, ...e }))) } : {},
|
|
6653
7116
|
topFiles: report.topFiles.slice(0, limit),
|
|
6654
7117
|
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 } })),
|
|
6655
7118
|
...withFindings ? {
|
|
@@ -6784,19 +7247,19 @@ async function buildPlan(ctx, a, tool) {
|
|
|
6784
7247
|
file = v[v.length - 1].file;
|
|
6785
7248
|
}
|
|
6786
7249
|
let value = r.value;
|
|
6787
|
-
let
|
|
7250
|
+
let item2;
|
|
6788
7251
|
if (value === void 0) {
|
|
6789
7252
|
if (!r.entry) throw new Error("restore needs value, or entry (the history entry whose value to put back).");
|
|
6790
7253
|
const e = findEntry(report.entries, r.entry);
|
|
6791
7254
|
const items = tokenHistory([e], path, config).filter((i) => !file || i.file === file);
|
|
6792
7255
|
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.`);
|
|
6793
7256
|
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.`);
|
|
6794
|
-
|
|
6795
|
-
file ??=
|
|
6796
|
-
value = r.side === "before" ?
|
|
6797
|
-
if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${
|
|
7257
|
+
item2 = items[0];
|
|
7258
|
+
file ??= item2.file;
|
|
7259
|
+
value = r.side === "before" ? item2.before : item2.after;
|
|
7260
|
+
if (value === void 0) throw new Error(`${path} has no ${r.side ?? "after"} value in ${e.id} (it was ${item2.kind}).`);
|
|
6798
7261
|
}
|
|
6799
|
-
return planRestore({ current, path, ...file ? { file } : {}, value, ...
|
|
7262
|
+
return planRestore({ current, path, ...file ? { file } : {}, value, ...item2 ? { item: item2, label: item2.pr ? `#${item2.pr.number}` : item2.entryId } : {}, pairs, tool });
|
|
6800
7263
|
}
|
|
6801
7264
|
const entry = findEntry(report.entries, a.target);
|
|
6802
7265
|
const later = report.entries.filter((e) => e.id !== entry.id && Date.parse(e.date) >= Date.parse(entry.date));
|
|
@@ -7153,11 +7616,11 @@ async function pendingDrafts(ctx, a) {
|
|
|
7153
7616
|
const images = [];
|
|
7154
7617
|
if (a.images !== false) {
|
|
7155
7618
|
for (const s of sessions) {
|
|
7156
|
-
for (const
|
|
7157
|
-
for (const img of
|
|
7619
|
+
for (const item2 of s.items) {
|
|
7620
|
+
for (const img of item2.images ?? []) {
|
|
7158
7621
|
if (images.length >= IMAGES_PER_CALL || !img.path) continue;
|
|
7159
7622
|
const f = await gh.getFileBase64(img.path, REQUESTS_BRANCH).catch(() => void 0);
|
|
7160
|
-
if (f && f.size <= IMAGE_INLINE_MAX) images.push({ sessionId: s.sessionId, itemId:
|
|
7623
|
+
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 });
|
|
7161
7624
|
}
|
|
7162
7625
|
}
|
|
7163
7626
|
}
|
|
@@ -7189,7 +7652,7 @@ async function writeDrafts(ctx, a) {
|
|
|
7189
7652
|
}
|
|
7190
7653
|
|
|
7191
7654
|
// src/server.ts
|
|
7192
|
-
var VERSION = true ? "0.3.
|
|
7655
|
+
var VERSION = true ? "0.3.2" : "dev";
|
|
7193
7656
|
var ok = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
7194
7657
|
var fail = (e) => ({ content: [{ type: "text", text: `Error: ${e.message ?? String(e)}` }], isError: true });
|
|
7195
7658
|
var run = (fn) => async (a) => {
|
|
@@ -7547,13 +8010,14 @@ function createServer(ctx) {
|
|
|
7547
8010
|
"bridge_audit",
|
|
7548
8011
|
{
|
|
7549
8012
|
title: "Audit design system adoption",
|
|
7550
|
-
description: "Measure how a product codebase uses this design system (local only).
|
|
8013
|
+
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.",
|
|
7551
8014
|
inputSchema: {
|
|
7552
8015
|
path: z.string().min(1).describe("Product folder or file, absolute or relative to the design system repo root."),
|
|
7553
8016
|
baseline: z.string().optional().describe("Earlier report from `bridge audit --json` to compare with (progress per metric and file)."),
|
|
7554
8017
|
label: z.string().optional(),
|
|
7555
8018
|
limit: z.number().int().min(1).max(50).optional().describe("Top files and fixes to return. Default 10."),
|
|
7556
|
-
findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file.")
|
|
8019
|
+
findings: z.boolean().optional().describe("Include per-file findings with line numbers. Default: only when auditing one file."),
|
|
8020
|
+
allowlist: z.string().optional().describe('Allowlist file of known exceptions: { "entries": [{ "file", "line", "what"?, "reason" }] }, paths relative to the audited folder.')
|
|
7557
8021
|
},
|
|
7558
8022
|
annotations: RO
|
|
7559
8023
|
},
|
|
@@ -7683,7 +8147,7 @@ function createServer(ctx) {
|
|
|
7683
8147
|
},
|
|
7684
8148
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
7685
8149
|
},
|
|
7686
|
-
run((a) => startExploration(ctx, a))
|
|
8150
|
+
run((a) => startExploration(ctx, { ...a, source: "mcp" }))
|
|
7687
8151
|
);
|
|
7688
8152
|
server.registerTool(
|
|
7689
8153
|
"bridge_list_explorations",
|
package/dist/explorations.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { explorationRules, type ExplorationTryIt } from '@bridge-ds/core';
|
|
1
|
+
import { explorationRules, type ExplorationSource, type ExplorationTryIt } from '@bridge-ds/core';
|
|
2
2
|
import { BridgeContext } from './context.js';
|
|
3
3
|
/** `explore/<slug>` from a branch, a slug or a name; throws with how to fix it when it cannot be one. */
|
|
4
4
|
export declare function toExplorationBranch(ref: string): string;
|
|
@@ -8,6 +8,9 @@ export interface StartExplorationInput {
|
|
|
8
8
|
useCase?: string | undefined;
|
|
9
9
|
experimental?: string[] | undefined;
|
|
10
10
|
author?: string | undefined;
|
|
11
|
+
/** Where it is started from (the MCP tool says mcp, the CLI says cli), and the AI agent behind it when known. */
|
|
12
|
+
source?: ExplorationSource | undefined;
|
|
13
|
+
agent?: string | undefined;
|
|
11
14
|
dryRun?: boolean | undefined;
|
|
12
15
|
}
|
|
13
16
|
/**
|
|
@@ -106,6 +109,8 @@ export declare function listExplorations(ctx: BridgeContext, a?: {
|
|
|
106
109
|
status: import("@bridge-ds/core").ExplorationStatus;
|
|
107
110
|
draft: boolean;
|
|
108
111
|
author: string;
|
|
112
|
+
source?: ExplorationSource | undefined;
|
|
113
|
+
agent?: string | undefined;
|
|
109
114
|
updatedAt: string;
|
|
110
115
|
headSha: string;
|
|
111
116
|
previews: {
|
|
@@ -135,6 +140,8 @@ export declare function listExplorations(ctx: BridgeContext, a?: {
|
|
|
135
140
|
status: import("@bridge-ds/core").ExplorationStatus;
|
|
136
141
|
draft: boolean;
|
|
137
142
|
author: string;
|
|
143
|
+
source?: ExplorationSource | undefined;
|
|
144
|
+
agent?: string | undefined;
|
|
138
145
|
updatedAt: string;
|
|
139
146
|
headSha: string;
|
|
140
147
|
previews: {
|
|
@@ -170,6 +177,8 @@ export declare function listExplorations(ctx: BridgeContext, a?: {
|
|
|
170
177
|
status: import("@bridge-ds/core").ExplorationStatus;
|
|
171
178
|
draft: boolean;
|
|
172
179
|
author: string;
|
|
180
|
+
source?: ExplorationSource | undefined;
|
|
181
|
+
agent?: string | undefined;
|
|
173
182
|
updatedAt: string;
|
|
174
183
|
headSha: string;
|
|
175
184
|
})[];
|
package/dist/index.js
CHANGED
package/dist/lib.js
CHANGED
package/dist/ops.d.ts
CHANGED
|
@@ -651,6 +651,7 @@ export declare function auditOp(ctx: BridgeContext, a: {
|
|
|
651
651
|
label?: string;
|
|
652
652
|
limit?: number;
|
|
653
653
|
findings?: boolean;
|
|
654
|
+
allowlist?: string;
|
|
654
655
|
}): Promise<{
|
|
655
656
|
next: string;
|
|
656
657
|
findings?: {
|
|
@@ -671,23 +672,6 @@ export declare function auditOp(ctx: BridgeContext, a: {
|
|
|
671
672
|
library: string;
|
|
672
673
|
}[];
|
|
673
674
|
}[] | undefined;
|
|
674
|
-
byCategory: Partial<Record<import("@bridge-ds/core/audit").FindingCategory, import("@bridge-ds/core/audit").CategoryCount>>;
|
|
675
|
-
components: {
|
|
676
|
-
ds: Record<string, number>;
|
|
677
|
-
foreign: {
|
|
678
|
-
[k: string]: {
|
|
679
|
-
instances: number;
|
|
680
|
-
top: string[];
|
|
681
|
-
};
|
|
682
|
-
};
|
|
683
|
-
native: Record<string, number>;
|
|
684
|
-
recreated: {
|
|
685
|
-
name: string;
|
|
686
|
-
file: string;
|
|
687
|
-
line: number;
|
|
688
|
-
instances: number;
|
|
689
|
-
}[];
|
|
690
|
-
};
|
|
691
675
|
topFiles: {
|
|
692
676
|
path: string;
|
|
693
677
|
hardcoded: number;
|
|
@@ -710,6 +694,37 @@ export declare function auditOp(ctx: BridgeContext, a: {
|
|
|
710
694
|
category: import("@bridge-ds/core/audit").FindingCategory;
|
|
711
695
|
count: number;
|
|
712
696
|
})[];
|
|
697
|
+
ignoreErrors?: {
|
|
698
|
+
line: number;
|
|
699
|
+
message: string;
|
|
700
|
+
file: string;
|
|
701
|
+
}[] | undefined;
|
|
702
|
+
ignored?: {
|
|
703
|
+
total: number;
|
|
704
|
+
comment: number;
|
|
705
|
+
allowlist: number;
|
|
706
|
+
items: (import("@bridge-ds/core/audit").IgnoredItem & {
|
|
707
|
+
file: string;
|
|
708
|
+
})[];
|
|
709
|
+
staleAllowlist: import("@bridge-ds/core/audit").AllowlistEntry[];
|
|
710
|
+
} | undefined;
|
|
711
|
+
byCategory: Partial<Record<import("@bridge-ds/core/audit").FindingCategory, import("@bridge-ds/core/audit").CategoryCount>>;
|
|
712
|
+
components: {
|
|
713
|
+
ds: Record<string, number>;
|
|
714
|
+
foreign: {
|
|
715
|
+
[k: string]: {
|
|
716
|
+
instances: number;
|
|
717
|
+
top: string[];
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
native: Record<string, number>;
|
|
721
|
+
recreated: {
|
|
722
|
+
name: string;
|
|
723
|
+
file: string;
|
|
724
|
+
line: number;
|
|
725
|
+
instances: number;
|
|
726
|
+
}[];
|
|
727
|
+
};
|
|
713
728
|
sinceBaseline?: {
|
|
714
729
|
tokenAdoption: {
|
|
715
730
|
before: number | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge MCP server: lets Claude (Claude Code, Claude Desktop or any MCP client) read, check and propose changes to a DTCG design system repo through pull requests.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:package": "node ../../scripts/build-packages.mjs mcp"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@bridge-ds/core": "^0.
|
|
58
|
-
"@bridge-ds/visual": "^0.2.
|
|
57
|
+
"@bridge-ds/core": "^0.5.0",
|
|
58
|
+
"@bridge-ds/visual": "^0.2.3",
|
|
59
59
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
60
60
|
"typescript": "5.9.3",
|
|
61
61
|
"zod": "3.25.76"
|