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
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
|
-
|
|
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`.
|
package/bin/lib/dev-banner.mjs
CHANGED
|
@@ -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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
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).
|
|
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
|
|
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).
|
|
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
|
+
});
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
193
|
+
embedConfig,
|
|
183
194
|
"--mode",
|
|
184
195
|
"serve",
|
|
185
196
|
"--port",
|