dslinter 0.1.4 → 0.1.13

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +80 -1
  2. package/README.md +46 -35
  3. package/bin/lib/project-root.mjs +20 -14
  4. package/bin/lib/project-root.test.mjs +21 -0
  5. package/bin/lib/scaffold-config.mjs +75 -0
  6. package/bin/lib/scaffold-config.test.mjs +33 -0
  7. package/bin/modes/build.mjs +6 -0
  8. package/bin/modes/dev.mjs +26 -2
  9. package/bin/modes/init.mjs +39 -36
  10. package/bin/modes/init.test.mjs +16 -0
  11. package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +1 -0
  12. package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +1 -0
  13. package/dashboard-dist/assets/index-Cbv7vXvH.css +1 -0
  14. package/dashboard-dist/assets/index-e20cwqnb.js +206 -0
  15. package/dashboard-dist/index.html +2 -2
  16. package/index.cjs +52 -52
  17. package/package.json +16 -7
  18. package/src/components/ComponentInspectPane.tsx +7 -11
  19. package/src/dashboard/ComponentUsageDetails.tsx +9 -3
  20. package/src/index.ts +1 -0
  21. package/src/playground/scanPlaygroundModules.ts +1 -0
  22. package/src/playground/usePlaygroundFromReport.test.ts +37 -0
  23. package/src/playground/usePlaygroundFromReport.ts +23 -0
  24. package/src/playground/virtual-playground-modules.d.ts +6 -0
  25. package/src/shell/DashboardLayout.tsx +37 -4
  26. package/src/shell/DashboardLayoutAuto.tsx +20 -0
  27. package/templates/vite.dslint-scan-alias.snippet.ts +2 -12
  28. package/templates/vite.dslinter.snippet.ts +11 -19
  29. package/vite/collectScanModules.test.ts +42 -0
  30. package/vite/collectScanModules.ts +60 -0
  31. package/vite/consumer.config.mjs +22 -0
  32. package/vite/plugin.ts +140 -0
  33. package/dashboard-dist/assets/index-BhDQfrwA.css +0 -1
  34. package/dashboard-dist/assets/index-DGUG_3SK.js +0 -205
package/CHANGELOG.md CHANGED
@@ -1,14 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.1.13
4
+
5
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.12...v0.1.13)
6
+
7
+ ### 🏡 Chore
8
+
9
+ - **release:** V0.1.12 ([7c2190a](https://github.com/jrmybtlr/DSLinter/commit/7c2190a))
10
+
11
+ ### ❤️ Contributors
12
+
13
+ - Jeremy Butler <jeremy.butler@laravel.com>
14
+
15
+ ## v0.1.12
16
+
17
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.10...v0.1.12)
18
+
19
+ ### 🏡 Chore
20
+
21
+ - **release:** V0.1.11 ([bc2d134](https://github.com/jrmybtlr/DSLinter/commit/bc2d134))
22
+
23
+ ### ❤️ Contributors
24
+
25
+ - Jeremy Butler <jeremy.butler@laravel.com>
26
+
27
+ ## v0.1.11
28
+
29
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.10...v0.1.11)
30
+
31
+ ## v0.1.10
32
+
33
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.8...v0.1.10)
34
+
35
+ ### 🏡 Chore
36
+
37
+ - **release:** V0.1.9 ([a54ea04](https://github.com/jrmybtlr/DSLinter/commit/a54ea04))
38
+
39
+ ### ❤️ Contributors
40
+
41
+ - Jeremy Butler <jeremy.butler@laravel.com>
42
+
43
+ ## v0.1.8
44
+
45
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.7...v0.1.8)
46
+
47
+ ### 🚀 Enhancements
48
+
49
+ - Auto-scaffold dslint config and add scan scope options ([572e97b](https://github.com/jrmybtlr/DSLinter/commit/572e97b))
50
+
51
+ ### 🩹 Fixes
52
+
53
+ - Warn when css_entrypoint path is missing ([88a5038](https://github.com/jrmybtlr/DSLinter/commit/88a5038))
54
+ - Don't scope CSS discovery to include_dirs ([7e6df94](https://github.com/jrmybtlr/DSLinter/commit/7e6df94))
55
+
56
+ ## v0.1.7
57
+
58
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.5...v0.1.7)
59
+
60
+ ### 🏡 Chore
61
+
62
+ - **release:** V0.1.6 ([1463fbc](https://github.com/jrmybtlr/DSLinter/commit/1463fbc))
63
+
64
+ ### ❤️ Contributors
65
+
66
+ - Jeremy Butler <jeremy.butler@laravel.com>
67
+
68
+ ## v0.1.6
69
+
70
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.5...v0.1.6)
71
+
72
+ ## v0.1.5
73
+
74
+ [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.4...v0.1.5)
75
+
3
76
  ## v0.1.4
4
77
 
5
78
  [compare changes](https://github.com/jrmybtlr/DSLinter/compare/v0.1.3...v0.1.4)
6
79
 
7
80
  ## Unreleased
8
81
 
82
+ ### 🚀 Enhancements
83
+
84
+ - **`dslinter/vite` plugin** — virtual `playground-modules` map keyed by `@dslint-scan/<rel_path>`; scanner proxy, react dedupe, `optimizeDeps.exclude`, and `server.fs.allow` for the scan root.
85
+ - **`DashboardLayout autoPlayground`** and **`usePlaygroundFromReport`** — zero-config live previews without `buildRegistry.ts`.
86
+ - **`npx dslinter`** auto-merges the plugin into consumer Vite via `vite/consumer.config.mjs` when a host app is detected.
87
+
9
88
  ### 🩹 Fixes
10
89
 
11
- - Remove `@/` path aliases from published `src/` so host apps with their own `@/*` Vite alias (Laravel/Inertia, etc.) no longer resolve dslinter UI imports to the wrong tree. Consumers need only the official vite snippet (proxy, react dedupe, `optimizeDeps.exclude`).
90
+ - Remove `@/` path aliases from published `src/` so host apps with their own `@/*` Vite alias (Laravel/Inertia, etc.) no longer resolve dslinter UI imports to the wrong tree.
12
91
 
13
92
  ## v0.1.3
14
93
 
package/README.md CHANGED
@@ -48,26 +48,48 @@ The crates.io crate **`dslint`** is a **different project**. Use **`cargo instal
48
48
  Typical usage:
49
49
 
50
50
  ```bash
51
- npx dslinter init # scaffold src/playground/buildRegistry.ts
51
+ npx dslinter init # scaffold .dslint.json + src/playground/buildRegistry.ts
52
52
  npx dslinter # dev (watch + dashboard)
53
53
  npx dslinter --report /path/to/repo --json
54
54
  npx dslinter --report --output public/dslint-report.json
55
55
  npx dslinter --watch --output public/dslint-report.json
56
56
  ```
57
57
 
58
- 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. Merge `templates/vite.dslinter.snippet.ts` (copied by `npx dslinter init`) or see repo `demo/vite.config.ts` for a linked-workspace example.
58
+ Set `DSLINT_SERVE_PORT` to override the default scanner port (`7878`).
59
+ On first local run, `npx dslinter` scaffolds `.dslint.json` automatically if missing.
59
60
 
60
- ### Consumer Vite (Laravel, Inertia, existing `@/*` aliases)
61
+ ### Zero-config live previews (recommended)
62
+
63
+ **`npx dslinter .`** from your project root auto-merges the dslinter Vite plugin (playground module glob, scanner proxy, react dedupe). In your app:
64
+
65
+ ```tsx
66
+ import { DashboardLayout, useWorkspaceReport } from "dslinter";
67
+
68
+ const dslinterReport = useWorkspaceReport({
69
+ reportUrl: "/dslint-report.json",
70
+ watchUrl: "/events",
71
+ });
72
+
73
+ <DashboardLayout autoPlayground dslinterReport={dslinterReport} tokenCatalog={...} />;
74
+ ```
75
+
76
+ No `buildRegistry.ts` scaffold required. Works with Laravel/Inertia (`resources/js/Components/...`) and existing `@/*` → `resources/js/*` aliases.
77
+
78
+ **Direct `vite --mode serve`:** add one line to `vite.config.ts`:
61
79
 
62
- Published `dslinter` source uses **relative imports only** — no `@/components/...` paths inside `node_modules/dslinter/src`. Your app's `@/*` alias (for example Laravel `@/*` → `resources/js/*`) will not hijack dslinter's internal UI imports.
80
+ ```ts
81
+ import dslinter from "dslinter/vite";
63
82
 
64
- After `npx dslinter init --laravel`, merge only the official snippet into `vite.config.ts`:
83
+ export default defineConfig({
84
+ plugins: [dslinter(), /* your plugins */],
85
+ });
86
+ ```
65
87
 
66
- - `resolve.dedupe`: `["react", "react-dom"]`
67
- - `optimizeDeps.exclude`: `["dslinter"]` (source-first package; transpile from `node_modules`)
68
- - `server.proxy` for `/dslint-report.json` and `/events` `DSLINT_SERVE_PORT`
88
+ The plugin sets `DSLINT_SCAN_ROOT` from the environment (set by `npx dslinter`) or defaults to `process.cwd()`.
89
+
90
+ ### Consumer Vite (Laravel, Inertia, existing `@/*` aliases)
69
91
 
70
- You do **not** need alias overrides such as `@/components` → `node_modules/dslinter/src/components`.
92
+ Published `dslinter` source uses **relative imports only** your app's `@/*` alias does not hijack dslinter internal UI. You do **not** need `@/components` → `node_modules/dslinter` alias overrides.
71
93
 
72
94
  ## Styles (Tailwind v4)
73
95
 
@@ -84,44 +106,33 @@ You do **not** need alias overrides such as `@/components` → `node_modules/dsl
84
106
  @import "dslinter/theme.css";
85
107
  ```
86
108
 
87
- ## Live component previews (consumer Vite apps)
109
+ ## Live component previews (advanced / custom glob)
88
110
 
89
- If the dashboard shows **“Scan snapshot no live preview”** for a component that appears in the catalog, the scanner found the file but Vite did not load it. Wire previews in three steps:
111
+ Use **`autoPlayground`** (above) for zero-config previews. Optionally scaffold a narrower glob for faster dev or custom controls:
90
112
 
91
- 1. **Scaffold** (optional): `npx dslinter init` → `src/playground/buildRegistry.ts`; for Inertia/Laravel (`resources/js/…`) use `npx dslinter init --laravel`
92
- 2. **Glob** must cover nested paths, e.g. `import.meta.glob("../components/**/*.{tsx,jsx}", { eager: true })`
93
- 3. **App**: pass `playgroundEntries` and `playgroundJoinSkips` from the registry into `DashboardLayout`
113
+ - `npx dslinter init` → `src/playground/buildRegistry.ts`
114
+ - `npx dslinter init --laravel` `resources/js/playground/buildRegistry.ts`
115
+
116
+ `npx dslinter init` now also scaffolds a starter `.dslint.json` (unless one already exists), including:
117
+ - `include_dirs` (directory scope for discovery)
118
+ - `ignore_globs` (file/directory ignores)
119
+ - `css_entrypoints` (main CSS entry files for token analysis)
94
120
 
95
121
  ```tsx
96
122
  import { useMemo } from "react";
97
123
  import { DashboardLayout, useWorkspaceReport } from "dslinter";
98
- import {
99
- buildPlaygroundEntries,
100
- getPlaygroundJoinSkips,
101
- } from "./playground/buildRegistry";
102
-
103
- const dslinterReport = useWorkspaceReport({
104
- reportUrl: "/dslint-report.json",
105
- watchUrl: "/events",
106
- });
124
+ import { buildPlaygroundEntries } from "./playground/buildRegistry";
107
125
 
126
+ const dslinterReport = useWorkspaceReport({ reportUrl: "/dslint-report.json", watchUrl: "/events" });
108
127
  const playgroundEntries = useMemo(
109
128
  () => buildPlaygroundEntries(dslinterReport.report),
110
129
  [dslinterReport.report],
111
130
  );
112
- const playgroundJoinSkips = useMemo(
113
- () => getPlaygroundJoinSkips(dslinterReport.report),
114
- [dslinterReport.report],
115
- );
116
131
 
117
- <DashboardLayout
118
- playgroundEntries={playgroundEntries}
119
- playgroundJoinSkips={playgroundJoinSkips}
120
- dslinterReport={dslinterReport}
121
- />;
132
+ <DashboardLayout playgroundEntries={playgroundEntries} dslinterReport={dslinterReport} />;
122
133
  ```
123
134
 
124
- In dev, skipped joins are also logged to the console. The inspect pane shows the expected Vite glob key when a preview fails.
135
+ Or use `usePlaygroundFromReport(report)` with `dslinter/playground-modules` when the Vite plugin is active.
125
136
 
126
137
  Run the scanner from the **project root** (`npx dslinter .`) so `playgrounds[].rel_path` matches your repo layout.
127
138
 
@@ -129,8 +140,8 @@ Run the scanner from the **project root** (`npx dslinter .`) so `playgrounds[].r
129
140
 
130
141
  `DashboardLayout` needs:
131
142
 
132
- - **`playgroundEntries`** — from the report’s `playgrounds` list joined to your React modules (see repo `demo/src/playground/buildRegistry.ts`).
133
- - **`playgroundJoinSkips`** (optional) — from `buildPlaygroundEntriesFromReportWithSkips` for richer inspect-pane hints.
143
+ - **`autoPlayground`** (recommended) or **`playgroundEntries`** from a custom registry / `usePlaygroundFromReport`.
144
+ - **`playgroundJoinSkips`** (optional) — auto-filled when using `autoPlayground`.
134
145
  - **`tokenCatalog`** — token wall data (see `demo/src/tokenCatalog.ts`).
135
146
  - **`dslinterReport`** — from `useWorkspaceReport({ reportUrl: "/dslint-report.json", ... })`.
136
147
 
@@ -27,12 +27,26 @@ function maxMtimeInDir(dir, latest = 0) {
27
27
  return latest;
28
28
  }
29
29
 
30
+ /**
31
+ * @param {string} [root]
32
+ * @returns {boolean} true when embed SPA sources exist (monorepo / git checkout).
33
+ */
34
+ export function hasEmbedDashboard(root = packageRoot) {
35
+ return existsSync(join(root, "index.html"));
36
+ }
37
+
30
38
  /**
31
39
  * Rebuild `dashboard-dist/` when embed sources are newer than the bundle (or dist is missing).
40
+ * Published npm installs omit embed sources; use prebuilt `dashboard-dist/` only.
32
41
  * @param {string} root
33
42
  */
34
43
  export function ensureDashboardBuilt(root = packageRoot) {
35
- const distIndex = join(root, "dashboard-dist", "index.html");
44
+ const distDir = join(root, "dashboard-dist");
45
+ if (!hasEmbedDashboard(root)) {
46
+ return dashboardDirIfReady(distDir);
47
+ }
48
+
49
+ const distIndex = join(distDir, "index.html");
36
50
  const force =
37
51
  process.env.DSLINTER_REBUILD_DASHBOARD === "1" ||
38
52
  process.env.DSLINTER_REBUILD_DASHBOARD?.toLowerCase() === "true";
@@ -40,12 +54,9 @@ export function ensureDashboardBuilt(root = packageRoot) {
40
54
  let needsBuild = force || !existsSync(distIndex);
41
55
  if (!needsBuild) {
42
56
  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
- }
57
+ const embedDir = join(root, "embed");
58
+ if (existsSync(embedDir) && maxMtimeInDir(embedDir) > distMtime) {
59
+ needsBuild = true;
49
60
  }
50
61
  const configPath = join(root, "vite.config.ts");
51
62
  if (existsSync(configPath) && statSync(configPath).mtimeMs > distMtime) {
@@ -53,7 +64,7 @@ export function ensureDashboardBuilt(root = packageRoot) {
53
64
  }
54
65
  }
55
66
 
56
- if (!needsBuild) return dashboardDirIfReady(join(root, "dashboard-dist"));
67
+ if (!needsBuild) return dashboardDirIfReady(distDir);
57
68
 
58
69
  process.stderr.write("[dslinter] Building dashboard bundle (dashboard-dist)…\n");
59
70
  const result = spawnSync("npm", ["run", "build:dashboard"], {
@@ -64,12 +75,7 @@ export function ensureDashboardBuilt(root = packageRoot) {
64
75
  if (result.status !== 0) {
65
76
  throw new Error("dslinter: dashboard build failed");
66
77
  }
67
- return dashboardDirIfReady(join(root, "dashboard-dist"));
68
- }
69
-
70
- /** @returns {boolean} */
71
- export function hasEmbedDashboard() {
72
- return existsSync(join(packageRoot, "index.html"));
78
+ return dashboardDirIfReady(distDir);
73
79
  }
74
80
 
75
81
  const VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"];
@@ -0,0 +1,21 @@
1
+ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { ensureDashboardBuilt, hasEmbedDashboard } from "./project-root.mjs";
6
+
7
+ describe("ensureDashboardBuilt (published install layout)", () => {
8
+ it("returns prebuilt dashboard-dist without spawning build when embed sources are absent", () => {
9
+ const root = mkdtempSync(join(tmpdir(), "dslinter-published-"));
10
+ mkdirSync(join(root, "dashboard-dist"), { recursive: true });
11
+ writeFileSync(join(root, "dashboard-dist", "index.html"), "<!doctype html>");
12
+ mkdirSync(join(root, "src"), { recursive: true });
13
+ writeFileSync(join(root, "src", "index.ts"), "export {};\n");
14
+
15
+ expect(hasEmbedDashboard(root)).toBe(false);
16
+
17
+ const dist = ensureDashboardBuilt(root);
18
+ expect(dist).toBeTruthy();
19
+ expect(dist).toContain("dashboard-dist");
20
+ });
21
+ });
@@ -0,0 +1,75 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+
4
+ const CONFIG_NAMES = [".dslint.json", "dslint.json"];
5
+
6
+ /**
7
+ * @param {string} targetDir
8
+ * @returns {string | null}
9
+ */
10
+ export function findDslintConfigPath(targetDir) {
11
+ for (const name of CONFIG_NAMES) {
12
+ const candidate = join(targetDir, name);
13
+ if (existsSync(candidate)) return candidate;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ /**
19
+ * @param {string} targetDir
20
+ * @param {string[]} candidates
21
+ * @returns {string[]}
22
+ */
23
+ function existingPaths(targetDir, candidates) {
24
+ return candidates.filter((rel) => existsSync(join(targetDir, rel)));
25
+ }
26
+
27
+ /**
28
+ * @param {string} targetDir
29
+ * @param {"laravel" | "default"} layout
30
+ */
31
+ function buildStarterConfig(targetDir, layout) {
32
+ const includeCandidates =
33
+ layout === "laravel"
34
+ ? ["resources/js/Components", "resources/js/components", "resources/js"]
35
+ : ["src/components", "src/ui", "src"];
36
+ const cssCandidates =
37
+ layout === "laravel"
38
+ ? ["resources/css/app.css", "src/index.css"]
39
+ : ["src/index.css", "src/styles.css", "src/app.css", "app/globals.css"];
40
+
41
+ const includeDirs = existingPaths(targetDir, includeCandidates);
42
+ const cssEntrypoints = existingPaths(targetDir, cssCandidates);
43
+ const groupPrefix = includeDirs[0];
44
+
45
+ return {
46
+ include_dirs: includeDirs,
47
+ ignore_globs: [],
48
+ css_entrypoints: cssEntrypoints,
49
+ ...(groupPrefix
50
+ ? {
51
+ playground_groups: {
52
+ components: [groupPrefix],
53
+ },
54
+ }
55
+ : {}),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * @param {{ targetDir: string; layout: "laravel" | "default" }} opts
61
+ * @returns {{ created: boolean; path: string; existed: boolean }}
62
+ */
63
+ export function ensureDslintConfig(opts) {
64
+ const targetDir = resolve(opts.targetDir);
65
+ const existing = findDslintConfigPath(targetDir);
66
+ if (existing) {
67
+ return { created: false, path: existing, existed: true };
68
+ }
69
+
70
+ const configPath = join(targetDir, ".dslint.json");
71
+ mkdirSync(targetDir, { recursive: true });
72
+ const payload = buildStarterConfig(targetDir, opts.layout);
73
+ writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`);
74
+ return { created: true, path: configPath, existed: false };
75
+ }
@@ -0,0 +1,33 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { ensureDslintConfig } from "./scaffold-config.mjs";
6
+
7
+ describe("ensureDslintConfig", () => {
8
+ it("writes starter config with detected include/css paths", () => {
9
+ const root = mkdtempSync(join(tmpdir(), "dslinter-scaffold-"));
10
+ mkdirSync(join(root, "src", "components"), { recursive: true });
11
+ writeFileSync(join(root, "src", "index.css"), "@theme { --color-primary: #000; }");
12
+
13
+ const result = ensureDslintConfig({ targetDir: root, layout: "default" });
14
+ expect(result.created).toBe(true);
15
+
16
+ const raw = readFileSync(result.path, "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ expect(parsed.include_dirs).toContain("src/components");
19
+ expect(parsed.css_entrypoints).toContain("src/index.css");
20
+ expect(parsed.ignore_globs).toEqual([]);
21
+ });
22
+
23
+ it("does not overwrite an existing config", () => {
24
+ const root = mkdtempSync(join(tmpdir(), "dslinter-scaffold-existing-"));
25
+ const existing = join(root, ".dslint.json");
26
+ writeFileSync(existing, "{\n \"ignore_globs\": [\"custom/**\"]\n}\n");
27
+
28
+ const result = ensureDslintConfig({ targetDir: root, layout: "default" });
29
+ expect(result.created).toBe(false);
30
+ expect(result.path).toBe(existing);
31
+ expect(readFileSync(existing, "utf8")).toContain("custom/**");
32
+ });
33
+ });
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { resolve } from "node:path";
2
3
  import { defaultReportPath, findViteRoot, resolveViteBin } from "../lib/project-root.mjs";
3
4
  import { runScannerSync } from "../lib/run-scanner.mjs";
4
5
 
@@ -32,9 +33,14 @@ export function runBuildMode({ scanPath, outputPath, scannerArgs }) {
32
33
  process.stderr.write(`dslinter: vite not installed in ${viteRoot}. Run npm install.\n`);
33
34
  process.exit(1);
34
35
  }
36
+ const scanAbs = resolve(scanPath);
35
37
  const child = spawnSync(process.execPath, [viteBin, "build"], {
36
38
  cwd: viteRoot,
37
39
  stdio: "inherit",
40
+ env: {
41
+ ...process.env,
42
+ DSLINT_SCAN_ROOT: scanAbs,
43
+ },
38
44
  });
39
45
  process.exit(child.status === null ? 1 : child.status);
40
46
  }
package/bin/modes/dev.mjs CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import { writeDevBanner } from "../lib/dev-banner.mjs";
13
13
  import { findAvailablePort, warnIfPortBusy } from "../lib/port-check.mjs";
14
14
  import { spawnScanner } from "../lib/run-scanner.mjs";
15
+ import { ensureDslintConfig } from "../lib/scaffold-config.mjs";
15
16
  import { waitForPort } from "../lib/wait-for-port.mjs";
16
17
 
17
18
  const POLL_MS = 150;
@@ -56,6 +57,10 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
56
57
  const port = servePort ?? defaultServePort();
57
58
  const reportPath = defaultReportPath(scanPath, outputPath);
58
59
  const scanAbs = resolve(scanPath);
60
+ const configResult = ensureDslintConfig({ targetDir: scanAbs, layout: "default" });
61
+ if (configResult.created) {
62
+ process.stderr.write(`dslinter: scaffolded ${configResult.path}\n`);
63
+ }
59
64
  const consumerViteRoot = findViteRoot(process.cwd());
60
65
  const embedRoot = getDashboardPackageRoot();
61
66
  const embedViteBin = hasEmbedDashboard() ? resolveViteBin(embedRoot) : null;
@@ -66,6 +71,10 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
66
71
  process.env.DSLINTER_NO_EMBED_VITE?.trim() !== "1";
67
72
 
68
73
  const bundledDist = useEmbedViteDev ? null : resolveBundledDashboardDir();
74
+ const useConsumerViteDev =
75
+ consumerViteRoot != null &&
76
+ bundledDist == null &&
77
+ process.env.DSLINTER_NO_CONSUMER_VITE?.trim() !== "1";
69
78
 
70
79
  const args = [...scannerArgs];
71
80
  const hasServe = args.some((a) => a === "--serve" || a.startsWith("--serve="));
@@ -172,7 +181,7 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
172
181
  return;
173
182
  }
174
183
 
175
- if (consumerViteRoot) {
184
+ if (useConsumerViteDev) {
176
185
  const viteBin = resolveViteBin(consumerViteRoot);
177
186
  if (!viteBin) {
178
187
  process.stderr.write(`dslinter: vite not installed in ${consumerViteRoot}. Run npm install.\n`);
@@ -183,9 +192,23 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
183
192
  const uiPort = await resolveUiPort(5173);
184
193
  const dashboardUrl = `http://localhost:${uiPort}/`;
185
194
 
195
+ const consumerConfig = join(
196
+ getDashboardPackageRoot(),
197
+ "vite",
198
+ "consumer.config.mjs",
199
+ );
186
200
  const vite = spawn(
187
201
  process.execPath,
188
- [viteBin, "--mode", "serve", "--port", String(uiPort), "--strictPort"],
202
+ [
203
+ viteBin,
204
+ "--config",
205
+ consumerConfig,
206
+ "--mode",
207
+ "serve",
208
+ "--port",
209
+ String(uiPort),
210
+ "--strictPort",
211
+ ],
189
212
  {
190
213
  cwd: consumerViteRoot,
191
214
  stdio: "inherit",
@@ -193,6 +216,7 @@ export async function runDevMode({ scanPath, outputPath, scannerArgs, servePort
193
216
  ...process.env,
194
217
  DSLINT_SERVE_PORT: String(port),
195
218
  DSLINT_SCAN_ROOT: scanAbs,
219
+ DSLINT_VITE_ROOT: consumerViteRoot,
196
220
  },
197
221
  },
198
222
  );
@@ -1,6 +1,7 @@
1
1
  import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { ensureDslintConfig } from "../lib/scaffold-config.mjs";
4
5
 
5
6
  const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
6
7
 
@@ -39,62 +40,64 @@ export function runInitMode(opts = {}) {
39
40
  templateName,
40
41
  );
41
42
 
42
- if (existsSync(registryPath)) {
43
- process.stderr.write(
44
- `dslinter init: ${registryPath} already exists — remove it first to re-scaffold.\n`,
45
- );
46
- process.exit(1);
43
+ const configResult = ensureDslintConfig({ targetDir, layout });
44
+ const wroteRegistry = !existsSync(registryPath);
45
+ if (wroteRegistry) {
46
+ mkdirSync(registryDir, { recursive: true });
47
+ copyFileSync(templatePath, registryPath);
47
48
  }
48
49
 
49
- mkdirSync(registryDir, { recursive: true });
50
- copyFileSync(templatePath, registryPath);
51
-
52
50
  const snippetPath = join(registryDir, "vite.dslinter.snippet.ts");
53
51
  const snippetTemplate = join(
54
52
  packageRoot,
55
53
  "templates",
56
54
  "vite.dslinter.snippet.ts",
57
55
  );
58
- if (!existsSync(snippetPath) && existsSync(snippetTemplate)) {
56
+ if (wroteRegistry && !existsSync(snippetPath) && existsSync(snippetTemplate)) {
59
57
  copyFileSync(snippetTemplate, snippetPath);
60
58
  }
61
59
 
62
60
  const appHintPath = join(registryDir, "README.txt");
63
- writeFileSync(
64
- appHintPath,
65
- [
66
- "Wire live component previews in your App:",
67
- "",
68
- " import { useMemo } from 'react';",
69
- " import { DashboardLayout, useWorkspaceReport } from 'dslinter';",
70
- layout === "laravel"
71
- ? " import { buildPlaygroundEntries } from './playground/buildRegistry'; // adjust relative path from your entry file"
72
- : " import { buildPlaygroundEntries } from './playground/buildRegistry';",
73
- "",
74
- " const dslinterReport = useWorkspaceReport({ reportUrl: '/dslint-report.json', watchUrl: '/events' });",
75
- " const playgroundEntries = useMemo(() => buildPlaygroundEntries(dslinterReport.report), [dslinterReport.report]);",
76
- "",
77
- " <DashboardLayout playgroundEntries={playgroundEntries} dslinterReport={dslinterReport} ... />",
78
- "",
79
- "Run the scanner from the repo root: npx dslinter .",
80
- "",
81
- ].join("\n"),
82
- );
61
+ if (wroteRegistry) {
62
+ writeFileSync(
63
+ appHintPath,
64
+ [
65
+ "Recommended (no registry file):",
66
+ "",
67
+ " import { DashboardLayout, useWorkspaceReport } from 'dslinter';",
68
+ " const dslinterReport = useWorkspaceReport({ reportUrl: '/dslint-report.json', watchUrl: '/events' });",
69
+ " <DashboardLayout autoPlayground dslinterReport={dslinterReport} ... />",
70
+ "",
71
+ "Optional — custom glob via this registry:",
72
+ "",
73
+ " import { buildPlaygroundEntries } from './playground/buildRegistry';",
74
+ " <DashboardLayout playgroundEntries={buildPlaygroundEntries(dslinterReport.report)} ... />",
75
+ "",
76
+ "Run the scanner from the repo root: npx dslinter .",
77
+ "",
78
+ ].join("\n"),
79
+ );
80
+ }
83
81
 
84
82
  process.stdout.write(
85
83
  [
86
- "dslinter init: wrote playground registry",
84
+ wroteRegistry
85
+ ? "dslinter init: wrote playground registry"
86
+ : "dslinter init: playground registry already exists (kept)",
87
87
  ` ${registryPath}`,
88
88
  "",
89
+ configResult.created
90
+ ? "dslinter init: wrote config"
91
+ : "dslinter init: config already exists (kept)",
92
+ ` ${configResult.path}`,
93
+ "",
89
94
  `Layout: ${layout}`,
90
95
  "",
91
96
  "Next:",
92
- ` 1. Import buildPlaygroundEntries in your App (see ${registryDir}/README.txt)`,
93
- " 2. Merge vite.dslinter.snippet.ts into vite.config.ts (proxy, react dedupe, optimizeDeps.exclude)",
94
- layout === "laravel"
95
- ? " No extra @ alias remapping is required dslinter UI uses relative imports."
96
- : " No @ alias overrides needed for dslinter internal UI.",
97
- " 3. Run npx dslinter . from the project root",
97
+ ` 1. Use <DashboardLayout autoPlayground /> (see ${registryDir}/README.txt)`,
98
+ " 2. Run npx dslinter . from the project root (merges dslinter/vite automatically)",
99
+ " Or add plugins: [dslinter()] from dslinter/vite for direct vite --mode serve",
100
+ " 3. Optional: use buildRegistry.ts for a narrower glob or control overrides",
98
101
  "",
99
102
  ].join("\n"),
100
103
  );
@@ -0,0 +1,16 @@
1
+ import { existsSync, mkdtempSync, mkdirSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { runInitMode } from "./init.mjs";
6
+
7
+ describe("runInitMode", () => {
8
+ it("scaffolds config and playground registry", () => {
9
+ const root = mkdtempSync(join(tmpdir(), "dslinter-init-"));
10
+ mkdirSync(join(root, "src", "components"), { recursive: true });
11
+ runInitMode({ targetDir: root, argv: [] });
12
+
13
+ expect(existsSync(join(root, ".dslint.json"))).toBe(true);
14
+ expect(existsSync(join(root, "src", "playground", "buildRegistry.ts"))).toBe(true);
15
+ });
16
+ });