dslinter 0.1.3 → 0.1.5
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 +20 -0
- package/README.md +44 -27
- package/bin/modes/build.mjs +6 -0
- package/bin/modes/dev.mjs +16 -1
- package/bin/modes/init.mjs +10 -14
- package/dashboard-dist/assets/DashboardLayoutAuto-BPPtPsYh.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Dp3bAQxH.js +1 -0
- package/dashboard-dist/assets/index-DsjwnDdX.js +206 -0
- package/dashboard-dist/assets/index-jaCmZJlW.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +16 -7
- package/src/components/ComponentInspectPane.tsx +9 -13
- package/src/components/DashboardCommandPalette.tsx +1 -1
- package/src/components/PlaygroundA11yAndCode.tsx +3 -3
- package/src/components/PlaygroundControlField.tsx +4 -4
- package/src/components/PlaygroundControls.tsx +1 -1
- package/src/components/PlaygroundVariantMatrix.tsx +1 -1
- package/src/components/Sidebar.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +2 -2
- package/src/dashboard/ComponentUsageDetails.tsx +11 -5
- package/src/dashboard/DashboardBody.tsx +1 -1
- package/src/dashboard/FindingsList.tsx +3 -3
- package/src/dashboard/ScannedTokenWall.tsx +1 -1
- package/src/dashboard/mergeTokenCatalog.ts +1 -1
- package/src/index.ts +1 -0
- package/src/playground/scanPlaygroundModules.ts +1 -0
- package/src/playground/usePlaygroundFromReport.test.ts +37 -0
- package/src/playground/usePlaygroundFromReport.ts +23 -0
- package/src/playground/virtual-playground-modules.d.ts +6 -0
- package/src/shell/DashboardLayout.tsx +38 -5
- package/src/shell/DashboardLayoutAuto.tsx +20 -0
- package/templates/vite.dslint-scan-alias.snippet.ts +4 -21
- package/templates/vite.dslinter.snippet.ts +14 -16
- package/vite/collectScanModules.test.ts +42 -0
- package/vite/collectScanModules.ts +60 -0
- package/vite/consumer.config.mjs +22 -0
- package/vite/plugin.ts +140 -0
- package/dashboard-dist/assets/index-BhDQfrwA.css +0 -1
- package/dashboard-dist/assets/index-DGUG_3SK.js +0 -205
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
HoverCard,
|
|
4
4
|
HoverCardContent,
|
|
5
5
|
HoverCardTrigger,
|
|
6
|
-
} from "
|
|
6
|
+
} from "../components/ui/hover-card";
|
|
7
7
|
import {
|
|
8
8
|
Table,
|
|
9
9
|
TableBody,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
TableHead,
|
|
12
12
|
TableHeader,
|
|
13
13
|
TableRow,
|
|
14
|
-
} from "
|
|
14
|
+
} from "../components/ui/table";
|
|
15
15
|
import {
|
|
16
16
|
aggregateDeclaredProps,
|
|
17
17
|
aggregateDefinitions,
|
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
TableHead,
|
|
7
7
|
TableHeader,
|
|
8
8
|
TableRow,
|
|
9
|
-
} from "
|
|
9
|
+
} from "../components/ui/table";
|
|
10
10
|
import type { UsageLocation, WorkspaceReport } from "../types/report";
|
|
11
11
|
import { usageMap } from "./aggregate";
|
|
12
12
|
import { shortPath } from "./paths";
|
|
13
13
|
import { EmptyCard } from "../components/EmptyCard";
|
|
14
|
-
import { InlineCode } from "
|
|
14
|
+
import { InlineCode } from "../components/InlineCode";
|
|
15
15
|
|
|
16
16
|
function formatCallSiteProps(loc: UsageLocation): string {
|
|
17
17
|
if (!loc.props.length) return "—";
|
|
@@ -76,18 +76,24 @@ export function ComponentUsageDetails({
|
|
|
76
76
|
<Table className="[&>table]:table-fixed [&>table]:w-full">
|
|
77
77
|
<TableHeader>
|
|
78
78
|
<TableRow>
|
|
79
|
-
<TableHead className="w-[40%]">File</TableHead>
|
|
79
|
+
<TableHead className="w-[40%] min-w-0">File</TableHead>
|
|
80
80
|
<TableHead className="w-14 whitespace-nowrap">Line</TableHead>
|
|
81
81
|
<TableHead className="min-w-0">Props at this call site</TableHead>
|
|
82
82
|
</TableRow>
|
|
83
83
|
</TableHeader>
|
|
84
84
|
<TableBody>
|
|
85
85
|
{rows.map((loc, i) => {
|
|
86
|
+
const fileText = shortPath(report.root, loc.path);
|
|
86
87
|
const propsText = formatCallSiteProps(loc);
|
|
87
88
|
return (
|
|
88
89
|
<TableRow key={`${loc.path}-${loc.line}-${i}`}>
|
|
89
|
-
<TableCell className="
|
|
90
|
-
|
|
90
|
+
<TableCell className="min-w-0 max-w-0">
|
|
91
|
+
<span
|
|
92
|
+
className="block truncate font-mono text-xs text-foreground"
|
|
93
|
+
title={fileText}
|
|
94
|
+
>
|
|
95
|
+
{fileText}
|
|
96
|
+
</span>
|
|
91
97
|
</TableCell>
|
|
92
98
|
<TableCell className="tabular-nums text-muted-foreground">
|
|
93
99
|
{loc.line}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
TableHead,
|
|
7
7
|
TableHeader,
|
|
8
8
|
TableRow,
|
|
9
|
-
} from "
|
|
9
|
+
} from "../components/ui/table";
|
|
10
10
|
import type { WorkspaceReport } from "../types/report";
|
|
11
11
|
import { ComponentCatalog } from "./ComponentCatalog";
|
|
12
12
|
import { FindingsList } from "./FindingsList";
|
|
@@ -6,11 +6,11 @@ import {
|
|
|
6
6
|
TableHead,
|
|
7
7
|
TableHeader,
|
|
8
8
|
TableRow,
|
|
9
|
-
} from "
|
|
9
|
+
} from "../components/ui/table";
|
|
10
10
|
import { shortPath } from "./paths";
|
|
11
11
|
import type { LintFinding, Severity } from "../types/report";
|
|
12
|
-
import { Badge } from "
|
|
13
|
-
import { ToggleGroup, ToggleGroupItem } from "
|
|
12
|
+
import { Badge } from "../components/ui/badge";
|
|
13
|
+
import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
|
|
14
14
|
|
|
15
15
|
type Filter = "all" | Severity;
|
|
16
16
|
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
createPlaygroundRegistry,
|
|
40
40
|
createPlaygroundRegistryEntriesOnly,
|
|
41
41
|
} from "./playground/createPlaygroundRegistry";
|
|
42
|
+
export { usePlaygroundFromReport } from "./playground/usePlaygroundFromReport";
|
|
42
43
|
export type { CreatePlaygroundRegistryOptions } from "./playground/createPlaygroundRegistry";
|
|
43
44
|
export type { DefinedPlayground } from "./playground/definePlayground";
|
|
44
45
|
export type { PlaygroundPreviewProps, PlaygroundPreviewComponent } from "./types/preview";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { scanPlaygroundModules } from "virtual:dslinter/playground-modules";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { embedGlobKeyFromRelPath } from "../../vite/collectScanModules";
|
|
3
|
+
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
4
|
+
|
|
5
|
+
describe("autoPlayground join (embed glob keys)", () => {
|
|
6
|
+
it("joins report playgrounds to virtual module keys", () => {
|
|
7
|
+
const relPath =
|
|
8
|
+
"resources/js/Components/Billing/AdditionalEventLimitModal.tsx";
|
|
9
|
+
const globKey = embedGlobKeyFromRelPath(relPath);
|
|
10
|
+
const modules = {
|
|
11
|
+
[globKey]: {
|
|
12
|
+
AdditionalEventLimitModal: function AdditionalEventLimitModal() {
|
|
13
|
+
return null;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
const report = {
|
|
18
|
+
playgrounds: [
|
|
19
|
+
{
|
|
20
|
+
id: "AdditionalEventLimitModal",
|
|
21
|
+
export_name: "AdditionalEventLimitModal",
|
|
22
|
+
rel_path: relPath,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(
|
|
28
|
+
report,
|
|
29
|
+
modules,
|
|
30
|
+
{ logJoinSkips: false },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(skipped).toHaveLength(0);
|
|
34
|
+
expect(entries).toHaveLength(1);
|
|
35
|
+
expect(entries[0]?.modulePath).toBe(globKey);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { WorkspaceReport } from "../types/report";
|
|
3
|
+
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
4
|
+
import type { BuildPlaygroundResult } from "./buildPlaygroundEntriesFromReport";
|
|
5
|
+
import { scanPlaygroundModules } from "./scanPlaygroundModules";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Join scanner `playgrounds` to eager modules from the dslinter Vite plugin
|
|
9
|
+
* (`virtual:dslinter/playground-modules`). Requires `plugins: [dslinter()]` from
|
|
10
|
+
* `dslinter/vite`, or `npx dslinter` which merges the plugin automatically.
|
|
11
|
+
*/
|
|
12
|
+
export function usePlaygroundFromReport(
|
|
13
|
+
report: WorkspaceReport | null | undefined,
|
|
14
|
+
): BuildPlaygroundResult {
|
|
15
|
+
return useMemo(
|
|
16
|
+
() =>
|
|
17
|
+
buildPlaygroundEntriesFromReportWithSkips(
|
|
18
|
+
report,
|
|
19
|
+
scanPlaygroundModules,
|
|
20
|
+
),
|
|
21
|
+
[report],
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import {
|
|
3
3
|
createContext,
|
|
4
|
+
lazy,
|
|
5
|
+
Suspense,
|
|
4
6
|
useCallback,
|
|
5
7
|
useContext,
|
|
6
8
|
useEffect,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
import type { PlaygroundEntry } from "../types/playground";
|
|
11
13
|
import type { TokenCatalog } from "../types/tokenCatalog";
|
|
12
14
|
import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
|
|
13
|
-
import { Button } from "
|
|
15
|
+
import { Button } from "../components/ui/button";
|
|
14
16
|
import { cn } from "../lib/utils";
|
|
15
17
|
import { ComponentInspectPane } from "../components/ComponentInspectPane";
|
|
16
18
|
import { ComponentPlaygroundPane } from "../components/ComponentPlaygroundPane";
|
|
@@ -26,6 +28,8 @@ import {
|
|
|
26
28
|
} from "../playground/playgroundJoin";
|
|
27
29
|
import { useHashRoute } from "./useHashRoute";
|
|
28
30
|
|
|
31
|
+
const DashboardLayoutAuto = lazy(() => import("./DashboardLayoutAuto"));
|
|
32
|
+
|
|
29
33
|
const STORAGE_KEY = "dslinter-dashboard-theme";
|
|
30
34
|
|
|
31
35
|
export type DashboardThemePreference = "light" | "dark";
|
|
@@ -124,7 +128,14 @@ export function useDashboardTheme(): DashboardThemeContextValue {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
export type DashboardLayoutProps = {
|
|
127
|
-
|
|
131
|
+
/**
|
|
132
|
+
* When true, loads playground modules from the dslinter Vite plugin
|
|
133
|
+
* (`virtual:dslinter/playground-modules`). Requires `plugins: [dslinter()]`
|
|
134
|
+
* from `dslinter/vite`, or running via `npx dslinter`.
|
|
135
|
+
*/
|
|
136
|
+
autoPlayground?: boolean;
|
|
137
|
+
/** Required unless `autoPlayground` is true. */
|
|
138
|
+
playgroundEntries?: PlaygroundEntry[];
|
|
128
139
|
/** Join failures from `buildPlaygroundEntriesFromReportWithSkips` — powers inspect-pane hints. */
|
|
129
140
|
playgroundJoinSkips?: PlaygroundJoinSkip[];
|
|
130
141
|
tokenCatalog?: TokenCatalog;
|
|
@@ -140,7 +151,15 @@ export type DashboardLayoutProps = {
|
|
|
140
151
|
dslinterReport: DslinterReportState;
|
|
141
152
|
};
|
|
142
153
|
|
|
143
|
-
|
|
154
|
+
export type DashboardLayoutInnerProps = Omit<
|
|
155
|
+
DashboardLayoutProps,
|
|
156
|
+
"autoPlayground" | "playgroundEntries" | "playgroundJoinSkips"
|
|
157
|
+
> & {
|
|
158
|
+
playgroundEntries: PlaygroundEntry[];
|
|
159
|
+
playgroundJoinSkips?: PlaygroundJoinSkip[];
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export function DashboardLayoutInner({
|
|
144
163
|
playgroundEntries,
|
|
145
164
|
playgroundJoinSkips,
|
|
146
165
|
tokenCatalog,
|
|
@@ -149,7 +168,7 @@ function DashboardLayoutInner({
|
|
|
149
168
|
dslinterReportHint,
|
|
150
169
|
formatModulePath,
|
|
151
170
|
dslinterReport,
|
|
152
|
-
}:
|
|
171
|
+
}: DashboardLayoutInnerProps) {
|
|
153
172
|
const [route, navigate] = useHashRoute();
|
|
154
173
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
155
174
|
const { theme, setTheme, resolvedTheme } = useDashboardTheme();
|
|
@@ -267,9 +286,23 @@ function DashboardLayoutInner({
|
|
|
267
286
|
}
|
|
268
287
|
|
|
269
288
|
export function DashboardLayout(props: DashboardLayoutProps) {
|
|
289
|
+
if (props.autoPlayground) {
|
|
290
|
+
return (
|
|
291
|
+
<DashboardThemeProvider>
|
|
292
|
+
<Suspense fallback={null}>
|
|
293
|
+
<DashboardLayoutAuto {...props} />
|
|
294
|
+
</Suspense>
|
|
295
|
+
</DashboardThemeProvider>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
270
299
|
return (
|
|
271
300
|
<DashboardThemeProvider>
|
|
272
|
-
<DashboardLayoutInner
|
|
301
|
+
<DashboardLayoutInner
|
|
302
|
+
{...props}
|
|
303
|
+
playgroundEntries={props.playgroundEntries ?? []}
|
|
304
|
+
playgroundJoinSkips={props.playgroundJoinSkips}
|
|
305
|
+
/>
|
|
273
306
|
</DashboardThemeProvider>
|
|
274
307
|
);
|
|
275
308
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DashboardLayoutProps } from "./DashboardLayout";
|
|
2
|
+
import { usePlaygroundFromReport } from "../playground/usePlaygroundFromReport";
|
|
3
|
+
import { DashboardLayoutInner } from "./DashboardLayout";
|
|
4
|
+
|
|
5
|
+
/** Loaded via `React.lazy` when `autoPlayground` is set (pulls virtual playground modules). */
|
|
6
|
+
export default function DashboardLayoutAuto(props: DashboardLayoutProps) {
|
|
7
|
+
const autoPlaygroundBuild = usePlaygroundFromReport(props.dslinterReport.report);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<DashboardLayoutInner
|
|
11
|
+
{...props}
|
|
12
|
+
playgroundEntries={autoPlaygroundBuild.entries}
|
|
13
|
+
playgroundJoinSkips={autoPlaygroundBuild.skipped}
|
|
14
|
+
formatModulePath={
|
|
15
|
+
props.formatModulePath ??
|
|
16
|
+
((modulePath: string) => modulePath.replace(/^@dslint-scan\//, ""))
|
|
17
|
+
}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -1,21 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* const scanRoot = path.resolve(process.env.DSLINT_SCAN_ROOT ?? ".");
|
|
6
|
-
*
|
|
7
|
-
* resolve: {
|
|
8
|
-
* alias: {
|
|
9
|
-
* "@dslint-scan": scanRoot,
|
|
10
|
-
* },
|
|
11
|
-
* },
|
|
12
|
-
* server: {
|
|
13
|
-
* fs: { allow: [scanRoot] },
|
|
14
|
-
* },
|
|
15
|
-
*
|
|
16
|
-
* App:
|
|
17
|
-
* const modules = import.meta.glob("@dslint-scan/**/*.{tsx,jsx}", { eager: true });
|
|
18
|
-
* buildPlaygroundEntriesFromReport(report, modules);
|
|
19
|
-
*
|
|
20
|
-
* Prefer `npx dslinter init --laravel` (relative glob) when you have your own Vite app.
|
|
21
|
-
*/
|
|
1
|
+
// Deprecated — use `import dslinter from "dslinter/vite"` instead.
|
|
2
|
+
// The plugin generates virtual playground modules; no @dslint-scan alias required.
|
|
3
|
+
|
|
4
|
+
export {};
|
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Legacy snippet — prefer `import dslinter from "dslinter/vite"` (one plugin line).
|
|
3
|
+
* `npx dslinter` merges the plugin automatically when a consumer Vite app exists.
|
|
4
4
|
*/
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
5
|
const DSLINT_SERVE_PORT = Number(process.env.DSLINT_SERVE_PORT ?? "7878");
|
|
8
6
|
|
|
9
|
-
// Inside defineConfig(({ mode }) => ({ ... })):
|
|
10
7
|
export const dslinterViteSnippet = {
|
|
11
8
|
resolve: {
|
|
12
9
|
dedupe: ["react", "react-dom"],
|
|
13
10
|
},
|
|
11
|
+
optimizeDeps: {
|
|
12
|
+
exclude: ["dslinter"],
|
|
13
|
+
},
|
|
14
14
|
server: {
|
|
15
|
-
proxy:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
|
|
24
|
-
changeOrigin: true,
|
|
25
|
-
},
|
|
15
|
+
proxy: {
|
|
16
|
+
"/dslint-report.json": {
|
|
17
|
+
target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
|
|
18
|
+
changeOrigin: true,
|
|
19
|
+
},
|
|
20
|
+
"/events": {
|
|
21
|
+
target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
|
|
22
|
+
changeOrigin: true,
|
|
26
23
|
},
|
|
24
|
+
},
|
|
27
25
|
},
|
|
28
26
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
collectScanModuleRelPaths,
|
|
7
|
+
embedGlobKeyFromRelPath,
|
|
8
|
+
} from "./collectScanModules";
|
|
9
|
+
|
|
10
|
+
describe("embedGlobKeyFromRelPath", () => {
|
|
11
|
+
it("maps Laravel rel_path to @dslint-scan key", () => {
|
|
12
|
+
expect(
|
|
13
|
+
embedGlobKeyFromRelPath(
|
|
14
|
+
"resources/js/Components/Billing/AdditionalEventLimitModal.tsx",
|
|
15
|
+
),
|
|
16
|
+
).toBe(
|
|
17
|
+
"@dslint-scan/resources/js/Components/Billing/AdditionalEventLimitModal.tsx",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("strips leading slashes", () => {
|
|
22
|
+
expect(embedGlobKeyFromRelPath("/src/components/Foo.tsx")).toBe(
|
|
23
|
+
"@dslint-scan/src/components/Foo.tsx",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("collectScanModuleRelPaths", () => {
|
|
29
|
+
it("collects tsx/jsx and skips node_modules", () => {
|
|
30
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-"));
|
|
31
|
+
mkdirSync(join(root, "resources", "js", "Components"), { recursive: true });
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(root, "resources", "js", "Components", "Button.tsx"),
|
|
34
|
+
"export function Button() { return null; }",
|
|
35
|
+
);
|
|
36
|
+
mkdirSync(join(root, "node_modules", "pkg"), { recursive: true });
|
|
37
|
+
writeFileSync(join(root, "node_modules", "pkg", "Ignored.tsx"), "");
|
|
38
|
+
|
|
39
|
+
const paths = collectScanModuleRelPaths(root);
|
|
40
|
+
expect(paths).toEqual(["resources/js/Components/Button.tsx"]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const SKIP_DIR_NAMES = new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
"vendor",
|
|
7
|
+
".git",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"dashboard-dist",
|
|
11
|
+
"storage",
|
|
12
|
+
"bootstrap",
|
|
13
|
+
"coverage",
|
|
14
|
+
".next",
|
|
15
|
+
".nuxt",
|
|
16
|
+
".output",
|
|
17
|
+
".turbo",
|
|
18
|
+
".cache",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SOURCE_EXT = /\.(tsx|jsx)$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Collect repo-relative posix paths for `.tsx`/`.jsx` files under `scanRoot`.
|
|
25
|
+
*/
|
|
26
|
+
export function collectScanModuleRelPaths(scanRoot: string): string[] {
|
|
27
|
+
const root = resolve(scanRoot);
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
|
|
30
|
+
function walk(dir: string): void {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const ent of entries) {
|
|
38
|
+
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
39
|
+
const full = join(dir, ent.name);
|
|
40
|
+
if (ent.isDirectory()) {
|
|
41
|
+
if (SKIP_DIR_NAMES.has(ent.name)) continue;
|
|
42
|
+
walk(full);
|
|
43
|
+
} else if (ent.isFile() && SOURCE_EXT.test(ent.name)) {
|
|
44
|
+
out.push(relative(root, full).replace(/\\/g, "/"));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
walk(root);
|
|
50
|
+
out.sort();
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Virtual module map key for a scanner `rel_path` (embed convention).
|
|
56
|
+
*/
|
|
57
|
+
export function embedGlobKeyFromRelPath(relPath: string): string {
|
|
58
|
+
const trimmed = relPath.replace(/^\/+/, "");
|
|
59
|
+
return `@dslint-scan/${trimmed}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges the consumer's vite.config with the dslinter plugin.
|
|
5
|
+
* Used by `npx dslinter` when a host Vite app is detected (`DSLINT_VITE_ROOT`).
|
|
6
|
+
*/
|
|
7
|
+
export default defineConfig(async ({ command, mode }) => {
|
|
8
|
+
const viteRoot = process.env.DSLINT_VITE_ROOT?.trim() || process.cwd();
|
|
9
|
+
const loaded = await loadConfigFromFile(
|
|
10
|
+
{ command, mode },
|
|
11
|
+
undefined,
|
|
12
|
+
viteRoot,
|
|
13
|
+
);
|
|
14
|
+
const userConfig = loaded?.config ?? {};
|
|
15
|
+
const { default: dslinter } = await import("./plugin.ts");
|
|
16
|
+
return mergeConfig(
|
|
17
|
+
userConfig,
|
|
18
|
+
defineConfig({
|
|
19
|
+
plugins: [dslinter()],
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
});
|
package/vite/plugin.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Plugin, UserConfig } from "vite";
|
|
3
|
+
import {
|
|
4
|
+
collectScanModuleRelPaths,
|
|
5
|
+
embedGlobKeyFromRelPath,
|
|
6
|
+
} from "./collectScanModules";
|
|
7
|
+
|
|
8
|
+
export const VIRTUAL_PLAYGROUND_MODULES_ID = "virtual:dslinter/playground-modules";
|
|
9
|
+
const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_PLAYGROUND_MODULES_ID}`;
|
|
10
|
+
|
|
11
|
+
export type DslinterVitePluginOptions = {
|
|
12
|
+
/** Scan root (repo root passed to `npx dslinter`). Defaults to `DSLINT_SCAN_ROOT` or `process.cwd()`. */
|
|
13
|
+
scanRoot?: string;
|
|
14
|
+
/** Scanner HTTP port for report + SSE proxy in `serve` mode. */
|
|
15
|
+
servePort?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function defaultServePort(): number {
|
|
19
|
+
const fromEnv = process.env.DSLINT_SERVE_PORT?.trim();
|
|
20
|
+
if (fromEnv) {
|
|
21
|
+
const n = Number.parseInt(fromEnv, 10);
|
|
22
|
+
if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
|
|
23
|
+
}
|
|
24
|
+
return 7878;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function generatePlaygroundModulesSource(
|
|
28
|
+
scanRoot: string,
|
|
29
|
+
relPaths: string[],
|
|
30
|
+
): string {
|
|
31
|
+
const root = resolve(scanRoot);
|
|
32
|
+
const lines: string[] = [
|
|
33
|
+
"// Generated by dslinter/vite — do not edit",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < relPaths.length; i++) {
|
|
37
|
+
const rel = relPaths[i]!;
|
|
38
|
+
const abs = resolve(root, rel).replace(/\\/g, "/");
|
|
39
|
+
lines.push(`import * as __dslinter_m${i} from ${JSON.stringify(abs)};`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.push("export const scanPlaygroundModules = {");
|
|
43
|
+
for (let i = 0; i < relPaths.length; i++) {
|
|
44
|
+
const key = embedGlobKeyFromRelPath(relPaths[i]!);
|
|
45
|
+
lines.push(` ${JSON.stringify(key)}: __dslinter_m${i},`);
|
|
46
|
+
}
|
|
47
|
+
lines.push("};");
|
|
48
|
+
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Vite plugin: virtual playground module map, scanner proxy, react dedupe, fs.allow for scan root.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import dslinter from "dslinter/vite";
|
|
58
|
+
* export default defineConfig({ plugins: [dslinter()] });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export default function dslinter(
|
|
62
|
+
options: DslinterVitePluginOptions = {},
|
|
63
|
+
): Plugin {
|
|
64
|
+
const scanRoot = resolve(
|
|
65
|
+
options.scanRoot ??
|
|
66
|
+
process.env.DSLINT_SCAN_ROOT ??
|
|
67
|
+
process.cwd(),
|
|
68
|
+
);
|
|
69
|
+
const servePort = options.servePort ?? defaultServePort();
|
|
70
|
+
let relPaths: string[] = [];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
name: "dslinter",
|
|
74
|
+
enforce: "pre",
|
|
75
|
+
|
|
76
|
+
config(config, { mode }): UserConfig {
|
|
77
|
+
const proxy =
|
|
78
|
+
mode === "serve"
|
|
79
|
+
? {
|
|
80
|
+
"/dslint-report.json": {
|
|
81
|
+
target: `http://127.0.0.1:${servePort}`,
|
|
82
|
+
changeOrigin: true,
|
|
83
|
+
},
|
|
84
|
+
"/events": {
|
|
85
|
+
target: `http://127.0.0.1:${servePort}`,
|
|
86
|
+
changeOrigin: true,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: undefined;
|
|
90
|
+
|
|
91
|
+
const existingAllow = config.server?.fs?.allow;
|
|
92
|
+
const fsAllow = Array.isArray(existingAllow)
|
|
93
|
+
? [...existingAllow, scanRoot]
|
|
94
|
+
: [scanRoot];
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
resolve: {
|
|
98
|
+
dedupe: ["react", "react-dom"],
|
|
99
|
+
},
|
|
100
|
+
optimizeDeps: {
|
|
101
|
+
exclude: ["dslinter"],
|
|
102
|
+
},
|
|
103
|
+
server: {
|
|
104
|
+
fs: {
|
|
105
|
+
allow: fsAllow,
|
|
106
|
+
},
|
|
107
|
+
proxy,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
configResolved() {
|
|
113
|
+
relPaths = collectScanModuleRelPaths(scanRoot);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
resolveId(id) {
|
|
117
|
+
if (id === VIRTUAL_PLAYGROUND_MODULES_ID) {
|
|
118
|
+
return RESOLVED_VIRTUAL_ID;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
load(id) {
|
|
123
|
+
if (id !== RESOLVED_VIRTUAL_ID) return;
|
|
124
|
+
return generatePlaygroundModulesSource(scanRoot, relPaths);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
configureServer(server) {
|
|
128
|
+
const refresh = () => {
|
|
129
|
+
relPaths = collectScanModuleRelPaths(scanRoot);
|
|
130
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
|
|
131
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
132
|
+
};
|
|
133
|
+
server.watcher.add(scanRoot);
|
|
134
|
+
server.watcher.on("add", refresh);
|
|
135
|
+
server.watcher.on("unlink", refresh);
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { collectScanModuleRelPaths, embedGlobKeyFromRelPath };
|