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 +28 -0
- package/README.md +17 -3
- package/bin/dslinter.mjs +40 -63
- package/bin/lib/parse-args.mjs +79 -0
- package/bin/lib/parse-args.test.mjs +64 -0
- package/bin/lib/project-root.mjs +108 -0
- package/bin/lib/run-scanner.mjs +135 -0
- package/bin/lib/wait-for-port.mjs +30 -0
- package/bin/modes/build.mjs +40 -0
- package/bin/modes/dev.mjs +136 -0
- package/bin/modes/report.mjs +9 -0
- package/dashboard-dist/assets/index-BTfdBwuZ.js +245 -0
- package/dashboard-dist/assets/index-Dgfmp0Yv.css +1 -0
- package/dashboard-dist/index.html +13 -0
- package/index.cjs +52 -52
- package/package.json +13 -7
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
|
|
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
|
|
42
|
-
|
|
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
|
-
*
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
|
13
|
-
const packageRoot = join(__dirname, "..");
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
11
|
+
const rawArgs = process.argv.slice(2);
|
|
15
12
|
|
|
16
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|