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.
Files changed (40) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +44 -27
  3. package/bin/modes/build.mjs +6 -0
  4. package/bin/modes/dev.mjs +16 -1
  5. package/bin/modes/init.mjs +10 -14
  6. package/dashboard-dist/assets/DashboardLayoutAuto-BPPtPsYh.css +1 -0
  7. package/dashboard-dist/assets/DashboardLayoutAuto-Dp3bAQxH.js +1 -0
  8. package/dashboard-dist/assets/index-DsjwnDdX.js +206 -0
  9. package/dashboard-dist/assets/index-jaCmZJlW.css +1 -0
  10. package/dashboard-dist/index.html +2 -2
  11. package/index.cjs +52 -52
  12. package/package.json +16 -7
  13. package/src/components/ComponentInspectPane.tsx +9 -13
  14. package/src/components/DashboardCommandPalette.tsx +1 -1
  15. package/src/components/PlaygroundA11yAndCode.tsx +3 -3
  16. package/src/components/PlaygroundControlField.tsx +4 -4
  17. package/src/components/PlaygroundControls.tsx +1 -1
  18. package/src/components/PlaygroundVariantMatrix.tsx +1 -1
  19. package/src/components/Sidebar.tsx +2 -2
  20. package/src/dashboard/ComponentCatalog.tsx +2 -2
  21. package/src/dashboard/ComponentUsageDetails.tsx +11 -5
  22. package/src/dashboard/DashboardBody.tsx +1 -1
  23. package/src/dashboard/FindingsList.tsx +3 -3
  24. package/src/dashboard/ScannedTokenWall.tsx +1 -1
  25. package/src/dashboard/mergeTokenCatalog.ts +1 -1
  26. package/src/index.ts +1 -0
  27. package/src/playground/scanPlaygroundModules.ts +1 -0
  28. package/src/playground/usePlaygroundFromReport.test.ts +37 -0
  29. package/src/playground/usePlaygroundFromReport.ts +23 -0
  30. package/src/playground/virtual-playground-modules.d.ts +6 -0
  31. package/src/shell/DashboardLayout.tsx +38 -5
  32. package/src/shell/DashboardLayoutAuto.tsx +20 -0
  33. package/templates/vite.dslint-scan-alias.snippet.ts +4 -21
  34. package/templates/vite.dslinter.snippet.ts +14 -16
  35. package/vite/collectScanModules.test.ts +42 -0
  36. package/vite/collectScanModules.ts +60 -0
  37. package/vite/consumer.config.mjs +22 -0
  38. package/vite/plugin.ts +140 -0
  39. package/dashboard-dist/assets/index-BhDQfrwA.css +0 -1
  40. 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 "@/components/ui/hover-card";
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 "@/components/ui/table";
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 "@/components/ui/table";
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 "@/components/InlineCode";
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="font-mono text-xs text-foreground">
90
- {shortPath(report.root, loc.path)}
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 "@/components/ui/table";
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 "@/components/ui/table";
9
+ } from "../components/ui/table";
10
10
  import { shortPath } from "./paths";
11
11
  import type { LintFinding, Severity } from "../types/report";
12
- import { Badge } from "@/components/ui/badge";
13
- import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
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
 
@@ -3,7 +3,7 @@ import {
3
3
  HoverCard,
4
4
  HoverCardContent,
5
5
  HoverCardTrigger,
6
- } from "@/components/ui/hover-card";
6
+ } from "../components/ui/hover-card";
7
7
  import { cn } from "../lib/utils";
8
8
  import { shortPath } from "./paths";
9
9
  import {
@@ -1,5 +1,5 @@
1
1
  import type { TokenCatalog } from "../types/tokenCatalog";
2
- import type { CssTokenSummary, WorkspaceReport } from "../types/report";
2
+ import type { WorkspaceReport } from "../types/report";
3
3
 
4
4
  export type TokenUsageFilter = "all" | "used" | "unused";
5
5
 
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
+ }
@@ -0,0 +1,6 @@
1
+ declare module "virtual:dslinter/playground-modules" {
2
+ export const scanPlaygroundModules: Record<
3
+ string,
4
+ Record<string, unknown>
5
+ >;
6
+ }
@@ -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 "@/components/ui/button";
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
- playgroundEntries: PlaygroundEntry[];
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
- function DashboardLayoutInner({
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
- }: DashboardLayoutProps) {
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 {...props} />
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
- * Add to `vite.config.ts` when using the `@dslint-scan` embed-style glob in your App:
3
- *
4
- * import path from "node:path";
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
- * Add to your existing `vite.config.ts` when using `npx dslinter` dev mode.
3
- * Adjust paths if your app layout differs from `src/` + `public/dslint-report.json`.
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
- // mode === "serve" when started via `npx dslinter` (not plain `vite`)
17
- {
18
- "/dslint-report.json": {
19
- target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
20
- changeOrigin: true,
21
- },
22
- "/events": {
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 };