dslinter 0.1.4 → 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.
@@ -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,
@@ -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,14 +1,4 @@
1
- // Add to vite.config.ts when using @dslint-scan embed-style glob in App:
2
- //
3
- // import path from "node:path";
4
- // const scanRoot = path.resolve(process.env.DSLINT_SCAN_ROOT ?? ".");
5
- //
6
- // resolve: { alias: { "@dslint-scan": scanRoot } },
7
- // server: { fs: { allow: [scanRoot] } },
8
- //
9
- // App — glob every .tsx/.jsx under @dslint-scan (use ** recursive glob in import.meta.glob).
10
- // buildPlaygroundEntriesFromReport(report, modules);
11
- //
12
- // Prefer: npx dslinter init --laravel (relative glob) for Inertia resources/js layouts.
1
+ // Deprecated use `import dslinter from "dslinter/vite"` instead.
2
+ // The plugin generates virtual playground modules; no @dslint-scan alias required.
13
3
 
14
4
  export {};
@@ -1,34 +1,26 @@
1
1
  /**
2
- * Merge into your existing `vite.config.ts` when using `npx dslinter` dev mode.
3
- *
4
- * Published dslinter source uses relative imports only — your app's `@/*` alias
5
- * (e.g. Laravel `@/*` → `resources/js/*`) does not need remapping for package UI.
6
- *
7
- * Adjust proxy paths if your report is not served from the Vite dev server root.
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.
8
4
  */
9
5
  const DSLINT_SERVE_PORT = Number(process.env.DSLINT_SERVE_PORT ?? "7878");
10
6
 
11
- // Inside defineConfig(({ mode }) => ({ ... })):
12
7
  export const dslinterViteSnippet = {
13
8
  resolve: {
14
9
  dedupe: ["react", "react-dom"],
15
10
  },
16
11
  optimizeDeps: {
17
- /** Source-first package: transpile from node_modules instead of pre-bundling. */
18
12
  exclude: ["dslinter"],
19
13
  },
20
14
  server: {
21
- proxy:
22
- // mode === "serve" when started via `npx dslinter` (not plain `vite`)
23
- {
24
- "/dslint-report.json": {
25
- target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
26
- changeOrigin: true,
27
- },
28
- "/events": {
29
- target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
30
- changeOrigin: true,
31
- },
15
+ proxy: {
16
+ "/dslint-report.json": {
17
+ target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
18
+ changeOrigin: true,
32
19
  },
20
+ "/events": {
21
+ target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
22
+ changeOrigin: true,
23
+ },
24
+ },
33
25
  },
34
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 };