dslinter 0.0.27 → 0.0.29

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.0.29
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.28...v0.0.29)
6
+
7
+ ### 💅 Refactors
8
+
9
+ - Enhance dashboard directory resolution and improve dev mode output ([5670315](https://github.com/jrmybtlr/DSLinter/commit/5670315))
10
+
11
+ ### 🏡 Chore
12
+
13
+ - Update GitHub Actions and package.json ([b9541b2](https://github.com/jrmybtlr/DSLinter/commit/b9541b2))
14
+
15
+ ### ❤️ Contributors
16
+
17
+ - Jeremy Butler <jeremy.butler@laravel.com>
18
+
19
+ ## v0.0.28
20
+
21
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.27...v0.0.28)
22
+
23
+ ### 🏡 Chore
24
+
25
+ - Update dashboard build process and CLI commands ([43c71d6](https://github.com/jrmybtlr/DSLinter/commit/43c71d6))
26
+
27
+ ### ❤️ Contributors
28
+
29
+ - Jeremy Butler <jeremy.butler@laravel.com>
30
+
3
31
  ## v0.0.27
4
32
 
5
33
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.0.26...v0.0.27)
package/README.md CHANGED
@@ -19,7 +19,17 @@ This package is **source-first**: entry points resolve to TypeScript/TSX under `
19
19
 
20
20
  ## CLI (`npx dslinter`)
21
21
 
22
- The **`dslinter`** command runs the design-system scanner with the same flags as the Rust CLI (`--json`, `-o`, `--serve`, etc.) via a **napi-rs** native binding (same distribution model as **`oxlint`**).
22
+ The **`dslinter`** command orchestrates the Rust scanner (via **napi-rs**, same distribution model as **`oxlint`**) and, in a Vite host app, the dashboard dev loop.
23
+
24
+ | Mode | Flag | Behavior |
25
+ |------|------|----------|
26
+ | Dev (default locally) | _(none)_ | `--serve`, watch, write `--output`, start Vite `--mode serve` |
27
+ | Report | `--report` | One-shot scan; human stdout or `--json`; `--output` writes JSON |
28
+ | Watch | `--watch` | Watch + write JSON only |
29
+ | Build | `--build` | One-shot report to `--output`, then `vite build` |
30
+ | CI default | `CI=true` | Same as `--report` |
31
+
32
+ Scanner flags: `--json`, `-p` / `--parallel`, `--fail-on-warnings`, `--max-warnings`, `--output`, `[PATH]`. Low-level: `--serve <port>` (watch + HTTP, no Vite).
23
33
 
24
34
  ### Without installing Rust
25
35
 
@@ -38,10 +48,14 @@ The crates.io crate **`dslint`** is a **different project**. Use **`cargo instal
38
48
  Typical usage:
39
49
 
40
50
  ```bash
41
- dslinter /path/to/repo --json -o dslint-report.json
42
- # or --serve for live reload while developing a dashboard
51
+ npx dslinter # dev (watch + dashboard)
52
+ npx dslinter --report /path/to/repo --json
53
+ npx dslinter --report --output public/dslint-report.json
54
+ npx dslinter --watch --output public/dslint-report.json
43
55
  ```
44
56
 
57
+ Set `DSLINT_SERVE_PORT` to override the default scanner port (`7878`). Your Vite config should proxy `/dslint-report.json` and `/events` to that port in `serve` mode (see repo `demo/vite.config.ts`).
58
+
45
59
  ## Styles (Tailwind v4)
46
60
 
47
61
  1. In your app CSS, load Tailwind, then point Tailwind at this package so utility scanning picks up dashboard classes:
package/bin/dslinter.mjs CHANGED
@@ -1,77 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Runs the design-system scanner via the NAPI native binding (oxlint-style).
4
- * Falls back to DSLINT_BIN (cargo-built binary) when set.
3
+ * DSLinter CLI: mode routing (dev / report / watch / build) + scanner (NAPI or DSLINT_BIN).
5
4
  */
6
- import { spawnSync } from "node:child_process";
7
- import { createRequire } from "node:module";
8
- import { existsSync } from "node:fs";
9
- import { dirname, join } from "node:path";
10
- import { fileURLToPath } from "node:url";
5
+ import { parseDslinterArgs } from "./lib/parse-args.mjs";
6
+ import { runScannerInternal } from "./lib/run-scanner.mjs";
7
+ import { runBuildMode } from "./modes/build.mjs";
8
+ import { runDevMode } from "./modes/dev.mjs";
9
+ import { runReportMode } from "./modes/report.mjs";
11
10
 
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const packageRoot = join(__dirname, "..");
14
- const require = createRequire(import.meta.url);
11
+ const rawArgs = process.argv.slice(2);
15
12
 
16
- const args = process.argv.slice(2);
17
-
18
- const SCANNER_VERSION_MARKER = "design system linting";
19
-
20
- function isOurScanner(binary) {
21
- const help = spawnSync(binary, ["--help"], { encoding: "utf8" });
22
- const out = `${help.stdout ?? ""}${help.stderr ?? ""}`;
23
- return out.includes(SCANNER_VERSION_MARKER);
13
+ if (process.env.DSLINTER_INTERNAL === "1") {
14
+ runScannerInternal(rawArgs);
24
15
  }
25
16
 
26
- function runViaNapi() {
27
- let runCli;
28
- try {
29
- ({ runCli } = require(join(packageRoot, "index.cjs")));
30
- } catch (err) {
31
- process.stderr.write(
32
- `dslinter: failed to load native binding.\n` +
33
- ` Run \`pnpm --filter dslinter run build:napi\` from the repo root, or reinstall dslinter.\n` +
34
- ` ${err instanceof Error ? err.message : err}\n`,
35
- );
36
- process.exit(127);
37
- }
38
-
39
- const argv = ["dslinter", ...args];
40
- const code = runCli(argv);
41
- process.exit(code);
17
+ if (rawArgs.includes("--version") || rawArgs.includes("-V")) {
18
+ runScannerInternal(["--version"]);
42
19
  }
43
20
 
44
- function runViaBinary(binary) {
45
- const child = spawnSync(binary, args, { stdio: "inherit" });
46
- if (child.error && "code" in child.error && child.error.code === "ENOENT") {
47
- process.stderr.write(`dslinter: failed to execute ${binary}\n`);
48
- process.exit(127);
49
- }
50
- process.exit(child.status === null ? 1 : child.status);
21
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
22
+ runScannerInternal(["--help"]);
51
23
  }
52
24
 
53
- const fromEnv = process.env.DSLINT_BIN?.trim();
54
- if (fromEnv) {
55
- if (!existsSync(fromEnv)) {
56
- process.stderr.write(`dslinter: DSLINT_BIN not found: ${fromEnv}\n`);
57
- process.exit(127);
58
- }
59
- if (!isOurScanner(fromEnv)) {
60
- process.stderr.write(
61
- `dslinter: DSLINT_BIN does not look like the DSLint design-system scanner.\n` +
62
- ` Expected output containing "${SCANNER_VERSION_MARKER}".\n`,
63
- );
64
- process.exit(1);
65
- }
66
- runViaBinary(fromEnv);
25
+ let parsed;
26
+ try {
27
+ parsed = parseDslinterArgs(rawArgs);
28
+ } catch (err) {
29
+ process.stderr.write(`${err instanceof Error ? err.message : err}\n`);
30
+ process.exit(1);
67
31
  }
68
32
 
69
- if (process.env.DSLINT_ALLOW_PATH === "1") {
70
- const onPath = spawnSync("dslinter", ["--help"], { encoding: "utf8" });
71
- const out = `${onPath.stdout ?? ""}${onPath.stderr ?? ""}`;
72
- if (onPath.status === 0 && out.includes(SCANNER_VERSION_MARKER)) {
73
- runViaBinary("dslinter");
74
- }
33
+ const { mode, scannerArgs } = parsed;
34
+
35
+ switch (mode) {
36
+ case "dev":
37
+ await runDevMode(parsed);
38
+ break;
39
+ case "report":
40
+ runReportMode(["--report", ...scannerArgs]);
41
+ break;
42
+ case "watch":
43
+ runScannerInternal(["--watch", ...scannerArgs]);
44
+ break;
45
+ case "scanner":
46
+ runScannerInternal(scannerArgs);
47
+ break;
48
+ case "build":
49
+ runBuildMode(parsed);
50
+ break;
51
+ default:
52
+ process.stderr.write(`dslinter: unknown mode ${mode}\n`);
53
+ process.exit(1);
75
54
  }
76
-
77
- runViaNapi();
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Parse dslinter CLI argv into mode + scanner args (no subprocess).
3
+ */
4
+
5
+ const MODE_FLAGS = new Set(["--report", "--watch", "--build"]);
6
+
7
+ /** @param {string | undefined} raw */
8
+ function parseServePort(raw) {
9
+ const n = Number.parseInt(raw ?? "", 10);
10
+ if (!Number.isFinite(n) || n < 1 || n > 65_535) return null;
11
+ return n;
12
+ }
13
+
14
+ /**
15
+ * @param {string[]} argv process.argv.slice(2)
16
+ * @returns {{
17
+ * mode: "dev" | "report" | "watch" | "build" | "scanner";
18
+ * scannerArgs: string[];
19
+ * scanPath: string;
20
+ * outputPath: string | null;
21
+ * servePort: number | null;
22
+ * }}
23
+ */
24
+ export function parseDslinterArgs(argv) {
25
+ const modes = [];
26
+ const scannerArgs = [];
27
+
28
+ for (const arg of argv) {
29
+ if (MODE_FLAGS.has(arg)) modes.push(arg.slice(2));
30
+ else scannerArgs.push(arg);
31
+ }
32
+
33
+ if (modes.length > 1) {
34
+ throw new Error(
35
+ `dslinter: mode flags are mutually exclusive (got ${modes.map((m) => `--${m}`).join(", ")})`,
36
+ );
37
+ }
38
+
39
+ let servePort = null;
40
+ let outputPath = null;
41
+ let positional = null;
42
+
43
+ for (let i = 0; i < scannerArgs.length; i++) {
44
+ const arg = scannerArgs[i];
45
+ if (arg === "--serve") {
46
+ servePort = parseServePort(scannerArgs[i + 1]);
47
+ } else if (arg.startsWith("--serve=")) {
48
+ servePort = parseServePort(arg.slice("--serve=".length));
49
+ } else if (arg === "--output" || arg === "-o") {
50
+ outputPath = scannerArgs[i + 1] ?? null;
51
+ } else if (arg.startsWith("--output=")) {
52
+ outputPath = arg.slice("--output=".length);
53
+ } else if (!arg.startsWith("-") && positional === null) {
54
+ positional = arg;
55
+ }
56
+ }
57
+
58
+ let mode;
59
+ if (servePort !== null && modes.length === 0) {
60
+ mode = "scanner";
61
+ } else if (modes.length === 1) {
62
+ mode = modes[0];
63
+ } else if (modes.length === 0) {
64
+ const ci = process.env.CI === "true" || process.env.CI === "1";
65
+ mode = ci ? "report" : "dev";
66
+ } else {
67
+ mode = "dev";
68
+ }
69
+
70
+ const scanPath = positional ?? ".";
71
+
72
+ return {
73
+ mode,
74
+ scannerArgs,
75
+ scanPath,
76
+ outputPath,
77
+ servePort,
78
+ };
79
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseDslinterArgs } from "./parse-args.mjs";
3
+
4
+ describe("parseDslinterArgs", () => {
5
+ it("defaults to dev locally", () => {
6
+ const prev = process.env.CI;
7
+ delete process.env.CI;
8
+ expect(parseDslinterArgs([]).mode).toBe("dev");
9
+ process.env.CI = prev;
10
+ });
11
+
12
+ it("defaults to report in CI", () => {
13
+ const prev = process.env.CI;
14
+ process.env.CI = "true";
15
+ expect(parseDslinterArgs([]).mode).toBe("report");
16
+ process.env.CI = prev;
17
+ });
18
+
19
+ it("rejects multiple mode flags", () => {
20
+ expect(() => parseDslinterArgs(["--report", "--watch"])).toThrow(/mutually exclusive/);
21
+ });
22
+
23
+ it("passes through scanner flags", () => {
24
+ const p = parseDslinterArgs(["--report", "demo", "-p", "--json"]);
25
+ expect(p.mode).toBe("report");
26
+ expect(p.scannerArgs).toEqual(["demo", "-p", "--json"]);
27
+ expect(p.scanPath).toBe("demo");
28
+ });
29
+
30
+ it("uses scanner mode for --serve only", () => {
31
+ expect(parseDslinterArgs([".", "--serve", "7878"]).mode).toBe("scanner");
32
+ });
33
+
34
+ it("extracts output and serve ports", () => {
35
+ const p = parseDslinterArgs(["--watch", "--output", "out.json", "--serve", "9"]);
36
+ expect(p.mode).toBe("watch");
37
+ expect(p.outputPath).toBe("out.json");
38
+ expect(p.servePort).toBe(9);
39
+ });
40
+
41
+ it("parses --serve=port", () => {
42
+ const p = parseDslinterArgs(["--serve=7878"]);
43
+ expect(p.mode).toBe("scanner");
44
+ expect(p.servePort).toBe(7878);
45
+ });
46
+
47
+ it("does not use scanner mode when --serve port is missing or invalid", () => {
48
+ const prev = process.env.CI;
49
+ delete process.env.CI;
50
+ for (const argv of [
51
+ ["--serve"],
52
+ ["--serve", ""],
53
+ ["--serve", "abc"],
54
+ ["--serve=not-a-port"],
55
+ ["--serve", "0"],
56
+ ["--serve=70000"],
57
+ ]) {
58
+ const p = parseDslinterArgs(argv);
59
+ expect(p.servePort).toBeNull();
60
+ expect(p.mode).toBe("dev");
61
+ }
62
+ process.env.CI = prev;
63
+ });
64
+ });
@@ -0,0 +1,108 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
7
+
8
+ const VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"];
9
+
10
+ /**
11
+ * @param {string} startDir
12
+ * @returns {string | null}
13
+ */
14
+ export function findViteRoot(startDir) {
15
+ let dir = resolve(startDir);
16
+ for (;;) {
17
+ for (const name of VITE_CONFIG_NAMES) {
18
+ if (existsSync(join(dir, name))) return dir;
19
+ }
20
+ const parent = dirname(dir);
21
+ if (parent === dir) return null;
22
+ dir = parent;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @param {string} scanPath
28
+ * @param {string | null} outputFlag
29
+ * @returns {string}
30
+ */
31
+ export function defaultReportPath(scanPath, outputFlag) {
32
+ if (outputFlag) return resolve(outputFlag);
33
+ return resolve(scanPath, "public", "dslint-report.json");
34
+ }
35
+
36
+ /**
37
+ * @returns {number}
38
+ */
39
+ export function defaultServePort() {
40
+ const fromEnv = process.env.DSLINT_SERVE_PORT?.trim();
41
+ if (fromEnv) {
42
+ const n = Number.parseInt(fromEnv, 10);
43
+ if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
44
+ }
45
+ return 7878;
46
+ }
47
+
48
+ /**
49
+ * @param {string} dir
50
+ * @returns {string | null} canonical absolute path when `index.html` exists
51
+ */
52
+ function dashboardDirIfReady(dir) {
53
+ const indexHtml = join(dir, "index.html");
54
+ if (!existsSync(indexHtml)) return null;
55
+ try {
56
+ return realpathSync(dir);
57
+ } catch {
58
+ return resolve(dir);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Pre-built dashboard SPA for `--dashboard-static` (same port as `--serve`).
64
+ *
65
+ * Resolution order:
66
+ * 1. Skip when `DSLINTER_NO_BUNDLED_DASHBOARD=1`
67
+ * 2. `DSLINT_DASHBOARD_STATIC` — absolute or cwd-relative (temp/gitignored dirs ok)
68
+ * 3. `dashboard-dist/` next to the installed `dslinter` package
69
+ *
70
+ * @returns {string | null}
71
+ */
72
+ export function resolveBundledDashboardDir() {
73
+ const optOut = process.env.DSLINTER_NO_BUNDLED_DASHBOARD?.trim();
74
+ if (optOut === "1" || optOut?.toLowerCase() === "true") return null;
75
+
76
+ const fromEnv = process.env.DSLINT_DASHBOARD_STATIC?.trim();
77
+ if (fromEnv) {
78
+ const dir = isAbsolute(fromEnv) ? normalize(fromEnv) : resolve(process.cwd(), fromEnv);
79
+ return dashboardDirIfReady(dir);
80
+ }
81
+
82
+ return dashboardDirIfReady(join(packageRoot, "dashboard-dist"));
83
+ }
84
+
85
+ /**
86
+ * Resolve vite CLI entry from a project directory (supports hoisted / workspace installs).
87
+ * @param {string} projectDir
88
+ * @returns {string | null}
89
+ */
90
+ export function resolveViteBin(projectDir) {
91
+ let dir = resolve(projectDir);
92
+ for (;;) {
93
+ const pkgJson = join(dir, "package.json");
94
+ if (existsSync(pkgJson)) {
95
+ try {
96
+ const req = createRequire(pkgJson);
97
+ return req.resolve("vite/bin/vite.js");
98
+ } catch {
99
+ // try parent
100
+ }
101
+ }
102
+ const nested = join(dir, "node_modules", "vite", "bin", "vite.js");
103
+ if (existsSync(nested)) return nested;
104
+ const parent = dirname(dir);
105
+ if (parent === dir) return null;
106
+ dir = parent;
107
+ }
108
+ }
@@ -0,0 +1,135 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = join(__dirname, "../..");
9
+ const binScript = join(__dirname, "../dslinter.mjs");
10
+ const require = createRequire(import.meta.url);
11
+
12
+ const SCANNER_VERSION_MARKER = "design system linting";
13
+
14
+ function isOurScanner(binary) {
15
+ const help = spawnSync(binary, ["--help"], { encoding: "utf8" });
16
+ const out = `${help.stdout ?? ""}${help.stderr ?? ""}`;
17
+ return out.includes(SCANNER_VERSION_MARKER);
18
+ }
19
+
20
+ /**
21
+ * @returns {Promise<import("node:child_process").ChildProcess>}
22
+ */
23
+ export async function spawnScanner(scannerArgs, options = {}) {
24
+ const fromEnv = process.env.DSLINT_BIN?.trim();
25
+ if (fromEnv) {
26
+ if (!existsSync(fromEnv)) {
27
+ throw new Error(`dslinter: DSLINT_BIN not found: ${fromEnv}`);
28
+ }
29
+ if (!isOurScanner(fromEnv)) {
30
+ throw new Error("dslinter: DSLINT_BIN does not look like the DSLint scanner");
31
+ }
32
+ return spawn(fromEnv, scannerArgs, {
33
+ stdio: "inherit",
34
+ ...options,
35
+ });
36
+ }
37
+
38
+ return spawn(process.execPath, [binScript, ...scannerArgs], {
39
+ stdio: "inherit",
40
+ env: {
41
+ ...process.env,
42
+ DSLINTER_INTERNAL: "1",
43
+ },
44
+ ...options,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Run scanner synchronously (report / build scan). Returns exit code.
50
+ * @param {string[]} scannerArgs
51
+ * @param {{ captureStdout?: boolean }} [opts]
52
+ * @returns {number}
53
+ */
54
+ export function runScannerSync(scannerArgs, opts = {}) {
55
+ const fromEnv = process.env.DSLINT_BIN?.trim();
56
+ if (fromEnv) {
57
+ const child = spawnSync(fromEnv, scannerArgs, {
58
+ stdio: opts.captureStdout ? ["ignore", "pipe", "inherit"] : "inherit",
59
+ encoding: opts.captureStdout ? "utf8" : undefined,
60
+ });
61
+ if (opts.captureStdout && child.stdout) {
62
+ process.stdout.write(child.stdout);
63
+ }
64
+ return child.status === null ? 1 : child.status;
65
+ }
66
+
67
+ let runCli;
68
+ try {
69
+ ({ runCli } = require(join(packageRoot, "index.cjs")));
70
+ } catch (err) {
71
+ process.stderr.write(
72
+ `dslinter: failed to load native binding.\n` +
73
+ ` Run \`pnpm --filter dslinter run build:napi\` from the repo root, or reinstall dslinter.\n` +
74
+ ` ${err instanceof Error ? err.message : err}\n`,
75
+ );
76
+ return 127;
77
+ }
78
+
79
+ if (opts.captureStdout) {
80
+ const { execFileSync } = require("node:child_process");
81
+ try {
82
+ const out = execFileSync(process.execPath, [binScript, ...scannerArgs], {
83
+ env: { ...process.env, DSLINTER_INTERNAL: "1" },
84
+ encoding: "utf8",
85
+ stdio: ["ignore", "pipe", "inherit"],
86
+ });
87
+ process.stdout.write(out);
88
+ return 0;
89
+ } catch (e) {
90
+ if (e && typeof e === "object" && "status" in e && typeof e.status === "number") {
91
+ if (e.stdout) process.stdout.write(String(e.stdout));
92
+ return e.status;
93
+ }
94
+ return 1;
95
+ }
96
+ }
97
+
98
+ const argv = ["dslinter", ...scannerArgs];
99
+ return runCli(argv);
100
+ }
101
+
102
+ /**
103
+ * Internal path: run scanner only (no orchestration).
104
+ * @param {string[]} args
105
+ */
106
+ export function runScannerInternal(args) {
107
+ const fromEnv = process.env.DSLINT_BIN?.trim();
108
+ if (fromEnv) {
109
+ const child = spawnSync(fromEnv, args, { stdio: "inherit" });
110
+ process.exit(child.status === null ? 1 : child.status);
111
+ }
112
+
113
+ if (process.env.DSLINT_ALLOW_PATH === "1") {
114
+ const onPath = spawnSync("dslinter", ["--help"], { encoding: "utf8" });
115
+ const out = `${onPath.stdout ?? ""}${onPath.stderr ?? ""}`;
116
+ if (onPath.status === 0 && out.includes(SCANNER_VERSION_MARKER)) {
117
+ const child = spawnSync("dslinter", args, { stdio: "inherit" });
118
+ process.exit(child.status === null ? 1 : child.status);
119
+ }
120
+ }
121
+
122
+ let runCli;
123
+ try {
124
+ ({ runCli } = require(join(packageRoot, "index.cjs")));
125
+ } catch (err) {
126
+ process.stderr.write(
127
+ `dslinter: failed to load native binding.\n` +
128
+ ` ${err instanceof Error ? err.message : err}\n`,
129
+ );
130
+ process.exit(127);
131
+ }
132
+
133
+ const code = runCli(["dslinter", ...args]);
134
+ process.exit(code);
135
+ }
@@ -0,0 +1,30 @@
1
+ import net from "node:net";
2
+
3
+ /**
4
+ * @param {number} port
5
+ * @param {{ host?: string; timeoutMs?: number; intervalMs?: number }} [opts]
6
+ */
7
+ export function waitForPort(port, opts = {}) {
8
+ const host = opts.host ?? "127.0.0.1";
9
+ const timeoutMs = opts.timeoutMs ?? 60_000;
10
+ const intervalMs = opts.intervalMs ?? 100;
11
+ const deadline = Date.now() + timeoutMs;
12
+
13
+ return new Promise((resolve, reject) => {
14
+ const tryConnect = () => {
15
+ if (Date.now() > deadline) {
16
+ reject(new Error(`dslinter: timed out waiting for ${host}:${port}`));
17
+ return;
18
+ }
19
+ const socket = net.connect({ host, port }, () => {
20
+ socket.end();
21
+ resolve();
22
+ });
23
+ socket.on("error", () => {
24
+ socket.destroy();
25
+ setTimeout(tryConnect, intervalMs);
26
+ });
27
+ };
28
+ tryConnect();
29
+ });
30
+ }
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { defaultReportPath, findViteRoot, resolveViteBin } from "../lib/project-root.mjs";
3
+ import { runScannerSync } from "../lib/run-scanner.mjs";
4
+
5
+ /**
6
+ * @param {{
7
+ * scanPath: string;
8
+ * outputPath: string | null;
9
+ * scannerArgs: string[];
10
+ * }}
11
+ */
12
+ export function runBuildMode({ scanPath, outputPath, scannerArgs }) {
13
+ const reportPath = defaultReportPath(scanPath, outputPath);
14
+ const args = ["--report", ...scannerArgs];
15
+ if (!args.some((a) => a === "--output" || a.startsWith("--output="))) {
16
+ args.push("--output", reportPath);
17
+ }
18
+
19
+ const code = runScannerSync(args);
20
+ if (code !== 0) process.exit(code);
21
+
22
+ const viteRoot = findViteRoot(process.cwd());
23
+ if (!viteRoot) {
24
+ process.stderr.write(
25
+ "dslinter: --build requires a Vite project (vite.config.* not found).\n",
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ const viteBin = resolveViteBin(viteRoot);
31
+ if (!viteBin) {
32
+ process.stderr.write(`dslinter: vite not installed in ${viteRoot}. Run npm install.\n`);
33
+ process.exit(1);
34
+ }
35
+ const child = spawnSync(process.execPath, [viteBin, "build"], {
36
+ cwd: viteRoot,
37
+ stdio: "inherit",
38
+ });
39
+ process.exit(child.status === null ? 1 : child.status);
40
+ }