dslinter 0.0.31 → 0.0.33

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.33
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.32...v0.0.33)
6
+
7
+ ### 🏡 Chore
8
+
9
+ - **release:** V0.0.32 ([bb07996](https://github.com/jrmybtlr/DSLinter/commit/bb07996))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Jeremy Butler <jeremy.butler@laravel.com>
14
+
15
+ ## v0.0.32
16
+
17
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.31...v0.0.32)
18
+
3
19
  ## v0.0.31
4
20
 
5
21
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.30...v0.0.31)
@@ -0,0 +1,57 @@
1
+ import net from "node:net";
2
+ import { spawnSync } from "node:child_process";
3
+
4
+ /**
5
+ * @param {number} port
6
+ * @param {string} [host]
7
+ */
8
+ export function isPortInUse(port, host = "127.0.0.1") {
9
+ return new Promise((resolve) => {
10
+ const server = net.createServer();
11
+ server.once("error", (err) => {
12
+ resolve(err && "code" in err && err.code === "EADDRINUSE");
13
+ });
14
+ server.once("listening", () => {
15
+ server.close(() => resolve(false));
16
+ });
17
+ server.listen(port, host);
18
+ });
19
+ }
20
+
21
+ /**
22
+ * @param {number} port
23
+ */
24
+ export function describePortOccupant(port) {
25
+ if (process.platform === "win32") {
26
+ const r = spawnSync("netstat", ["-ano"], { encoding: "utf8" });
27
+ const line = (r.stdout ?? "")
28
+ .split("\n")
29
+ .find((l) => l.includes(`:${port}`) && l.includes("LISTENING"));
30
+ return line?.trim() ?? null;
31
+ }
32
+ const r = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
33
+ encoding: "utf8",
34
+ });
35
+ const lines = (r.stdout ?? "").trim().split("\n").filter(Boolean);
36
+ if (lines.length < 2) return null;
37
+ return lines.slice(1).join("\n");
38
+ }
39
+
40
+ /**
41
+ * @param {number} port
42
+ */
43
+ export async function warnIfPortBusy(port) {
44
+ if (!(await isPortInUse(port))) return false;
45
+ const detail = describePortOccupant(port);
46
+ process.stderr.write(
47
+ [
48
+ "",
49
+ `[dslinter] Port ${port} is already in use — scanner cannot bind.`,
50
+ detail ? ` ${detail.replace(/\n/g, "\n ")}` : "",
51
+ ` Stop the old process, then restart. Example: lsof -nP -iTCP:${port} -sTCP:LISTEN`,
52
+ " If you already have `npm run dev` running, open the Vite URL it printed (e.g. http://localhost:5173/).",
53
+ "",
54
+ ].join("\n"),
55
+ );
56
+ return true;
57
+ }
@@ -1,10 +1,77 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import { createRequire } from "node:module";
2
- import { existsSync, realpathSync } from "node:fs";
3
+ import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
3
4
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
 
6
7
  const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
7
8
 
9
+ /** @returns {string} `packages/dashboard` root (embed SPA + library). */
10
+ export function getDashboardPackageRoot() {
11
+ return packageRoot;
12
+ }
13
+
14
+ /**
15
+ * @param {string} dir
16
+ * @param {number} [latest]
17
+ */
18
+ function maxMtimeInDir(dir, latest = 0) {
19
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
20
+ const p = join(dir, ent.name);
21
+ if (ent.isDirectory()) {
22
+ latest = maxMtimeInDir(p, latest);
23
+ } else if (ent.isFile()) {
24
+ latest = Math.max(latest, statSync(p).mtimeMs);
25
+ }
26
+ }
27
+ return latest;
28
+ }
29
+
30
+ /**
31
+ * Rebuild `dashboard-dist/` when embed sources are newer than the bundle (or dist is missing).
32
+ * @param {string} root
33
+ */
34
+ export function ensureDashboardBuilt(root = packageRoot) {
35
+ const distIndex = join(root, "dashboard-dist", "index.html");
36
+ const force =
37
+ process.env.DSLINTER_REBUILD_DASHBOARD === "1" ||
38
+ process.env.DSLINTER_REBUILD_DASHBOARD?.toLowerCase() === "true";
39
+
40
+ let needsBuild = force || !existsSync(distIndex);
41
+ if (!needsBuild) {
42
+ const distMtime = statSync(distIndex).mtimeMs;
43
+ for (const sub of ["src", "embed"]) {
44
+ const dir = join(root, sub);
45
+ if (existsSync(dir) && maxMtimeInDir(dir) > distMtime) {
46
+ needsBuild = true;
47
+ break;
48
+ }
49
+ }
50
+ const configPath = join(root, "vite.config.ts");
51
+ if (existsSync(configPath) && statSync(configPath).mtimeMs > distMtime) {
52
+ needsBuild = true;
53
+ }
54
+ }
55
+
56
+ if (!needsBuild) return dashboardDirIfReady(join(root, "dashboard-dist"));
57
+
58
+ process.stderr.write("[dslinter] Building dashboard bundle (dashboard-dist)…\n");
59
+ const result = spawnSync("npm", ["run", "build:dashboard"], {
60
+ cwd: root,
61
+ stdio: "inherit",
62
+ env: process.env,
63
+ });
64
+ if (result.status !== 0) {
65
+ throw new Error("dslinter: dashboard build failed");
66
+ }
67
+ return dashboardDirIfReady(join(root, "dashboard-dist"));
68
+ }
69
+
70
+ /** @returns {boolean} */
71
+ export function hasEmbedDashboard() {
72
+ return existsSync(join(packageRoot, "index.html"));
73
+ }
74
+
8
75
  const VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"];
9
76
 
10
77
  /**
@@ -79,7 +146,11 @@ export function resolveBundledDashboardDir() {
79
146
  return dashboardDirIfReady(dir);
80
147
  }
81
148
 
82
- return dashboardDirIfReady(join(packageRoot, "dashboard-dist"));
149
+ try {
150
+ return ensureDashboardBuilt(packageRoot);
151
+ } catch {
152
+ return dashboardDirIfReady(join(packageRoot, "dashboard-dist"));
153
+ }
83
154
  }
84
155
 
85
156
  /**
package/bin/modes/dev.mjs CHANGED
@@ -1,11 +1,15 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { join } from "node:path";
2
3
  import {
3
4
  defaultReportPath,
4
5
  defaultServePort,
5
6
  findViteRoot,
7
+ getDashboardPackageRoot,
8
+ hasEmbedDashboard,
6
9
  resolveBundledDashboardDir,
7
10
  resolveViteBin,
8
11
  } from "../lib/project-root.mjs";
12
+ import { warnIfPortBusy } from "../lib/port-check.mjs";
9
13
  import { spawnScanner } from "../lib/run-scanner.mjs";
10
14
  import { waitForPort } from "../lib/wait-for-port.mjs";
11
15
 
@@ -20,8 +24,16 @@ import { waitForPort } from "../lib/wait-for-port.mjs";
20
24
  export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort }) {
21
25
  const port = servePort ?? defaultServePort();
22
26
  const reportPath = defaultReportPath(scanPath, outputPath);
23
- const viteRoot = findViteRoot(process.cwd());
24
- const bundledDist = resolveBundledDashboardDir();
27
+ const consumerViteRoot = findViteRoot(process.cwd());
28
+ const embedRoot = getDashboardPackageRoot();
29
+ const embedViteBin = hasEmbedDashboard() ? resolveViteBin(embedRoot) : null;
30
+ /** Live embed UI from source (proxies report/SSE to the scanner port). */
31
+ const useEmbedViteDev =
32
+ embedViteBin != null &&
33
+ consumerViteRoot == null &&
34
+ process.env.DSLINTER_NO_EMBED_VITE?.trim() !== "1";
35
+
36
+ const bundledDist = useEmbedViteDev ? null : resolveBundledDashboardDir();
25
37
 
26
38
  const args = [...scannerArgs];
27
39
  const hasServe = args.some((a) => a === "--serve" || a.startsWith("--serve="));
@@ -31,10 +43,21 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
31
43
  if (!args.some((a) => a === "--output" || a.startsWith("--output="))) {
32
44
  args.push("--output", reportPath);
33
45
  }
34
- if (bundledDist && !args.some((a) => a === "--dashboard-static" || a.startsWith("--dashboard-static="))) {
46
+
47
+ const hasDashboardStaticFlag = args.some(
48
+ (a) => a === "--dashboard-static" || a.startsWith("--dashboard-static="),
49
+ );
50
+ const attachBundledStatic =
51
+ bundledDist != null && !useEmbedViteDev && !hasDashboardStaticFlag;
52
+
53
+ if (attachBundledStatic) {
35
54
  args.push("--dashboard-static", bundledDist);
36
55
  }
37
56
 
57
+ if (hasServe || args.some((a) => a.startsWith("--serve="))) {
58
+ await warnIfPortBusy(port);
59
+ }
60
+
38
61
  const scanner = await spawnScanner(args);
39
62
  const children = [scanner];
40
63
 
@@ -61,61 +84,108 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
61
84
  process.exit(1);
62
85
  }
63
86
 
64
- if (bundledDist) {
65
- const url = `http://127.0.0.1:${port}/`;
66
- const lines = [
67
- "",
68
- "[dslinter] Bundled dashboard at",
69
- ` ${url}`,
70
- ` Report: http://127.0.0.1:${port}/dslint-report.json`,
71
- ];
72
- if (viteRoot) {
73
- lines.push(
74
- " Vite dev server also starting (use its URL for your app; proxy /dslint-report.json and /events to this port if needed).",
75
- );
76
- }
77
- lines.push("");
78
- process.stderr.write(lines.join("\n"));
79
- if (!viteRoot) {
80
- maybeOpenBrowser(url);
81
- scanner.on("exit", (code) => process.exit(code ?? 0));
82
- return;
83
- }
84
- }
87
+ const apiUrl = `http://127.0.0.1:${port}/dslint-report.json`;
85
88
 
86
- if (!viteRoot) {
89
+ if (useEmbedViteDev) {
90
+ const uiPort = process.env.DSLINTER_DEV_UI_PORT?.trim() || "5175";
91
+ const vite = spawn(
92
+ process.execPath,
93
+ [embedViteBin, "--config", join(embedRoot, "vite.config.ts"), "--mode", "serve", "--port", uiPort],
94
+ {
95
+ cwd: embedRoot,
96
+ stdio: "inherit",
97
+ env: { ...process.env, DSLINT_SERVE_PORT: String(port) },
98
+ },
99
+ );
100
+ children.push(vite);
87
101
  process.stderr.write(
88
102
  [
89
103
  "",
90
- "[dslinter] No vite.config.* and no bundled dashboard-dist — scanner only (watch + serve).",
91
- " Reinstall dslinter or run `pnpm --filter dslinter run build:dashboard` from the repo.",
92
- " Or add a Vite app with proxy for /dslint-report.json and /events.",
93
- ` Scanner: http://127.0.0.1:${port}/dslint-report.json`,
104
+ "[dslinter] Dashboard UI (live source)",
105
+ ` http://127.0.0.1:${uiPort}/`,
106
+ "[dslinter] Scanner API",
107
+ ` ${apiUrl}`,
108
+ ` SSE: http://127.0.0.1:${port}/events`,
94
109
  "",
95
110
  ].join("\n"),
96
111
  );
97
- scanner.on("exit", (code) => process.exit(code ?? 0));
112
+ vite.on("exit", (code, signal) => {
113
+ cleanup("SIGTERM");
114
+ if (signal) process.kill(process.pid, signal);
115
+ else process.exit(code ?? 0);
116
+ });
98
117
  return;
99
118
  }
100
119
 
101
- const viteBin = resolveViteBin(viteRoot);
102
- if (!viteBin) {
103
- process.stderr.write(`dslinter: vite not installed in ${viteRoot}. Run npm install.\n`);
104
- cleanup("SIGTERM");
105
- process.exit(1);
120
+ if (consumerViteRoot) {
121
+ const viteBin = resolveViteBin(consumerViteRoot);
122
+ if (!viteBin) {
123
+ process.stderr.write(`dslinter: vite not installed in ${consumerViteRoot}. Run npm install.\n`);
124
+ cleanup("SIGTERM");
125
+ process.exit(1);
126
+ }
127
+
128
+ const bundledUrl = attachBundledStatic ? `http://127.0.0.1:${port}/` : null;
129
+ process.stderr.write(
130
+ [
131
+ "",
132
+ "[dslinter] Dashboard UI (recommended) — Vite dev server with live dslinter source:",
133
+ " http://localhost:5173/ (or the port Vite prints below if 5173 is taken)",
134
+ bundledUrl
135
+ ? `[dslinter] Bundled dashboard (same build as publish) — only if port ${port} bound successfully:`
136
+ : `[dslinter] Scanner API only on :${port} (port may be busy — use Vite URL above):`,
137
+ bundledUrl ? ` ${bundledUrl}` : null,
138
+ ` ${apiUrl}`,
139
+ ` SSE: http://127.0.0.1:${port}/events`,
140
+ "",
141
+ " Do not use `npx dslint` — that is a different npm package. Use `npm run dev` or `npx dslinter .` in demo/.",
142
+ "",
143
+ ]
144
+ .filter(Boolean)
145
+ .join("\n"),
146
+ );
147
+
148
+ const vite = spawn(process.execPath, [viteBin, "--mode", "serve"], {
149
+ cwd: consumerViteRoot,
150
+ stdio: "inherit",
151
+ env: { ...process.env, DSLINT_SERVE_PORT: String(port) },
152
+ });
153
+ children.push(vite);
154
+
155
+ vite.on("exit", (code, signal) => {
156
+ cleanup("SIGTERM");
157
+ if (signal) process.kill(process.pid, signal);
158
+ else process.exit(code ?? 0);
159
+ });
160
+ return;
106
161
  }
107
162
 
108
- const vite = spawn(process.execPath, [viteBin, "--mode", "serve"], {
109
- cwd: viteRoot,
110
- stdio: "inherit",
111
- });
112
- children.push(vite);
163
+ if (bundledDist) {
164
+ const url = `http://127.0.0.1:${port}/`;
165
+ process.stderr.write(
166
+ [
167
+ "",
168
+ "[dslinter] Bundled dashboard",
169
+ ` ${url}`,
170
+ ` Report: ${apiUrl}`,
171
+ "",
172
+ ].join("\n"),
173
+ );
174
+ maybeOpenBrowser(url);
175
+ scanner.on("exit", (code) => process.exit(code ?? 0));
176
+ return;
177
+ }
113
178
 
114
- vite.on("exit", (code, signal) => {
115
- cleanup("SIGTERM");
116
- if (signal) process.kill(process.pid, signal);
117
- else process.exit(code ?? 0);
118
- });
179
+ process.stderr.write(
180
+ [
181
+ "",
182
+ "[dslinter] Scanner only (no dashboard UI).",
183
+ ` Report: ${apiUrl}`,
184
+ " Run `pnpm --filter dslinter run build:dashboard` or use a Vite project with dslinter.",
185
+ "",
186
+ ].join("\n"),
187
+ );
188
+ scanner.on("exit", (code) => process.exit(code ?? 0));
119
189
  }
120
190
 
121
191
  /**