dslinter 0.0.33 → 0.0.36
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 +37 -0
- package/bin/lib/dev-banner.mjs +264 -0
- package/bin/lib/dev-banner.test.mjs +80 -0
- package/bin/lib/port-check.mjs +54 -21
- package/bin/lib/run-scanner.mjs +2 -1
- package/bin/modes/dev.mjs +91 -70
- package/dashboard-dist/assets/index-Pc1to7nD.css +1 -0
- package/dashboard-dist/assets/index-YvDeIoPr.js +205 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +7 -7
- package/src/components/ComponentPlaygroundPane.tsx +2 -2
- package/src/components/TokensPane.tsx +33 -4
- package/src/dashboard/ScannedTokenWall.tsx +242 -0
- package/src/dashboard/mergeTokenCatalog.ts +149 -0
- package/src/index.ts +11 -0
- package/src/report/modulePathMatch.ts +5 -2
- package/src/shell/DashboardLayout.tsx +7 -2
- package/src/types/report.ts +33 -0
- package/dashboard-dist/assets/index-BDok42uY.js +0 -245
- package/dashboard-dist/assets/index-Dgfmp0Yv.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.0.36
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.35...v0.0.36)
|
|
6
|
+
|
|
7
|
+
### 🏡 Chore
|
|
8
|
+
|
|
9
|
+
- Update native binding version checks to 0.0.34 in dashboard package ([917860a](https://github.com/jrmybtlr/DSLinter/commit/917860a))
|
|
10
|
+
|
|
11
|
+
### ❤️ Contributors
|
|
12
|
+
|
|
13
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
14
|
+
|
|
15
|
+
## v0.0.35
|
|
16
|
+
|
|
17
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.34...v0.0.35)
|
|
18
|
+
|
|
19
|
+
### 🏡 Chore
|
|
20
|
+
|
|
21
|
+
- Update dependencies and improve dashboard functionality ([71a42f2](https://github.com/jrmybtlr/DSLinter/commit/71a42f2))
|
|
22
|
+
- Update @napi-rs/cli to version 3.6.2 and refactor ANSI regex in dev-banner ([dd264b2](https://github.com/jrmybtlr/DSLinter/commit/dd264b2))
|
|
23
|
+
|
|
24
|
+
### ❤️ Contributors
|
|
25
|
+
|
|
26
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
27
|
+
|
|
28
|
+
## v0.0.34
|
|
29
|
+
|
|
30
|
+
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.33...v0.0.34)
|
|
31
|
+
|
|
32
|
+
### 🩹 Fixes
|
|
33
|
+
|
|
34
|
+
- Update native binding version checks to 0.0.33 and improve type definitions in ComponentPlaygroundPane ([99ad1e6](https://github.com/jrmybtlr/DSLinter/commit/99ad1e6))
|
|
35
|
+
|
|
36
|
+
### ❤️ Contributors
|
|
37
|
+
|
|
38
|
+
- Jeremy Butler <jeremy.butler@laravel.com>
|
|
39
|
+
|
|
3
40
|
## v0.0.33
|
|
4
41
|
|
|
5
42
|
[compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.32...v0.0.33)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const BOX = {
|
|
5
|
+
tl: "╭",
|
|
6
|
+
tr: "╮",
|
|
7
|
+
bl: "╰",
|
|
8
|
+
br: "╯",
|
|
9
|
+
h: "─",
|
|
10
|
+
v: "│",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Block-letter DSLinter (two lines). */
|
|
14
|
+
export const LOGO = [
|
|
15
|
+
"█▀▄\u2003█▀\u2003█░░\u2003█\u2003█▄░█\u2003▀█▀\u2003█▀▀\u2003█▀█",
|
|
16
|
+
"█▄▀\u2003▄█\u2003█▄▄\u2003█\u2003█░▀█\u2003░█░\u2003██▄\u2003█▀▄",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ESC = "\u001b";
|
|
20
|
+
const ANSI_RE = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
21
|
+
|
|
22
|
+
/** @param {string} s */
|
|
23
|
+
export function stripAnsi(s) {
|
|
24
|
+
return s.replace(ANSI_RE, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {string} s */
|
|
28
|
+
export function visibleLength(s) {
|
|
29
|
+
return stripAnsi(s).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} s
|
|
34
|
+
* @param {number} width
|
|
35
|
+
*/
|
|
36
|
+
function padVisible(s, width) {
|
|
37
|
+
const pad = Math.max(0, width - visibleLength(s));
|
|
38
|
+
return s + " ".repeat(pad);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} text
|
|
43
|
+
* @param {number} max
|
|
44
|
+
*/
|
|
45
|
+
function truncatePlain(text, max) {
|
|
46
|
+
if (text.length <= max) return text;
|
|
47
|
+
if (max <= 1) return text.slice(0, max);
|
|
48
|
+
return `${text.slice(0, max - 1)}…`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {boolean} enabled */
|
|
52
|
+
function createStyles(enabled) {
|
|
53
|
+
if (!enabled) {
|
|
54
|
+
const id = (s) => s;
|
|
55
|
+
return {
|
|
56
|
+
label: id,
|
|
57
|
+
value: id,
|
|
58
|
+
url: id,
|
|
59
|
+
dim: id,
|
|
60
|
+
ok: id,
|
|
61
|
+
warn: id,
|
|
62
|
+
err: id,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const esc = (n, s) => `\u001b[${n}m${s}\u001b[0m`;
|
|
66
|
+
return {
|
|
67
|
+
label: (s) => esc("2", s),
|
|
68
|
+
value: (s) => esc("0", s),
|
|
69
|
+
url: (s) => esc("4;36", s),
|
|
70
|
+
dim: (s) => esc("2", s),
|
|
71
|
+
ok: (s) => esc("32", s),
|
|
72
|
+
warn: (s) => esc("33", s),
|
|
73
|
+
err: (s) => esc("31", s),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} path
|
|
79
|
+
* @param {number} [maxLen]
|
|
80
|
+
*/
|
|
81
|
+
export function shortenPath(path, maxLen = 72) {
|
|
82
|
+
const home = homedir();
|
|
83
|
+
let s = path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
84
|
+
if (s.length <= maxLen) return s;
|
|
85
|
+
const head = Math.ceil((maxLen - 1) / 2);
|
|
86
|
+
const tail = Math.floor((maxLen - 1) / 2);
|
|
87
|
+
return `${s.slice(0, head)}…${s.slice(-tail)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} label
|
|
92
|
+
* @param {string} plainValue
|
|
93
|
+
* @param {number} contentWidth
|
|
94
|
+
* @param {(value: string) => string} [styleValue]
|
|
95
|
+
*/
|
|
96
|
+
function row(label, plainValue, contentWidth, styleValue = (v) => v) {
|
|
97
|
+
const labelCol = 14;
|
|
98
|
+
const gap = 2;
|
|
99
|
+
const valueWidth = Math.max(1, contentWidth - labelCol - gap);
|
|
100
|
+
const valueLines = wrapPlain(plainValue, valueWidth);
|
|
101
|
+
const lines = [];
|
|
102
|
+
for (let i = 0; i < valueLines.length; i++) {
|
|
103
|
+
const lbl = i === 0 ? padVisible(label, labelCol) : " ".repeat(labelCol);
|
|
104
|
+
lines.push(`${lbl}${" ".repeat(gap)}${styleValue(valueLines[i])}`);
|
|
105
|
+
}
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} text
|
|
111
|
+
* @param {number} width
|
|
112
|
+
* @returns {string[]}
|
|
113
|
+
*/
|
|
114
|
+
function wrapPlain(text, width) {
|
|
115
|
+
if (text.length <= width) return [text];
|
|
116
|
+
const words = text.split(/(\s+)/);
|
|
117
|
+
const lines = [];
|
|
118
|
+
let line = "";
|
|
119
|
+
for (const w of words) {
|
|
120
|
+
if (line.length + w.length <= width) {
|
|
121
|
+
line += w;
|
|
122
|
+
} else if (w.trim() === "") {
|
|
123
|
+
line += w;
|
|
124
|
+
} else if (w.length > width) {
|
|
125
|
+
if (line) lines.push(line.trimEnd());
|
|
126
|
+
for (let i = 0; i < w.length; i += width) {
|
|
127
|
+
lines.push(w.slice(i, i + width));
|
|
128
|
+
}
|
|
129
|
+
line = "";
|
|
130
|
+
} else {
|
|
131
|
+
if (line) lines.push(line.trimEnd());
|
|
132
|
+
line = w.trimStart();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (line) lines.push(line.trimEnd());
|
|
136
|
+
return lines.length ? lines : [""];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string[]} lines
|
|
141
|
+
* @param {number} totalWidth
|
|
142
|
+
*/
|
|
143
|
+
function boxLines(lines, totalWidth) {
|
|
144
|
+
const contentWidth = totalWidth - 4;
|
|
145
|
+
const out = [];
|
|
146
|
+
out.push(`${BOX.tl}${BOX.h.repeat(totalWidth - 2)}${BOX.tr}`);
|
|
147
|
+
for (const line of lines) {
|
|
148
|
+
const plain = stripAnsi(line);
|
|
149
|
+
const clipped =
|
|
150
|
+
plain.length > contentWidth ? truncatePlain(plain, contentWidth) : line;
|
|
151
|
+
out.push(`${BOX.v} ${padVisible(clipped, contentWidth)} ${BOX.v}`);
|
|
152
|
+
}
|
|
153
|
+
out.push(`${BOX.bl}${BOX.h.repeat(totalWidth - 2)}${BOX.br}`);
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {{
|
|
159
|
+
* scanPath: string;
|
|
160
|
+
* reportPath: string;
|
|
161
|
+
* apiPort: number;
|
|
162
|
+
* apiAvailable: boolean;
|
|
163
|
+
* dashboardUrl?: string | null;
|
|
164
|
+
* bundledUrl?: string | null;
|
|
165
|
+
* pollMs?: number;
|
|
166
|
+
* }} opts
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function formatDevBanner(opts) {
|
|
170
|
+
const color = createStyles(process.stderr.isTTY === true);
|
|
171
|
+
const terminalCols = process.stderr.columns ?? 80;
|
|
172
|
+
const maxBox = Math.min(Math.max(terminalCols, 64), 96);
|
|
173
|
+
|
|
174
|
+
const scanAbs = resolve(opts.scanPath);
|
|
175
|
+
const reportAbs = resolve(opts.reportPath);
|
|
176
|
+
const apiBase = `http://127.0.0.1:${opts.apiPort}`;
|
|
177
|
+
|
|
178
|
+
const apiStatusPlain = opts.apiAvailable ? "listening" : "unavailable — port in use";
|
|
179
|
+
const bundledStatusPlain = opts.apiAvailable ? "ready" : "port busy";
|
|
180
|
+
const scanPlain = shortenPath(scanAbs, 80);
|
|
181
|
+
const reportPlain = shortenPath(reportAbs, 80);
|
|
182
|
+
|
|
183
|
+
/** @type {number[]} */
|
|
184
|
+
const plainWidths = [
|
|
185
|
+
...LOGO.map((l) => visibleLength(l)),
|
|
186
|
+
14 + 2 + scanPlain.length,
|
|
187
|
+
14 + 2 + reportPlain.length,
|
|
188
|
+
];
|
|
189
|
+
if (opts.dashboardUrl) plainWidths.push(14 + 2 + opts.dashboardUrl.length);
|
|
190
|
+
if (opts.bundledUrl) {
|
|
191
|
+
plainWidths.push(14 + 2 + `${opts.bundledUrl} (${bundledStatusPlain})`.length);
|
|
192
|
+
}
|
|
193
|
+
plainWidths.push(14 + 2 + `${apiBase} (${apiStatusPlain})`.length);
|
|
194
|
+
if (opts.apiAvailable) {
|
|
195
|
+
plainWidths.push(14 + 2 + `${apiBase}/dslint-report.json`.length);
|
|
196
|
+
plainWidths.push(14 + 2 + `${apiBase}/events`.length);
|
|
197
|
+
}
|
|
198
|
+
if (opts.pollMs) plainWidths.push(14 + 2 + `polling every ${opts.pollMs} ms`.length);
|
|
199
|
+
plainWidths.push(visibleLength(" Open the Dashboard URL in your browser. Ctrl+C to stop."));
|
|
200
|
+
|
|
201
|
+
const contentWidth = Math.min(maxBox - 4, Math.max(...plainWidths, 40));
|
|
202
|
+
const totalWidth = contentWidth + 4;
|
|
203
|
+
|
|
204
|
+
/** @type {string[]} */
|
|
205
|
+
const styledRows = [];
|
|
206
|
+
styledRows.push("");
|
|
207
|
+
styledRows.push(...LOGO);
|
|
208
|
+
styledRows.push("");
|
|
209
|
+
styledRows.push(
|
|
210
|
+
...row(color.label("Scan path"), scanPlain, contentWidth, color.value),
|
|
211
|
+
);
|
|
212
|
+
styledRows.push(
|
|
213
|
+
...row(color.label("Report file"), reportPlain, contentWidth, color.value),
|
|
214
|
+
);
|
|
215
|
+
styledRows.push("");
|
|
216
|
+
if (opts.dashboardUrl) {
|
|
217
|
+
styledRows.push(
|
|
218
|
+
...row(color.label("Dashboard"), opts.dashboardUrl, contentWidth, color.url),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (opts.bundledUrl) {
|
|
222
|
+
const status = opts.apiAvailable ? color.ok(bundledStatusPlain) : color.warn(bundledStatusPlain);
|
|
223
|
+
styledRows.push(
|
|
224
|
+
...row(
|
|
225
|
+
color.label("Bundled UI"),
|
|
226
|
+
`${opts.bundledUrl} (${bundledStatusPlain})`,
|
|
227
|
+
contentWidth,
|
|
228
|
+
() => `${color.url(opts.bundledUrl)} ${color.dim("(")}${status}${color.dim(")")}`,
|
|
229
|
+
),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const apiStatus = opts.apiAvailable ? color.ok(apiStatusPlain) : color.err(apiStatusPlain);
|
|
233
|
+
styledRows.push(
|
|
234
|
+
...row(
|
|
235
|
+
color.label("Scanner API"),
|
|
236
|
+
`${apiBase} (${apiStatusPlain})`,
|
|
237
|
+
contentWidth,
|
|
238
|
+
() => `${color.url(apiBase)} ${color.dim("(")}${apiStatus}${color.dim(")")}`,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
if (opts.apiAvailable) {
|
|
242
|
+
styledRows.push(
|
|
243
|
+
...row("", `${apiBase}/dslint-report.json`, contentWidth, color.dim),
|
|
244
|
+
);
|
|
245
|
+
styledRows.push(...row("", `${apiBase}/events`, contentWidth, color.dim));
|
|
246
|
+
}
|
|
247
|
+
if (opts.pollMs) {
|
|
248
|
+
styledRows.push("");
|
|
249
|
+
styledRows.push(
|
|
250
|
+
...row(color.label("Watch"), `polling every ${opts.pollMs} ms`, contentWidth, color.dim),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
styledRows.push("");
|
|
254
|
+
styledRows.push(color.dim(" Open the Dashboard URL in your browser. Ctrl+C to stop."));
|
|
255
|
+
|
|
256
|
+
return boxLines(styledRows, totalWidth).join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {Parameters<typeof formatDevBanner>[0]} opts
|
|
261
|
+
*/
|
|
262
|
+
export function writeDevBanner(opts) {
|
|
263
|
+
process.stderr.write(`${formatDevBanner(opts)}\n`);
|
|
264
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatDevBanner,
|
|
4
|
+
LOGO,
|
|
5
|
+
shortenPath,
|
|
6
|
+
visibleLength,
|
|
7
|
+
} from "./dev-banner.mjs";
|
|
8
|
+
|
|
9
|
+
describe("shortenPath", () => {
|
|
10
|
+
it("replaces home with tilde", () => {
|
|
11
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
12
|
+
if (!home) return;
|
|
13
|
+
expect(shortenPath(`${home}/Projects/foo`, 80)).toBe("~/Projects/foo");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("truncates long paths in the middle", () => {
|
|
17
|
+
const long = "/very/long/path/segment/that/exceeds/the/maximum/allowed/length/for/display";
|
|
18
|
+
const out = shortenPath(long, 40);
|
|
19
|
+
expect(out.length).toBeLessThanOrEqual(40);
|
|
20
|
+
expect(out).toContain("…");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("formatDevBanner", () => {
|
|
25
|
+
it("includes logo, scan path, report, dashboard, and API URLs", () => {
|
|
26
|
+
const text = formatDevBanner({
|
|
27
|
+
scanPath: "/tmp/components",
|
|
28
|
+
reportPath: "/tmp/components/public/dslint-report.json",
|
|
29
|
+
apiPort: 7878,
|
|
30
|
+
apiAvailable: true,
|
|
31
|
+
dashboardUrl: "http://localhost:5173/",
|
|
32
|
+
bundledUrl: "http://127.0.0.1:7878/",
|
|
33
|
+
pollMs: 150,
|
|
34
|
+
});
|
|
35
|
+
expect(text).toContain(LOGO[0]);
|
|
36
|
+
expect(text).toContain(LOGO[1]);
|
|
37
|
+
expect(text).toContain("Scan path");
|
|
38
|
+
expect(text).toContain("Report file");
|
|
39
|
+
expect(text).toContain("Dashboard");
|
|
40
|
+
expect(text).toContain("http://localhost:5173/");
|
|
41
|
+
expect(text).toContain("7878");
|
|
42
|
+
expect(text).toContain("dslint-report.json");
|
|
43
|
+
expect(text).toContain("polling every 150 ms");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("marks API unavailable when port is busy", () => {
|
|
47
|
+
const text = formatDevBanner({
|
|
48
|
+
scanPath: ".",
|
|
49
|
+
reportPath: "./public/dslint-report.json",
|
|
50
|
+
apiPort: 7878,
|
|
51
|
+
apiAvailable: false,
|
|
52
|
+
dashboardUrl: "http://localhost:5174/",
|
|
53
|
+
});
|
|
54
|
+
expect(text).toContain("unavailable");
|
|
55
|
+
expect(text).not.toContain("/events");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps right border aligned on every row", () => {
|
|
59
|
+
const text = formatDevBanner({
|
|
60
|
+
scanPath: "/very/long/path/that/could/push/the/box/wider/than/usual/Components",
|
|
61
|
+
reportPath: "/very/long/path/public/dslint-report.json",
|
|
62
|
+
apiPort: 7878,
|
|
63
|
+
apiAvailable: false,
|
|
64
|
+
dashboardUrl: "http://localhost:5175/",
|
|
65
|
+
bundledUrl: "http://127.0.0.1:7878/",
|
|
66
|
+
pollMs: 150,
|
|
67
|
+
});
|
|
68
|
+
const rows = text.split("\n").filter((l) => l.startsWith("│"));
|
|
69
|
+
const widths = rows.map((l) => visibleLength(l));
|
|
70
|
+
expect(new Set(widths).size).toBe(1);
|
|
71
|
+
expect(rows.every((l) => l.endsWith("│"))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("visibleLength", () => {
|
|
76
|
+
it("ignores ansi codes", () => {
|
|
77
|
+
expect(visibleLength("\u001b[32mok\u001b[0m")).toBe(2);
|
|
78
|
+
expect(visibleLength("\u001b[4;36mhttp://x\u001b[0m")).toBe(8);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/bin/lib/port-check.mjs
CHANGED
|
@@ -3,21 +3,51 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {number} port
|
|
6
|
-
* @param {string}
|
|
6
|
+
* @param {string} host
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
function isListening(port, host) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
resolve(
|
|
10
|
+
const socket = net.connect({ port, host }, () => {
|
|
11
|
+
socket.destroy();
|
|
12
|
+
resolve(true);
|
|
13
13
|
});
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
socket.once("error", () => resolve(false));
|
|
15
|
+
socket.setTimeout(400, () => {
|
|
16
|
+
socket.destroy();
|
|
17
|
+
resolve(false);
|
|
16
18
|
});
|
|
17
|
-
server.listen(port, host);
|
|
18
19
|
});
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Whether something is already accepting connections on `port`.
|
|
24
|
+
* @param {number} port
|
|
25
|
+
* @param {string} [host] When set, only that address (e.g. `127.0.0.1` for the Rust scanner).
|
|
26
|
+
*/
|
|
27
|
+
export async function isPortInUse(port, host) {
|
|
28
|
+
if (host) return isListening(port, host);
|
|
29
|
+
if (await isListening(port, "127.0.0.1")) return true;
|
|
30
|
+
try {
|
|
31
|
+
if (await isListening(port, "::1")) return true;
|
|
32
|
+
} catch {
|
|
33
|
+
// IPv6 unavailable
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {number} start
|
|
40
|
+
* @param {number} [tries]
|
|
41
|
+
*/
|
|
42
|
+
export async function findAvailablePort(start, tries = 32) {
|
|
43
|
+
for (let i = 0; i < tries; i++) {
|
|
44
|
+
const port = start + i;
|
|
45
|
+
if (port > 65535) break;
|
|
46
|
+
if (!(await isPortInUse(port))) return port;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`dslinter: no free port found near ${start}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
/**
|
|
22
52
|
* @param {number} port
|
|
23
53
|
*/
|
|
@@ -39,19 +69,22 @@ export function describePortOccupant(port) {
|
|
|
39
69
|
|
|
40
70
|
/**
|
|
41
71
|
* @param {number} port
|
|
72
|
+
* @param {{ silent?: boolean }} [opts]
|
|
42
73
|
*/
|
|
43
|
-
export async function warnIfPortBusy(port) {
|
|
44
|
-
if (!(await isPortInUse(port))) return false;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
export async function warnIfPortBusy(port, opts = {}) {
|
|
75
|
+
if (!(await isPortInUse(port, "127.0.0.1"))) return false;
|
|
76
|
+
if (!opts.silent) {
|
|
77
|
+
const detail = describePortOccupant(port);
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
[
|
|
80
|
+
"",
|
|
81
|
+
`[dslinter] Port ${port} is already in use — scanner cannot bind.`,
|
|
82
|
+
detail ? ` ${detail.replace(/\n/g, "\n ")}` : "",
|
|
83
|
+
` Stop the old process, then restart. Example: lsof -nP -iTCP:${port} -sTCP:LISTEN`,
|
|
84
|
+
" If you already have `npm run dev` running, open the Vite URL it printed (e.g. http://localhost:5173/).",
|
|
85
|
+
"",
|
|
86
|
+
].join("\n"),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
56
89
|
return true;
|
|
57
90
|
}
|
package/bin/lib/run-scanner.mjs
CHANGED
|
@@ -37,11 +37,12 @@ export async function spawnScanner(scannerArgs, options = {}) {
|
|
|
37
37
|
|
|
38
38
|
return spawn(process.execPath, [binScript, ...scannerArgs], {
|
|
39
39
|
stdio: "inherit",
|
|
40
|
+
...options,
|
|
40
41
|
env: {
|
|
41
42
|
...process.env,
|
|
42
43
|
DSLINTER_INTERNAL: "1",
|
|
44
|
+
...options.env,
|
|
43
45
|
},
|
|
44
|
-
...options,
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|