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.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -1
  3. package/bin/lib/dev-banner.mjs +54 -5
  4. package/bin/lib/dev-banner.test.mjs +17 -4
  5. package/bin/lib/network-hosts.mjs +81 -0
  6. package/bin/lib/network-hosts.test.mjs +57 -0
  7. package/bin/lib/project-root.mjs +25 -2
  8. package/bin/lib/project-root.test.mjs +24 -0
  9. package/bin/lib/scan-host.test.mjs +20 -0
  10. package/bin/modes/dev.mjs +14 -3
  11. package/dashboard-dist/assets/DashboardLayoutAuto-COU-fvpX.css +1 -0
  12. package/dashboard-dist/assets/DashboardLayoutAuto-NfQasneG.js +1 -0
  13. package/dashboard-dist/assets/{axe-DHHCqGjV.js → axe-0qmg8n-4.js} +1 -1
  14. package/dashboard-dist/assets/index-B0oyZAfn.js +219 -0
  15. package/dashboard-dist/assets/index-BjnQAYrx.css +1 -0
  16. package/dashboard-dist/index.html +2 -2
  17. package/embed/App.tsx +22 -0
  18. package/embed/index.css +3 -0
  19. package/embed/main.tsx +10 -0
  20. package/embed/tokenCatalog.ts +48 -0
  21. package/index.cjs +52 -52
  22. package/index.html +12 -0
  23. package/package.json +10 -8
  24. package/src/components/Section.tsx +1 -1
  25. package/src/components/ui/badge.tsx +1 -1
  26. package/src/dashboard/ComponentUsageDetails.tsx +7 -39
  27. package/src/dashboard/DashboardBody.tsx +2 -7
  28. package/src/dashboard/FindingsList.tsx +64 -56
  29. package/src/dashboard/ScannedTokenWall.tsx +40 -25
  30. package/src/dashboard/SourceLocationLink.tsx +40 -0
  31. package/src/mcp/rule-catalog.json +2 -2
  32. package/src/playground/inferKitJsx.ts +141 -14
  33. package/src/playground/inferKitParams.ts +40 -2
  34. package/src/playground/playgroundJoin.test.ts +35 -1
  35. package/src/playground/playgroundJoin.ts +18 -0
  36. package/src/shell/DashboardLayoutAuto.tsx +30 -5
  37. package/vite/embed-serve.config.ts +59 -0
  38. package/dashboard-dist/assets/DashboardLayoutAuto-BWuyjHPD.js +0 -1
  39. package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +0 -1
  40. package/dashboard-dist/assets/index-Bxk7tA3F.js +0 -219
  41. package/dashboard-dist/assets/index-D0O_5w5V.css +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.1
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.5.0...v0.5.1)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **dashboard:** Add embed support and improve dev server configuration ([80526e4](https://github.com/jrmybtlr/DSLinter/commit/80526e4))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Jeremy Butler <jeremy.butler@laravel.com>
14
+
15
+ ## v0.5.0
16
+
17
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.4.0...v0.5.0)
18
+
19
+ ### 🚀 Enhancements
20
+
21
+ - **dashboard:** Enhance dev banner with network and scanner information ([ef5f1c8](https://github.com/jrmybtlr/DSLinter/commit/ef5f1c8))
22
+
23
+ ### 🩹 Fixes
24
+
25
+ - **dashboard:** Update inline style handling and improve linting for color values ([70a7d1d](https://github.com/jrmybtlr/DSLinter/commit/70a7d1d))
26
+
27
+ ### 💅 Refactors
28
+
29
+ - **dashboard:** Update dev banner and button component styles ([3baa3d1](https://github.com/jrmybtlr/DSLinter/commit/3baa3d1))
30
+
31
+ ### 🏡 Chore
32
+
33
+ - Update package-lock.json and pnpm-lock.yaml, add SECURITY.md, and enhance version checks for native bindings ([9d29b82](https://github.com/jrmybtlr/DSLinter/commit/9d29b82))
34
+
35
+ ### ❤️ Contributors
36
+
37
+ - Jeremy Butler <jeremy.butler@laravel.com>
38
+
3
39
  ## v0.4.0
4
40
 
5
41
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.3.0...v0.4.0)
package/README.md CHANGED
@@ -77,7 +77,7 @@ Previews load your components with your Vite `@/` aliases and Inertia stubs (`us
77
77
 
78
78
  The embed dev server registers Tailwind `@source` paths for your `.dslinter.json` **`include_dirs`** (for example `resources/js/components`), so component utility classes like `px-3.5` are generated in preview CSS. For 100% parity with your app's full CSS pipeline (theme entry, `@custom-variant`, etc.), use `DSLINTER_USE_CONSUMER_VITE=1` instead.
79
79
 
80
- The prebuilt **`dashboard-dist`** bundle shipped on npm does not run this Vite transform; use embed dev mode (monorepo / git checkout) or consumer Vite for accurate preview styling.
80
+ `npx dslinter` on **npm installs** starts the embed dev server on port **5175** (using `vite/embed-serve.config.ts` and your project's Vite install). Open the **Dashboard** URL from the terminal banner — not the scanner-only port (`7878`). The prebuilt **`dashboard-dist`** fallback serves governance UI only; live previews need the embed dev server.
81
81
 
82
82
  For apps that already embed the dashboard (like this repo's `demo/`), dev mode uses your app's Vite server when `src/App.tsx` imports `DashboardLayout` from `dslinter`.
83
83
 
@@ -108,6 +108,14 @@ Open the **Dashboard** URL from the terminal banner (default port `5175`). The s
108
108
 
109
109
  No Inertia route, no `buildRegistry.ts`, and no `vite.config` edits are required for dev previews.
110
110
 
111
+ **Troubleshooting previews**
112
+
113
+ - Open the **Dashboard** URL from the `npx dslinter` banner (default `http://127.0.0.1:5175/`), not the scanner API port alone.
114
+ - Run from the **project root** (`npx dslinter .`) so `playgrounds[].rel_path` matches your repo (for example `resources/js/components/app-logo.tsx`).
115
+ - Ensure `.dslinter.json` **`include_dirs`** includes your components folder (for example `resources/js/components`).
116
+ - If you see `@dslinter-scan/…` or glob join errors, upgrade `dslinter` and confirm the embed dev server started (not the prebuilt `dashboard-dist` fallback).
117
+ - Use `<DashboardLayout autoPlayground />` — not `buildRegistry.ts` — unless you run through a Vite dev server that compiles your `import.meta.glob`.
118
+
111
119
  **Optional — embed dashboard in your app:** set `DSLINTER_USE_CONSUMER_VITE=1`, add `plugins: [dslinter()]` from `dslinter/vite`, and render `<DashboardLayout autoPlayground dslinterReport={...} />`.
112
120
 
113
121
  **Optional — custom playground controls:** `npx dslinter init --laravel` scaffolds `resources/js/playground/buildRegistry.ts`.
@@ -1,5 +1,14 @@
1
1
  import { homedir } from "node:os";
2
2
  import { resolve } from "node:path";
3
+ import {
4
+ dashboardSharesScannerPort,
5
+ formatMcpAgentHint,
6
+ formatMcpDataStatus,
7
+ getLanIpv4Addresses,
8
+ hasMcpConfig,
9
+ httpUrl,
10
+ scannerApiUrl,
11
+ } from "./network-hosts.mjs";
3
12
 
4
13
  const BOX = {
5
14
  tl: "╭",
@@ -163,6 +172,8 @@ function boxLines(lines, totalWidth) {
163
172
  * dashboardUrl?: string | null;
164
173
  * bundledUrl?: string | null;
165
174
  * pollMs?: number;
175
+ * projectRoot?: string;
176
+ * mcpConfigured?: boolean;
166
177
  * }} opts
167
178
  * @returns {string}
168
179
  */
@@ -177,6 +188,19 @@ export function formatDevBanner(opts) {
177
188
  const scannerWarnPlain = opts.apiAvailable
178
189
  ? null
179
190
  : `unavailable — port ${opts.apiPort} in use`;
191
+ const scannerUrl = scannerApiUrl(opts.apiPort);
192
+ const showScannerApi =
193
+ opts.apiAvailable &&
194
+ dashboardUrl != null &&
195
+ !dashboardSharesScannerPort(dashboardUrl, opts.apiPort);
196
+ const showMcpData = !showScannerApi;
197
+ const mcpDataPlain = formatMcpDataStatus(opts.apiPort, opts.apiAvailable);
198
+ const mcpConfigured =
199
+ opts.mcpConfigured ??
200
+ (opts.projectRoot ? hasMcpConfig(opts.projectRoot) : false);
201
+ const mcpHintPlain = formatMcpAgentHint(mcpConfigured);
202
+ const lanHost = opts.apiAvailable ? getLanIpv4Addresses()[0] : undefined;
203
+ const networkUrl = lanHost ? httpUrl(opts.apiPort, lanHost) : null;
180
204
 
181
205
  /** @type {number[]} */
182
206
  const plainWidths = [
@@ -184,12 +208,20 @@ export function formatDevBanner(opts) {
184
208
  14 + 2 + scanPlain.length,
185
209
  ];
186
210
  if (dashboardUrl) plainWidths.push(14 + 2 + dashboardUrl.length);
211
+ if (showScannerApi) plainWidths.push(14 + 2 + scannerUrl.length);
212
+ if (networkUrl) plainWidths.push(14 + 2 + networkUrl.length);
213
+ if (showMcpData) plainWidths.push(14 + 2 + mcpDataPlain.length);
187
214
  if (scannerWarnPlain) plainWidths.push(14 + 2 + scannerWarnPlain.length);
188
215
  if (opts.pollMs) plainWidths.push(14 + 2 + `polling every ${opts.pollMs} ms`.length);
189
- const footerPlain = dashboardUrl
190
- ? " Open the Dashboard in your browser. Ctrl+C to stop."
191
- : " Ctrl+C to stop.";
192
- plainWidths.push(visibleLength(footerPlain));
216
+ const footerLines = [
217
+ dashboardUrl
218
+ ? " Open the Dashboard in your browser. Ctrl+C to stop."
219
+ : " Ctrl+C to stop.",
220
+ ` ${mcpHintPlain}`,
221
+ ];
222
+ for (const line of footerLines) {
223
+ plainWidths.push(visibleLength(line));
224
+ }
193
225
 
194
226
  const contentWidth = Math.min(maxBox - 4, Math.max(...plainWidths, 40));
195
227
  const totalWidth = contentWidth + 4;
@@ -213,13 +245,30 @@ export function formatDevBanner(opts) {
213
245
  ...row(color.label("Dashboard"), dashboardUrl, contentWidth, color.url),
214
246
  );
215
247
  }
248
+ if (showScannerApi) {
249
+ styledRows.push(
250
+ ...row(color.label("Scanner API"), scannerUrl, contentWidth, color.url),
251
+ );
252
+ }
253
+ if (networkUrl) {
254
+ styledRows.push(
255
+ ...row(color.label("Network"), networkUrl, contentWidth, color.url),
256
+ );
257
+ }
258
+ if (showMcpData) {
259
+ styledRows.push(
260
+ ...row(color.label("MCP data"), mcpDataPlain, contentWidth, color.value),
261
+ );
262
+ }
216
263
  if (scannerWarnPlain) {
217
264
  styledRows.push(
218
265
  ...row(color.label("Scanner"), scannerWarnPlain, contentWidth, color.err),
219
266
  );
220
267
  }
221
268
  styledRows.push("");
222
- styledRows.push(color.dim(footerPlain));
269
+ for (const line of footerLines) {
270
+ styledRows.push(color.dim(line));
271
+ }
223
272
 
224
273
  return boxLines(styledRows, totalWidth).join("\n");
225
274
  }
@@ -22,7 +22,7 @@ describe("shortenPath", () => {
22
22
  });
23
23
 
24
24
  describe("formatDevBanner", () => {
25
- it("includes logo, scan path, dashboard URL, and watch info", () => {
25
+ it("includes logo, scan path, dashboard URL, scanner API, and agent hint", () => {
26
26
  const text = formatDevBanner({
27
27
  scanPath: "/tmp/components",
28
28
  reportPath: "/tmp/components/public/dslinter-report.json",
@@ -30,6 +30,7 @@ describe("formatDevBanner", () => {
30
30
  apiAvailable: true,
31
31
  dashboardUrl: "http://localhost:5173/",
32
32
  pollMs: 150,
33
+ mcpConfigured: false,
33
34
  });
34
35
  expect(text).toContain(LOGO[0]);
35
36
  expect(text).toContain(LOGO[1]);
@@ -37,14 +38,18 @@ describe("formatDevBanner", () => {
37
38
  expect(text).not.toContain("Report file");
38
39
  expect(text).toContain("Dashboard");
39
40
  expect(text).toContain("http://localhost:5173/");
40
- expect(text).not.toContain("Scanner API");
41
+ expect(text).toContain("Scanner API");
42
+ expect(text).toContain("http://127.0.0.1:7878/");
43
+ expect(text).not.toContain("MCP data");
44
+ expect(text).not.toContain("npx dslinter mcp");
45
+ expect(text).toContain("add dslinter to .cursor/mcp.json");
41
46
  expect(text).not.toContain("dslinter-report.json");
42
47
  expect(text).not.toContain("/events");
43
48
  expect(text).toContain("polling every 150 ms");
44
49
  expect(text).toContain("Open the Dashboard in your browser");
45
50
  });
46
51
 
47
- it("shows bundled URL as the dashboard when no separate dev server", () => {
52
+ it("shows MCP data row when dashboard shares scanner port", () => {
48
53
  const text = formatDevBanner({
49
54
  scanPath: "/tmp/components",
50
55
  reportPath: "/tmp/components/public/dslinter-report.json",
@@ -52,11 +57,16 @@ describe("formatDevBanner", () => {
52
57
  apiAvailable: true,
53
58
  bundledUrl: "http://127.0.0.1:7878/",
54
59
  pollMs: 150,
60
+ mcpConfigured: true,
55
61
  });
56
62
  expect(text).toContain("Dashboard");
57
63
  expect(text).toContain("http://127.0.0.1:7878/");
64
+ expect(text).toContain("MCP data");
65
+ expect(text).toContain("live @ http://127.0.0.1:7878");
66
+ expect(text).toContain("Cursor spawns MCP");
58
67
  expect(text).not.toContain("Bundled UI");
59
68
  expect(text).not.toContain("Scanner API");
69
+ expect(text).not.toContain("npx dslinter mcp");
60
70
  });
61
71
 
62
72
  it("marks scanner unavailable when port is busy", () => {
@@ -66,10 +76,12 @@ describe("formatDevBanner", () => {
66
76
  apiPort: 7878,
67
77
  apiAvailable: false,
68
78
  dashboardUrl: "http://localhost:5174/",
79
+ mcpConfigured: false,
69
80
  });
70
81
  expect(text).toContain("Scanner");
71
82
  expect(text).toContain("unavailable");
72
- expect(text).not.toContain("7878/");
83
+ expect(text).toContain("report file");
84
+ expect(text).toContain("add dslinter to .cursor/mcp.json");
73
85
  expect(text).not.toContain("/events");
74
86
  });
75
87
 
@@ -82,6 +94,7 @@ describe("formatDevBanner", () => {
82
94
  dashboardUrl: "http://localhost:5175/",
83
95
  bundledUrl: "http://127.0.0.1:7878/",
84
96
  pollMs: 150,
97
+ mcpConfigured: false,
85
98
  });
86
99
  const rows = text.split("\n").filter((l) => l.startsWith("│"));
87
100
  const widths = rows.map((l) => visibleLength(l));
@@ -0,0 +1,81 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { networkInterfaces } from "node:os";
4
+
5
+ /** @returns {string[]} Non-loopback IPv4 addresses (LAN). */
6
+ export function getLanIpv4Addresses() {
7
+ /** @type {string[]} */
8
+ const addrs = [];
9
+ const ifaces = networkInterfaces();
10
+ for (const entries of Object.values(ifaces)) {
11
+ if (!entries) continue;
12
+ for (const entry of entries) {
13
+ const family = entry.family;
14
+ const isIpv4 =
15
+ family === "IPv4" || family === 4 || String(family) === "IPv4";
16
+ if (isIpv4 && !entry.internal) {
17
+ addrs.push(entry.address);
18
+ }
19
+ }
20
+ }
21
+ return addrs;
22
+ }
23
+
24
+ /**
25
+ * @param {number} port
26
+ */
27
+ export function scannerApiUrl(port) {
28
+ return `http://127.0.0.1:${port}/`;
29
+ }
30
+
31
+ /**
32
+ * Status line for the MCP data source (not a shell command).
33
+ * @param {number} port
34
+ * @param {boolean} apiAvailable
35
+ */
36
+ export function formatMcpDataStatus(port, apiAvailable) {
37
+ if (!apiAvailable) {
38
+ return "offline — agents use report file";
39
+ }
40
+ return `live @ http://127.0.0.1:${port}`;
41
+ }
42
+
43
+ /**
44
+ * @param {boolean} [mcpConfigured]
45
+ */
46
+ export function formatMcpAgentHint(mcpConfigured = false) {
47
+ if (mcpConfigured) {
48
+ return "AI agents: dslinter in .cursor/mcp.json (Cursor spawns MCP)";
49
+ }
50
+ return "AI agents: add dslinter to .cursor/mcp.json";
51
+ }
52
+
53
+ /**
54
+ * @param {string} projectRoot
55
+ */
56
+ export function hasMcpConfig(projectRoot) {
57
+ try {
58
+ const raw = readFileSync(join(projectRoot, ".cursor", "mcp.json"), "utf8");
59
+ const parsed = JSON.parse(raw);
60
+ return Boolean(parsed?.mcpServers?.dslinter);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param {string | null | undefined} dashboardUrl
68
+ * @param {number} apiPort
69
+ */
70
+ export function dashboardSharesScannerPort(dashboardUrl, apiPort) {
71
+ if (!dashboardUrl) return false;
72
+ return dashboardUrl.includes(`:${apiPort}`);
73
+ }
74
+
75
+ /**
76
+ * @param {number} port
77
+ * @param {string} [host]
78
+ */
79
+ export function httpUrl(port, host = "127.0.0.1") {
80
+ return `http://${host}:${port}/`;
81
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ dashboardSharesScannerPort,
4
+ formatMcpAgentHint,
5
+ formatMcpDataStatus,
6
+ getLanIpv4Addresses,
7
+ httpUrl,
8
+ scannerApiUrl,
9
+ } from "./network-hosts.mjs";
10
+
11
+ describe("scannerApiUrl", () => {
12
+ it("formats loopback scanner URL", () => {
13
+ expect(scannerApiUrl(7878)).toBe("http://127.0.0.1:7878/");
14
+ });
15
+ });
16
+
17
+ describe("formatMcpDataStatus", () => {
18
+ it("shows live data URL when scanner is up", () => {
19
+ expect(formatMcpDataStatus(7878, true)).toBe(
20
+ "live @ http://127.0.0.1:7878",
21
+ );
22
+ });
23
+
24
+ it("notes offline scanner", () => {
25
+ expect(formatMcpDataStatus(7878, false)).toContain("report file");
26
+ });
27
+ });
28
+
29
+ describe("formatMcpAgentHint", () => {
30
+ it("prompts setup when not configured", () => {
31
+ expect(formatMcpAgentHint(false)).toContain("add dslinter");
32
+ expect(formatMcpAgentHint(false)).toContain(".cursor/mcp.json");
33
+ });
34
+
35
+ it("notes Cursor spawns MCP when configured", () => {
36
+ expect(formatMcpAgentHint(true)).toContain("Cursor spawns MCP");
37
+ });
38
+ });
39
+
40
+ describe("dashboardSharesScannerPort", () => {
41
+ it("detects bundled dashboard on scanner port", () => {
42
+ expect(dashboardSharesScannerPort("http://127.0.0.1:7878/", 7878)).toBe(true);
43
+ expect(dashboardSharesScannerPort("http://localhost:5173/", 7878)).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe("getLanIpv4Addresses", () => {
48
+ it("returns an array", () => {
49
+ expect(Array.isArray(getLanIpv4Addresses())).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe("httpUrl", () => {
54
+ it("builds URL with custom host", () => {
55
+ expect(httpUrl(7878, "192.168.1.10")).toBe("http://192.168.1.10:7878/");
56
+ });
57
+ });
@@ -32,7 +32,27 @@ function maxMtimeInDir(dir, latest = 0) {
32
32
 
33
33
  /**
34
34
  * @param {string} [root]
35
- * @returns {boolean} true when embed SPA sources exist (monorepo / git checkout).
35
+ * @returns {boolean} true when the embed SPA dev server can start (npm or monorepo).
36
+ */
37
+ export function canRunEmbedVite(root = packageRoot) {
38
+ return (
39
+ existsSync(join(root, "index.html")) ||
40
+ existsSync(join(root, "embed", "main.tsx"))
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @param {string} [root]
46
+ * @returns {string | null} absolute path to published embed Vite config
47
+ */
48
+ export function embedServeConfigPath(root = packageRoot) {
49
+ const configPath = join(root, "vite", "embed-serve.config.ts");
50
+ return existsSync(configPath) ? configPath : null;
51
+ }
52
+
53
+ /**
54
+ * @param {string} [root]
55
+ * @returns {boolean} true when embed SPA sources exist for building dashboard-dist.
36
56
  */
37
57
  export function hasEmbedDashboard(root = packageRoot) {
38
58
  return existsSync(join(root, "index.html"));
@@ -45,7 +65,10 @@ export function hasEmbedDashboard(root = packageRoot) {
45
65
  */
46
66
  export function ensureDashboardBuilt(root = packageRoot) {
47
67
  const distDir = join(root, "dashboard-dist");
48
- if (!hasEmbedDashboard(root)) {
68
+ const canBuildFromSource =
69
+ existsSync(join(root, "index.html")) &&
70
+ existsSync(join(root, "vite.config.ts"));
71
+ if (!canBuildFromSource) {
49
72
  return dashboardDirIfReady(distDir);
50
73
  }
51
74
 
@@ -5,6 +5,8 @@ import { describe, expect, it } from "vitest";
5
5
  import {
6
6
  defaultReportPath,
7
7
  ensureDashboardBuilt,
8
+ canRunEmbedVite,
9
+ embedServeConfigPath,
8
10
  hasEmbedDashboard,
9
11
  } from "./project-root.mjs";
10
12
 
@@ -17,6 +19,28 @@ describe("ensureDashboardBuilt (published install layout)", () => {
17
19
  writeFileSync(join(root, "src", "index.ts"), "export {};\n");
18
20
 
19
21
  expect(hasEmbedDashboard(root)).toBe(false);
22
+ expect(canRunEmbedVite(root)).toBe(false);
23
+
24
+ const dist = ensureDashboardBuilt(root);
25
+ expect(dist).toBeTruthy();
26
+ expect(dist).toContain("dashboard-dist");
27
+ });
28
+
29
+ it("canRunEmbedVite when published embed sources exist without root vite.config.ts", () => {
30
+ const root = mkdtempSync(join(tmpdir(), "dslinter-published-embed-"));
31
+ mkdirSync(join(root, "dashboard-dist"), { recursive: true });
32
+ writeFileSync(join(root, "dashboard-dist", "index.html"), "<!doctype html>");
33
+ mkdirSync(join(root, "embed"), { recursive: true });
34
+ writeFileSync(join(root, "embed", "main.tsx"), "export {};\n");
35
+ mkdirSync(join(root, "vite"), { recursive: true });
36
+ writeFileSync(
37
+ join(root, "vite", "embed-serve.config.ts"),
38
+ "export default {};\n",
39
+ );
40
+
41
+ expect(hasEmbedDashboard(root)).toBe(false);
42
+ expect(canRunEmbedVite(root)).toBe(true);
43
+ expect(embedServeConfigPath(root)).toBe(join(root, "vite", "embed-serve.config.ts"));
20
44
 
21
45
  const dist = ensureDashboardBuilt(root);
22
46
  expect(dist).toBeTruthy();
@@ -6,6 +6,7 @@ import {
6
6
  scanProjectHostsDashboard,
7
7
  shouldUseConsumerViteDev,
8
8
  } from "./scan-host.mjs";
9
+ import { canRunEmbedVite, embedServeConfigPath } from "./project-root.mjs";
9
10
 
10
11
  describe("scanProjectHostsDashboard", () => {
11
12
  it("detects DashboardLayout in src/App.tsx", () => {
@@ -38,4 +39,23 @@ describe("shouldUseConsumerViteDev", () => {
38
39
  expect(shouldUseConsumerViteDev(root)).toBe(false);
39
40
  process.env.DSLINTER_USE_CONSUMER_VITE = prev;
40
41
  });
42
+
43
+ it("uses embed vite on published npm layout when embed sources ship", () => {
44
+ const laravelRoot = mkdtempSync(join(tmpdir(), "dslinter-laravel-npm-"));
45
+ mkdirSync(join(laravelRoot, "resources", "js"), { recursive: true });
46
+ writeFileSync(join(laravelRoot, "vite.config.js"), "export default {};\n");
47
+
48
+ const dslinterPkg = mkdtempSync(join(tmpdir(), "dslinter-pkg-"));
49
+ mkdirSync(join(dslinterPkg, "embed"), { recursive: true });
50
+ writeFileSync(join(dslinterPkg, "embed", "main.tsx"), "export {};\n");
51
+ mkdirSync(join(dslinterPkg, "vite"), { recursive: true });
52
+ writeFileSync(
53
+ join(dslinterPkg, "vite", "embed-serve.config.ts"),
54
+ "export default {};\n",
55
+ );
56
+
57
+ expect(shouldUseConsumerViteDev(laravelRoot)).toBe(false);
58
+ expect(canRunEmbedVite(dslinterPkg)).toBe(true);
59
+ expect(embedServeConfigPath(dslinterPkg)).not.toBeNull();
60
+ });
41
61
  });
package/bin/modes/dev.mjs CHANGED
@@ -5,7 +5,8 @@ import {
5
5
  defaultServePort,
6
6
  findViteRoot,
7
7
  getDashboardPackageRoot,
8
- hasEmbedDashboard,
8
+ canRunEmbedVite,
9
+ embedServeConfigPath,
9
10
  resolveBundledDashboardDir,
10
11
  resolveViteBin,
11
12
  } from "../lib/project-root.mjs";
@@ -41,6 +42,7 @@ async function resolveUiPort(preferred) {
41
42
  * apiAvailable: boolean;
42
43
  * dashboardUrl?: string | null;
43
44
  * bundledUrl?: string | null;
45
+ * projectRoot: string;
44
46
  * }} banner
45
47
  */
46
48
  function printDevBanner(banner) {
@@ -79,13 +81,18 @@ export async function runDevMode({
79
81
 
80
82
  const consumerViteRoot = findViteRoot(scanAbs);
81
83
  const embedRoot = getDashboardPackageRoot();
82
- const embedViteBin = hasEmbedDashboard() ? resolveViteBin(embedRoot) : null;
84
+ const embedConfig = embedServeConfigPath(embedRoot);
85
+ const embedViteBin = canRunEmbedVite(embedRoot)
86
+ ? resolveViteBin(embedRoot) ??
87
+ (consumerViteRoot ? resolveViteBin(consumerViteRoot) : null)
88
+ : null;
83
89
 
84
90
  const useConsumerViteDev =
85
91
  consumerViteRoot != null && shouldUseConsumerViteDev(scanAbs);
86
92
 
87
93
  const useEmbedViteDev =
88
94
  embedViteBin != null &&
95
+ embedConfig != null &&
89
96
  !useConsumerViteDev &&
90
97
  process.env.DSLINTER_NO_EMBED_VITE?.trim() !== "1";
91
98
 
@@ -111,6 +118,9 @@ export async function runDevMode({
111
118
 
112
119
  if (attachBundledStatic) {
113
120
  args.push("--dashboard-static", bundledDist);
121
+ process.stderr.write(
122
+ "dslinter: warning: using prebuilt dashboard — live component previews are unavailable. Upgrade dslinter or ensure the embed dev server starts (Dashboard URL on port 5175).\n",
123
+ );
114
124
  }
115
125
 
116
126
  const apiAvailable = !(await warnIfPortBusy(port, { silent: true }));
@@ -164,6 +174,7 @@ export async function runDevMode({
164
174
  apiPort: port,
165
175
  apiAvailable,
166
176
  bundledUrl,
177
+ projectRoot: projectAbs,
167
178
  };
168
179
 
169
180
  const consumerViteRootForEnv = consumerViteRoot ?? findViteRoot(scanAbs);
@@ -179,7 +190,7 @@ export async function runDevMode({
179
190
  [
180
191
  embedViteBin,
181
192
  "--config",
182
- join(embedRoot, "vite.config.ts"),
193
+ embedConfig,
183
194
  "--mode",
184
195
  "serve",
185
196
  "--port",