dslinter 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -1
- package/bin/lib/dev-banner.mjs +54 -5
- package/bin/lib/dev-banner.test.mjs +17 -4
- package/bin/lib/network-hosts.mjs +81 -0
- package/bin/lib/network-hosts.test.mjs +57 -0
- package/bin/lib/project-root.mjs +25 -2
- package/bin/lib/project-root.test.mjs +24 -0
- package/bin/lib/scan-host.test.mjs +20 -0
- package/bin/modes/dev.mjs +14 -3
- package/dashboard-dist/assets/DashboardLayoutAuto-COU-fvpX.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-NfQasneG.js +1 -0
- package/dashboard-dist/assets/{axe-DHHCqGjV.js → axe-0qmg8n-4.js} +1 -1
- package/dashboard-dist/assets/index-B0oyZAfn.js +219 -0
- package/dashboard-dist/assets/index-BjnQAYrx.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/embed/App.tsx +22 -0
- package/embed/index.css +3 -0
- package/embed/main.tsx +10 -0
- package/embed/tokenCatalog.ts +48 -0
- package/index.cjs +52 -52
- package/index.html +12 -0
- package/package.json +10 -8
- package/src/components/Section.tsx +1 -1
- package/src/components/ui/badge.tsx +1 -1
- package/src/dashboard/ComponentUsageDetails.tsx +7 -39
- package/src/dashboard/DashboardBody.tsx +2 -7
- package/src/dashboard/FindingsList.tsx +64 -56
- package/src/dashboard/ScannedTokenWall.tsx +40 -25
- package/src/dashboard/SourceLocationLink.tsx +40 -0
- package/src/mcp/rule-catalog.json +2 -2
- package/src/playground/inferKitJsx.ts +141 -14
- package/src/playground/inferKitParams.ts +40 -2
- package/src/playground/playgroundJoin.test.ts +35 -1
- package/src/playground/playgroundJoin.ts +18 -0
- package/src/shell/DashboardLayoutAuto.tsx +30 -5
- package/vite/embed-serve.config.ts +59 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-BWuyjHPD.js +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +0 -1
- package/dashboard-dist/assets/index-Bxk7tA3F.js +0 -219
- package/dashboard-dist/assets/index-D0O_5w5V.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Table,
|
|
4
4
|
TableBody,
|
|
@@ -9,10 +9,8 @@ import {
|
|
|
9
9
|
} from "../components/ui/table";
|
|
10
10
|
import type { UsageLocation, WorkspaceReport } from "../types/report";
|
|
11
11
|
import { usageMap } from "./aggregate";
|
|
12
|
-
import { openSourceFile } from "./editorLink";
|
|
13
|
-
import { resolveReportAbsolutePath, shortPath } from "./paths";
|
|
14
12
|
import { EmptyCard } from "../components/EmptyCard";
|
|
15
|
-
import {
|
|
13
|
+
import { SourceLocationLink } from "./SourceLocationLink";
|
|
16
14
|
|
|
17
15
|
function formatCallSiteProps(loc: UsageLocation): string {
|
|
18
16
|
if (!loc.props.length) return "—";
|
|
@@ -33,40 +31,6 @@ function sortedLocations(
|
|
|
33
31
|
return list;
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
function UsageLocationLink({
|
|
37
|
-
root,
|
|
38
|
-
loc,
|
|
39
|
-
}: {
|
|
40
|
-
root: string;
|
|
41
|
-
loc: UsageLocation;
|
|
42
|
-
}) {
|
|
43
|
-
const fileText = shortPath(root, loc.path);
|
|
44
|
-
const locationText = `${fileText}:${loc.line}`;
|
|
45
|
-
const absolutePath = resolveReportAbsolutePath(root, loc.path);
|
|
46
|
-
|
|
47
|
-
const handleClick = useCallback(() => {
|
|
48
|
-
void openSourceFile(absolutePath, loc.line).catch((err) => {
|
|
49
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
-
window.alert(`Could not open file: ${message}`);
|
|
51
|
-
});
|
|
52
|
-
}, [absolutePath, loc.line]);
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<button
|
|
56
|
-
type="button"
|
|
57
|
-
onClick={handleClick}
|
|
58
|
-
className="block min-w-0 w-full text-left text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
59
|
-
title={locationText}
|
|
60
|
-
>
|
|
61
|
-
<TruncatedPath
|
|
62
|
-
path={`${fileText}:${loc.line}`}
|
|
63
|
-
className="text-xs"
|
|
64
|
-
title={undefined}
|
|
65
|
-
/>
|
|
66
|
-
</button>
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
34
|
export function ComponentUsageDetails({
|
|
71
35
|
report,
|
|
72
36
|
componentId,
|
|
@@ -117,7 +81,11 @@ export function ComponentUsageDetails({
|
|
|
117
81
|
return (
|
|
118
82
|
<TableRow key={`${loc.path}-${loc.line}-${i}`}>
|
|
119
83
|
<TableCell className="min-w-0">
|
|
120
|
-
<
|
|
84
|
+
<SourceLocationLink
|
|
85
|
+
root={report.root}
|
|
86
|
+
path={loc.path}
|
|
87
|
+
line={loc.line}
|
|
88
|
+
/>
|
|
121
89
|
</TableCell>
|
|
122
90
|
<TableCell className="min-w-0">
|
|
123
91
|
<span
|
|
@@ -79,11 +79,7 @@ export function DashboardBody({
|
|
|
79
79
|
|
|
80
80
|
<GovernanceInventoryTabs value={tab} onChange={setTab} counts={counts} />
|
|
81
81
|
|
|
82
|
-
<Section
|
|
83
|
-
id={section.id}
|
|
84
|
-
title={section.title}
|
|
85
|
-
description={section.description}
|
|
86
|
-
>
|
|
82
|
+
<Section id={section.id}>
|
|
87
83
|
{tab === "unused" ? (
|
|
88
84
|
<UnusedComponentsList
|
|
89
85
|
components={unusedComponents}
|
|
@@ -103,8 +99,7 @@ export function DashboardBody({
|
|
|
103
99
|
className="font-medium text-foreground underline decoration-dotted underline-offset-2 transition hover:decoration-solid"
|
|
104
100
|
>
|
|
105
101
|
View all components
|
|
106
|
-
</button>
|
|
107
|
-
{" "}
|
|
102
|
+
</button>{" "}
|
|
108
103
|
for prop usage and app reference details.
|
|
109
104
|
</p>
|
|
110
105
|
) : null}
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
TableHeader,
|
|
8
8
|
TableRow,
|
|
9
9
|
} from "../components/ui/table";
|
|
10
|
-
import { shortPath } from "./paths";
|
|
11
10
|
import type { LintFinding, Severity } from "../types/report";
|
|
12
11
|
import { Badge } from "../components/ui/badge";
|
|
12
|
+
import { EmptyCard } from "../components/EmptyCard";
|
|
13
13
|
import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
|
|
14
|
-
import {
|
|
14
|
+
import { SourceLocationLink } from "./SourceLocationLink";
|
|
15
15
|
|
|
16
16
|
type Filter = "all" | Severity;
|
|
17
17
|
|
|
@@ -24,6 +24,12 @@ function isFilter(value: string): value is Filter {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function emptyFilterMessage(filter: Exclude<Filter, "all">): string {
|
|
28
|
+
if (filter === "error") return "No errors in these findings.";
|
|
29
|
+
if (filter === "warning") return "No warnings in these findings.";
|
|
30
|
+
return "No info findings in these findings.";
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export function FindingsList({
|
|
28
34
|
findings,
|
|
29
35
|
root,
|
|
@@ -44,16 +50,15 @@ export function FindingsList({
|
|
|
44
50
|
}, [findings]);
|
|
45
51
|
|
|
46
52
|
const filtered = useMemo(
|
|
47
|
-
() =>
|
|
53
|
+
() =>
|
|
54
|
+
filter === "all"
|
|
55
|
+
? findings
|
|
56
|
+
: findings.filter((f) => f.severity === filter),
|
|
48
57
|
[findings, filter],
|
|
49
58
|
);
|
|
50
59
|
|
|
51
60
|
if (findings.length === 0) {
|
|
52
|
-
return
|
|
53
|
-
<p className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
|
|
54
|
-
No findings
|
|
55
|
-
</p>
|
|
56
|
-
);
|
|
61
|
+
return <EmptyCard>No findings</EmptyCard>;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
return (
|
|
@@ -107,55 +112,58 @@ export function FindingsList({
|
|
|
107
112
|
</ToggleGroupItem>
|
|
108
113
|
</ToggleGroup>
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<TableRow
|
|
122
|
-
key={`${f.rule_id}-${f.path}-${f.line ?? "x"}-${i}`}
|
|
123
|
-
className="border-border hover:bg-transparent"
|
|
124
|
-
>
|
|
125
|
-
<TableCell className="px-3 py-2">
|
|
126
|
-
<Badge
|
|
127
|
-
variant={
|
|
128
|
-
f.severity === "error"
|
|
129
|
-
? "destructive"
|
|
130
|
-
: f.severity === "warning"
|
|
131
|
-
? "secondary"
|
|
132
|
-
: "outline"
|
|
133
|
-
}
|
|
134
|
-
>
|
|
135
|
-
{f.severity}
|
|
136
|
-
</Badge>
|
|
137
|
-
</TableCell>
|
|
138
|
-
<TableCell className="px-3 py-2 font-mono text-xs text-muted-foreground">
|
|
139
|
-
{f.rule_id}
|
|
140
|
-
</TableCell>
|
|
141
|
-
<TableCell className="whitespace-normal px-3 py-2 text-sm">
|
|
142
|
-
{f.message}
|
|
143
|
-
</TableCell>
|
|
144
|
-
<TableCell className="min-w-0 px-3 py-2 font-mono text-xs text-muted-foreground">
|
|
145
|
-
<div className="flex min-w-0 items-baseline">
|
|
146
|
-
<TruncatedPath
|
|
147
|
-
path={shortPath(root, f.path)}
|
|
148
|
-
className="min-w-0 flex-1 text-xs"
|
|
149
|
-
/>
|
|
150
|
-
<span className="shrink-0">
|
|
151
|
-
:{f.line != null ? f.line : "—"}
|
|
152
|
-
</span>
|
|
153
|
-
</div>
|
|
154
|
-
</TableCell>
|
|
115
|
+
{filtered.length === 0 ? (
|
|
116
|
+
<EmptyCard className="mt-4">
|
|
117
|
+
{filter === "all" ? "No findings" : emptyFilterMessage(filter)}
|
|
118
|
+
</EmptyCard>
|
|
119
|
+
) : (
|
|
120
|
+
<Table className="mt-4">
|
|
121
|
+
<TableHeader>
|
|
122
|
+
<TableRow>
|
|
123
|
+
<TableHead scope="col">Severity</TableHead>
|
|
124
|
+
<TableHead scope="col">Rule</TableHead>
|
|
125
|
+
<TableHead scope="col">File</TableHead>
|
|
155
126
|
</TableRow>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
127
|
+
</TableHeader>
|
|
128
|
+
<TableBody className="align-top text-foreground">
|
|
129
|
+
{filtered.map((f, i) => (
|
|
130
|
+
<TableRow
|
|
131
|
+
key={`${f.rule_id}-${f.path}-${f.line ?? "x"}-${i}`}
|
|
132
|
+
className="border-border hover:bg-transparent"
|
|
133
|
+
>
|
|
134
|
+
<TableCell className="px-3 py-2">
|
|
135
|
+
<Badge
|
|
136
|
+
variant={
|
|
137
|
+
f.severity === "error"
|
|
138
|
+
? "destructive"
|
|
139
|
+
: f.severity === "warning"
|
|
140
|
+
? "secondary"
|
|
141
|
+
: "outline"
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
{f.severity}
|
|
145
|
+
</Badge>
|
|
146
|
+
</TableCell>
|
|
147
|
+
<TableCell className="min-w-0 whitespace-normal px-3 py-2">
|
|
148
|
+
<div className="font-mono text-xs text-muted-foreground">
|
|
149
|
+
{f.rule_id}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="mt-0.5 text-xs text-pretty text-foreground">
|
|
152
|
+
{f.message}
|
|
153
|
+
</div>
|
|
154
|
+
</TableCell>
|
|
155
|
+
<TableCell className="min-w-0 px-3 py-2">
|
|
156
|
+
<SourceLocationLink
|
|
157
|
+
root={root}
|
|
158
|
+
path={f.path}
|
|
159
|
+
line={f.line}
|
|
160
|
+
/>
|
|
161
|
+
</TableCell>
|
|
162
|
+
</TableRow>
|
|
163
|
+
))}
|
|
164
|
+
</TableBody>
|
|
165
|
+
</Table>
|
|
166
|
+
)}
|
|
159
167
|
</div>
|
|
160
168
|
);
|
|
161
169
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
HoverCardTrigger,
|
|
6
6
|
} from "../components/ui/hover-card";
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
|
+
import { EmptyCard } from "../components/EmptyCard";
|
|
8
9
|
import { TruncatedPath } from "../components/TruncatedPath";
|
|
9
10
|
import {
|
|
10
11
|
filterTokenRows,
|
|
@@ -20,6 +21,14 @@ const filterTabs: { id: TokenUsageFilter; label: string }[] = [
|
|
|
20
21
|
{ id: "unused", label: "Unused" },
|
|
21
22
|
];
|
|
22
23
|
|
|
24
|
+
function emptyFilterMessage(filter: TokenUsageFilter): string {
|
|
25
|
+
if (filter === "used") return "No used theme tokens match this filter.";
|
|
26
|
+
if (filter === "unused") {
|
|
27
|
+
return "No unused theme tokens — every scanned token is referenced in the workspace.";
|
|
28
|
+
}
|
|
29
|
+
return "No theme tokens found.";
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
function TokenSection({
|
|
24
33
|
title,
|
|
25
34
|
subtitle,
|
|
@@ -215,31 +224,37 @@ export function ScannedTokenWall({ view }: { view: MergedTokenView }) {
|
|
|
215
224
|
</div>
|
|
216
225
|
</div>
|
|
217
226
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
227
|
+
{filtered.length === 0 ? (
|
|
228
|
+
<EmptyCard>{emptyFilterMessage(filter)}</EmptyCard>
|
|
229
|
+
) : (
|
|
230
|
+
<>
|
|
231
|
+
<ColorSection rows={filtered} />
|
|
232
|
+
<ListSection
|
|
233
|
+
title="Spacing"
|
|
234
|
+
subtitle="--spacing-* custom properties."
|
|
235
|
+
rows={filtered}
|
|
236
|
+
category="spacing"
|
|
237
|
+
/>
|
|
238
|
+
<ListSection
|
|
239
|
+
title="Radius"
|
|
240
|
+
subtitle="--radius-* custom properties."
|
|
241
|
+
rows={filtered}
|
|
242
|
+
category="radius"
|
|
243
|
+
/>
|
|
244
|
+
<ListSection
|
|
245
|
+
title="Typography"
|
|
246
|
+
subtitle="--font-* custom properties."
|
|
247
|
+
rows={filtered}
|
|
248
|
+
category="typography"
|
|
249
|
+
/>
|
|
250
|
+
<ListSection
|
|
251
|
+
title="Other"
|
|
252
|
+
subtitle="Additional CSS variables."
|
|
253
|
+
rows={filtered}
|
|
254
|
+
category="other"
|
|
255
|
+
/>
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
243
258
|
</section>
|
|
244
259
|
);
|
|
245
260
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
3
|
+
import { openSourceFile } from "./editorLink";
|
|
4
|
+
import { resolveReportAbsolutePath, shortPath } from "./paths";
|
|
5
|
+
|
|
6
|
+
export function SourceLocationLink({
|
|
7
|
+
root,
|
|
8
|
+
path,
|
|
9
|
+
line,
|
|
10
|
+
}: {
|
|
11
|
+
root: string;
|
|
12
|
+
path: string;
|
|
13
|
+
line?: number | null;
|
|
14
|
+
}) {
|
|
15
|
+
const fileText = shortPath(root, path);
|
|
16
|
+
const locationText = line != null ? `${fileText}:${line}` : fileText;
|
|
17
|
+
const absolutePath = resolveReportAbsolutePath(root, path);
|
|
18
|
+
|
|
19
|
+
const handleClick = useCallback(() => {
|
|
20
|
+
void openSourceFile(absolutePath, line ?? undefined).catch((err) => {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
+
window.alert(`Could not open file: ${message}`);
|
|
23
|
+
});
|
|
24
|
+
}, [absolutePath, line]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={handleClick}
|
|
30
|
+
className="block min-w-0 w-full text-left text-xs text-muted-foreground transition-colors hover:text-foreground hover:underline"
|
|
31
|
+
title={locationText}
|
|
32
|
+
>
|
|
33
|
+
<TruncatedPath
|
|
34
|
+
path={locationText}
|
|
35
|
+
className="text-xs"
|
|
36
|
+
title={undefined}
|
|
37
|
+
/>
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
"rule_id": "code-inline-style",
|
|
116
116
|
"pillar": "code",
|
|
117
117
|
"default_severity": "warning",
|
|
118
|
-
"description": "Inline JSX style
|
|
119
|
-
"fix_hint": "Use className with theme utilities."
|
|
118
|
+
"description": "Inline JSX style uses hardcoded colors not in scanned CSS design tokens.",
|
|
119
|
+
"fix_hint": "Use className with theme utilities or CSS variables instead of inline color values."
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"rule_id": "code-empty-catch",
|
|
@@ -10,7 +10,53 @@ export type KitRootPropBinding = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
function kitSource(fn: Function): string {
|
|
13
|
-
|
|
13
|
+
const src = fn.toString();
|
|
14
|
+
const MAX_KIT_SOURCE_LEN = 64_000;
|
|
15
|
+
return src.length > MAX_KIT_SOURCE_LEN ? src.slice(0, MAX_KIT_SOURCE_LEN) : src;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function skipWhitespace(src: string, start: number): number {
|
|
19
|
+
let i = start;
|
|
20
|
+
while (i < src.length && /\s/.test(src[i]!)) i += 1;
|
|
21
|
+
return i;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isIdentChar(ch: string, first = false): boolean {
|
|
25
|
+
if (first) return /[A-Za-z_$]/.test(ch);
|
|
26
|
+
return /[\w$]/.test(ch);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readIdent(
|
|
30
|
+
src: string,
|
|
31
|
+
start: number,
|
|
32
|
+
firstUpper = false,
|
|
33
|
+
): { value: string; end: number } | null {
|
|
34
|
+
if (start >= src.length) return null;
|
|
35
|
+
const ch = src[start]!;
|
|
36
|
+
if (firstUpper && !/[A-Z]/.test(ch)) return null;
|
|
37
|
+
if (!isIdentChar(ch, true)) return null;
|
|
38
|
+
let end = start + 1;
|
|
39
|
+
while (end < src.length && isIdentChar(src[end]!, false)) end += 1;
|
|
40
|
+
const value = src.slice(start, end);
|
|
41
|
+
if (firstUpper && !/^[A-Z][A-Za-z0-9]*$/.test(value)) return null;
|
|
42
|
+
return { value, end };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipUntilChar(src: string, start: number, target: string): number {
|
|
46
|
+
let quote: "'" | '"' | "`" | null = null;
|
|
47
|
+
for (let i = start; i < src.length; i += 1) {
|
|
48
|
+
const ch = src[i]!;
|
|
49
|
+
if (quote) {
|
|
50
|
+
if (ch === quote && src[i - 1] !== "\\") quote = null;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
54
|
+
quote = ch;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === target) return i;
|
|
58
|
+
}
|
|
59
|
+
return -1;
|
|
14
60
|
}
|
|
15
61
|
|
|
16
62
|
/** Vite/esbuild wraps JSX as `(0, import.createElement)(Type, props)` (often split across lines). */
|
|
@@ -58,9 +104,45 @@ function collectJsxRuntimeSlots(src: string, out: KitJsxSlot[]): void {
|
|
|
58
104
|
}
|
|
59
105
|
|
|
60
106
|
function collectSourceJsxSlots(src: string, out: KitJsxSlot[]): void {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
let i = 0;
|
|
108
|
+
while (i < src.length) {
|
|
109
|
+
const lt = src.indexOf("<", i);
|
|
110
|
+
if (lt === -1) break;
|
|
111
|
+
if (src[lt + 1] === "/") {
|
|
112
|
+
i = lt + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const ident = readIdent(src, lt + 1, true);
|
|
116
|
+
if (!ident) {
|
|
117
|
+
i = lt + 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const tagEnd = skipUntilChar(src, ident.end, ">");
|
|
121
|
+
if (tagEnd === -1) break;
|
|
122
|
+
let j = skipWhitespace(src, tagEnd + 1);
|
|
123
|
+
if (src[j] !== "{") {
|
|
124
|
+
i = lt + 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
j = skipWhitespace(src, j + 1);
|
|
128
|
+
const paramIdent = readIdent(src, j);
|
|
129
|
+
if (!paramIdent) {
|
|
130
|
+
i = lt + 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
j = skipWhitespace(src, paramIdent.end);
|
|
134
|
+
if (src[j] !== "}") {
|
|
135
|
+
i = lt + 1;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
j = skipWhitespace(src, j + 1);
|
|
139
|
+
const closeTag = `</${ident.value}>`;
|
|
140
|
+
if (!src.slice(j).startsWith(closeTag)) {
|
|
141
|
+
i = lt + 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
out.push({ component: ident.value, param: paramIdent.value });
|
|
145
|
+
i = j + closeTag.length;
|
|
64
146
|
}
|
|
65
147
|
}
|
|
66
148
|
|
|
@@ -118,6 +200,60 @@ function parseJsxPropsObject(raw: string | undefined): Array<{ prop: string; par
|
|
|
118
200
|
return out;
|
|
119
201
|
}
|
|
120
202
|
|
|
203
|
+
function parseJsxAttrBindings(attrs: string, component: string, out: KitRootPropBinding[]): void {
|
|
204
|
+
let i = 0;
|
|
205
|
+
while (i < attrs.length) {
|
|
206
|
+
i = skipWhitespace(attrs, i);
|
|
207
|
+
if (i >= attrs.length) break;
|
|
208
|
+
const propIdent = readIdent(attrs, i);
|
|
209
|
+
if (!propIdent) {
|
|
210
|
+
i += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
i = skipWhitespace(attrs, propIdent.end);
|
|
214
|
+
if (attrs[i] !== "=") {
|
|
215
|
+
i = propIdent.end;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
i = skipWhitespace(attrs, i + 1);
|
|
219
|
+
if (attrs[i] !== "{") {
|
|
220
|
+
i += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
i = skipWhitespace(attrs, i + 1);
|
|
224
|
+
const paramIdent = readIdent(attrs, i);
|
|
225
|
+
if (!paramIdent) continue;
|
|
226
|
+
const prop = propIdent.value;
|
|
227
|
+
if (prop === "className" || prop === "style" || prop === "asChild") {
|
|
228
|
+
i = paramIdent.end;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
out.push({ component, prop, param: paramIdent.value });
|
|
232
|
+
i = paramIdent.end;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectSourceRootPropBindings(src: string, out: KitRootPropBinding[]): void {
|
|
237
|
+
let i = 0;
|
|
238
|
+
while (i < src.length) {
|
|
239
|
+
const lt = src.indexOf("<", i);
|
|
240
|
+
if (lt === -1) break;
|
|
241
|
+
if (src[lt + 1] === "/") {
|
|
242
|
+
i = lt + 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const ident = readIdent(src, lt + 1, true);
|
|
246
|
+
if (!ident) {
|
|
247
|
+
i = lt + 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const tagEnd = skipUntilChar(src, ident.end, ">");
|
|
251
|
+
if (tagEnd === -1) break;
|
|
252
|
+
parseJsxAttrBindings(src.slice(ident.end, tagEnd), ident.value, out);
|
|
253
|
+
i = tagEnd + 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
121
257
|
function collectRootPropBindings(src: string, out: KitRootPropBinding[]): void {
|
|
122
258
|
const normalized = normalizeKitSource(src);
|
|
123
259
|
for (const match of normalized.matchAll(RUNTIME_ROOT_PROPS_RE)) {
|
|
@@ -126,16 +262,7 @@ function collectRootPropBindings(src: string, out: KitRootPropBinding[]): void {
|
|
|
126
262
|
}
|
|
127
263
|
}
|
|
128
264
|
|
|
129
|
-
|
|
130
|
-
for (const match of src.matchAll(sourceRoot)) {
|
|
131
|
-
const attrs = match[2] ?? "";
|
|
132
|
-
const attrRe = /\b([A-Za-z_$][\w$]*)\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
133
|
-
for (const attr of attrs.matchAll(attrRe)) {
|
|
134
|
-
const prop = attr[1]!;
|
|
135
|
-
if (prop === "className" || prop === "style" || prop === "asChild") continue;
|
|
136
|
-
out.push({ component: match[1]!, prop, param: attr[2]! });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
265
|
+
collectSourceRootPropBindings(src, out);
|
|
139
266
|
}
|
|
140
267
|
|
|
141
268
|
/** Root component prop bindings such as `<Alert variant={variant}>`. */
|
|
@@ -91,8 +91,46 @@ function findTopLevelChar(input: string, target: string): number {
|
|
|
91
91
|
|
|
92
92
|
function objectDestructuringSource(fn: Function): string | undefined {
|
|
93
93
|
const src = fn.toString().trim();
|
|
94
|
-
const
|
|
95
|
-
|
|
94
|
+
const MAX_KIT_SOURCE_LEN = 64_000;
|
|
95
|
+
const bounded = src.length > MAX_KIT_SOURCE_LEN ? src.slice(0, MAX_KIT_SOURCE_LEN) : src;
|
|
96
|
+
|
|
97
|
+
let i = 0;
|
|
98
|
+
if (bounded.startsWith("async")) i = "async".length;
|
|
99
|
+
i = skipWhitespace(bounded, i);
|
|
100
|
+
if (bounded.slice(i).startsWith("function")) i += "function".length;
|
|
101
|
+
i = skipWhitespace(bounded, i);
|
|
102
|
+
if (bounded[i] !== "(") return undefined;
|
|
103
|
+
i = skipWhitespace(bounded, i + 1);
|
|
104
|
+
if (bounded[i] !== "{") return undefined;
|
|
105
|
+
|
|
106
|
+
const start = i + 1;
|
|
107
|
+
let depth = 1;
|
|
108
|
+
i += 1;
|
|
109
|
+
let quote: "'" | '"' | "`" | null = null;
|
|
110
|
+
while (i < bounded.length && depth > 0) {
|
|
111
|
+
const ch = bounded[i]!;
|
|
112
|
+
if (quote) {
|
|
113
|
+
if (ch === quote && bounded[i - 1] !== "\\") quote = null;
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
118
|
+
quote = ch;
|
|
119
|
+
i += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (ch === "{") depth += 1;
|
|
123
|
+
else if (ch === "}") depth -= 1;
|
|
124
|
+
i += 1;
|
|
125
|
+
}
|
|
126
|
+
if (depth !== 0) return undefined;
|
|
127
|
+
return bounded.slice(start, i - 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function skipWhitespace(src: string, start: number): number {
|
|
131
|
+
let i = start;
|
|
132
|
+
while (i < src.length && /\s/.test(src[i]!)) i += 1;
|
|
133
|
+
return i;
|
|
96
134
|
}
|
|
97
135
|
|
|
98
136
|
/** Read destructured parameter keys (and inline defaults) from a kit callback. */
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { WorkspaceReport } from "../types/report";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
defaultConsumerGlobKeyFromRelPath,
|
|
5
|
+
diagnosePlaygroundJoinSkips,
|
|
6
|
+
isStaticBundledPreviewUnavailable,
|
|
7
|
+
} from "./playgroundJoin";
|
|
4
8
|
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
5
9
|
|
|
6
10
|
describe("defaultConsumerGlobKeyFromRelPath", () => {
|
|
@@ -140,3 +144,33 @@ describe("diagnosePlaygroundJoinSkips", () => {
|
|
|
140
144
|
expect(entries[0]?.modulePath).toBe("../Components/Button.tsx");
|
|
141
145
|
});
|
|
142
146
|
});
|
|
147
|
+
|
|
148
|
+
describe("isStaticBundledPreviewUnavailable", () => {
|
|
149
|
+
it("returns true in production when all skips are missing embed modules", () => {
|
|
150
|
+
const skipped = [
|
|
151
|
+
{
|
|
152
|
+
export_name: "AppLogo",
|
|
153
|
+
rel_path: "resources/js/components/app-logo.tsx",
|
|
154
|
+
globKey: "@dslinter-scan/resources/js/components/app-logo.tsx",
|
|
155
|
+
reason: "module_not_found" as const,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
expect(isStaticBundledPreviewUnavailable(skipped, { production: true })).toBe(
|
|
159
|
+
true,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns false in dev mode", () => {
|
|
164
|
+
const skipped = [
|
|
165
|
+
{
|
|
166
|
+
export_name: "AppLogo",
|
|
167
|
+
rel_path: "resources/js/components/app-logo.tsx",
|
|
168
|
+
globKey: "@dslinter-scan/resources/js/components/app-logo.tsx",
|
|
169
|
+
reason: "module_not_found" as const,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
expect(isStaticBundledPreviewUnavailable(skipped, { production: false })).toBe(
|
|
173
|
+
false,
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
});
|