dslinter 0.1.13 → 0.2.2

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 (181) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +50 -29
  3. package/bin/dslinter.mjs +26 -5
  4. package/bin/lib/config-hide-component.mjs +44 -0
  5. package/bin/lib/config-hide-component.test.mjs +33 -0
  6. package/bin/lib/constants.mjs +20 -0
  7. package/bin/lib/dev-banner.mjs +16 -51
  8. package/bin/lib/dev-banner.test.mjs +20 -18
  9. package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
  10. package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
  11. package/bin/lib/enrich-report-cli.mjs +14 -0
  12. package/bin/lib/env.mjs +20 -0
  13. package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
  14. package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
  15. package/bin/lib/parse-args.mjs +13 -1
  16. package/bin/lib/parse-args.test.mjs +7 -1
  17. package/bin/lib/paths.mjs +8 -0
  18. package/bin/lib/project-root.mjs +72 -10
  19. package/bin/lib/project-root.test.mjs +32 -1
  20. package/bin/lib/prompt.mjs +31 -0
  21. package/bin/lib/resolve-project.mjs +78 -0
  22. package/bin/lib/resolve-project.test.mjs +74 -0
  23. package/bin/lib/run-scanner.mjs +40 -6
  24. package/bin/lib/scaffold-config.mjs +128 -9
  25. package/bin/lib/scaffold-config.test.mjs +24 -2
  26. package/bin/lib/scan-host.mjs +44 -0
  27. package/bin/lib/scan-host.test.mjs +41 -0
  28. package/bin/lib/setup-readiness.mjs +153 -0
  29. package/bin/lib/setup-readiness.test.mjs +32 -0
  30. package/bin/modes/build.mjs +31 -6
  31. package/bin/modes/dev.mjs +55 -21
  32. package/bin/modes/init.mjs +3 -22
  33. package/bin/modes/init.test.mjs +1 -1
  34. package/bin/modes/mcp.mjs +49 -0
  35. package/bin/modes/report.mjs +29 -4
  36. package/bin/modes/watch.mjs +85 -0
  37. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
  38. package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
  39. package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
  40. package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
  41. package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
  42. package/dashboard-dist/index.html +2 -2
  43. package/index.cjs +53 -52
  44. package/index.d.ts +3 -0
  45. package/package.json +18 -12
  46. package/shared/env.ts +15 -0
  47. package/shared/paths.ts +8 -0
  48. package/shared/reportPath.test.ts +19 -0
  49. package/shared/reportPath.ts +12 -0
  50. package/shared/servePort.ts +16 -0
  51. package/src/components/ComponentInspectPane.tsx +67 -19
  52. package/src/components/ComponentPlaygroundPane.tsx +262 -113
  53. package/src/components/DashboardCommandPalette.tsx +6 -11
  54. package/src/components/GovernancePane.tsx +2 -2
  55. package/src/components/HideFromCatalogButton.tsx +44 -0
  56. package/src/components/OpenInEditorButton.tsx +36 -0
  57. package/src/components/PlaygroundA11yAndCode.tsx +53 -53
  58. package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
  59. package/src/components/PlaygroundControls.tsx +5 -11
  60. package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
  61. package/src/components/PlaygroundUsageCode.tsx +6 -4
  62. package/src/components/PlaygroundVariantMatrix.tsx +101 -34
  63. package/src/components/Section.tsx +5 -2
  64. package/src/components/Sidebar.tsx +131 -46
  65. package/src/components/TruncatedPath.tsx +44 -0
  66. package/src/components/controlApiTable.test.ts +29 -0
  67. package/src/components/controlApiTable.ts +3 -0
  68. package/src/components/playgroundUsageHighlight.ts +14 -3
  69. package/src/components/ui/badge.tsx +1 -1
  70. package/src/components/ui/table.tsx +2 -2
  71. package/src/dashboard/ComponentCatalog.tsx +16 -23
  72. package/src/dashboard/ComponentUsageDetails.tsx +6 -15
  73. package/src/dashboard/DashboardBody.tsx +0 -35
  74. package/src/dashboard/FindingsList.tsx +65 -55
  75. package/src/dashboard/ScannedTokenWall.tsx +3 -3
  76. package/src/dashboard/aggregate.test.ts +74 -0
  77. package/src/dashboard/aggregate.ts +145 -21
  78. package/src/dashboard/catalogVisibility.test.ts +93 -0
  79. package/src/dashboard/catalogVisibility.ts +108 -0
  80. package/src/dashboard/editorLink.test.ts +57 -0
  81. package/src/dashboard/editorLink.ts +71 -0
  82. package/src/dashboard/paths.test.ts +49 -0
  83. package/src/dashboard/paths.ts +51 -3
  84. package/src/dashboard/updateDslintConfig.ts +22 -0
  85. package/src/dashboard/useWorkspaceReport.ts +21 -17
  86. package/src/index.ts +26 -0
  87. package/src/mcp/agent-context.ts +148 -0
  88. package/src/mcp/agent-query.test.ts +89 -0
  89. package/src/mcp/agent-query.ts +373 -0
  90. package/src/mcp/config.ts +53 -0
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/normalize-paths.ts +65 -0
  93. package/src/mcp/report-cache.ts +212 -0
  94. package/src/mcp/rule-catalog.json +156 -0
  95. package/src/mcp/rule-catalog.ts +33 -0
  96. package/src/mcp/schemas.ts +54 -0
  97. package/src/mcp/server.test.ts +44 -0
  98. package/src/mcp/server.ts +343 -0
  99. package/src/mcp/start.ts +29 -0
  100. package/src/mcp/verify-loop.test.ts +49 -0
  101. package/src/mcp/verify-loop.ts +149 -0
  102. package/src/playground/appPreviewTheme.test.ts +148 -0
  103. package/src/playground/appPreviewTheme.ts +137 -0
  104. package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
  105. package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
  106. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
  107. package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
  108. package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
  109. package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
  110. package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
  111. package/src/playground/collectDefinedPlaygrounds.ts +68 -0
  112. package/src/playground/controls.ts +177 -0
  113. package/src/playground/createPlaygroundRegistry.ts +1 -1
  114. package/src/playground/definePlayground.tsx +88 -16
  115. package/src/playground/definePlaygroundFromKit.ts +17 -0
  116. package/src/playground/embedGlobKey.ts +8 -0
  117. package/src/playground/enrichKitControls.test.ts +25 -0
  118. package/src/playground/enrichKitControls.ts +197 -0
  119. package/src/playground/expandPlaygroundControls.test.ts +50 -0
  120. package/src/playground/expandPlaygroundControls.ts +97 -0
  121. package/src/playground/inferKitJsx.test.ts +77 -0
  122. package/src/playground/inferKitJsx.ts +165 -0
  123. package/src/playground/inferKitParams.test.ts +41 -0
  124. package/src/playground/inferKitParams.ts +113 -0
  125. package/src/playground/inferPropTypesFromTs.d.mts +47 -0
  126. package/src/playground/inferPropTypesFromTs.mjs +343 -0
  127. package/src/playground/inferPropTypesFromTs.test.ts +227 -0
  128. package/src/playground/inferPropTypesFromTs.ts +17 -0
  129. package/src/playground/mergePlaygroundEntries.test.ts +32 -0
  130. package/src/playground/mergePlaygroundEntries.ts +28 -0
  131. package/src/playground/playgroundJoin.test.ts +79 -19
  132. package/src/playground/playgroundJoin.ts +47 -22
  133. package/src/playground/playgroundModuleExport.test.ts +42 -0
  134. package/src/playground/playgroundModuleExport.ts +22 -0
  135. package/src/playground/playgroundSpecsKey.ts +8 -0
  136. package/src/playground/propCoerce.ts +91 -0
  137. package/src/playground/scanVariantA11y.test.ts +46 -0
  138. package/src/playground/scanVariantA11y.ts +107 -0
  139. package/src/playground/snippet.ts +83 -0
  140. package/src/playground/usePlaygroundFromReport.test.ts +18 -8
  141. package/src/playground/usePlaygroundFromReport.ts +3 -1
  142. package/src/report/a11yForModule.ts +2 -7
  143. package/src/report/a11yScoring.test.ts +24 -0
  144. package/src/report/a11yScoring.ts +17 -0
  145. package/src/report/index.ts +6 -0
  146. package/src/shell/DashboardLayout.tsx +71 -45
  147. package/src/shell/DashboardLayoutAuto.tsx +0 -4
  148. package/src/shell/hashRoute.test.ts +7 -15
  149. package/src/shell/hashRoute.ts +31 -31
  150. package/src/shell/useHashRoute.ts +38 -13
  151. package/src/styles/dashboard-theme.css +18 -7
  152. package/src/types/controls.ts +11 -0
  153. package/src/types/playground.ts +4 -0
  154. package/src/types/report.ts +32 -9
  155. package/templates/playground/buildRegistry.ts +1 -1
  156. package/templates/vite.dslinter.snippet.ts +15 -4
  157. package/vite/collectScanModules.test.ts +91 -3
  158. package/vite/collectScanModules.ts +94 -29
  159. package/vite/consumer.config.mjs +6 -3
  160. package/vite/consumerAlias.test.ts +47 -0
  161. package/vite/consumerAlias.ts +114 -0
  162. package/vite/embedTailwindSources.test.ts +74 -0
  163. package/vite/embedTailwindSources.ts +97 -0
  164. package/vite/loadConsumerAliases.test.ts +131 -0
  165. package/vite/loadConsumerAliases.ts +155 -0
  166. package/vite/openFileInEditor.mjs +196 -0
  167. package/vite/openFileInEditor.test.mjs +87 -0
  168. package/vite/plugin.resolve.test.ts +72 -0
  169. package/vite/plugin.ts +216 -19
  170. package/vite/reportPath.test.ts +19 -0
  171. package/vite/resolveWayfinderImport.ts +56 -0
  172. package/vite/shims/inertia-react.tsx +85 -0
  173. package/vite/shims/wayfinder-actions.ts +33 -0
  174. package/vite/shims/wayfinder-routes.ts +30 -0
  175. package/vite/shims/ziggy-js.ts +12 -0
  176. package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
  178. package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
  179. package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
@@ -0,0 +1,72 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createServer } from "vite";
5
+ import { describe, expect, it } from "vitest";
6
+ import { loadConsumerAliases } from "./loadConsumerAliases";
7
+ import { collectScanModuleRelPaths } from "./collectScanModules";
8
+ import { resolveExistingModule } from "./resolveWayfinderImport";
9
+ import dslinter from "./plugin";
10
+
11
+ const packageRoot = resolve(
12
+ fileURLToPath(new URL(".", import.meta.url)),
13
+ "..",
14
+ );
15
+ const demoInertiaRoot = resolve(packageRoot, "../../demo-inertia");
16
+ const navFooter = join(
17
+ demoInertiaRoot,
18
+ "resources/js/components/nav-footer.tsx",
19
+ );
20
+
21
+ describe("dslinter vite plugin resolveId", () => {
22
+ it("has scan paths and consumer aliases for demo-inertia", () => {
23
+ const relPaths = collectScanModuleRelPaths(demoInertiaRoot);
24
+ expect(relPaths).toContain("resources/js/components/nav-footer.tsx");
25
+ const aliases = loadConsumerAliases(demoInertiaRoot, undefined);
26
+ const file = resolveExistingModule("@/components/ui/sidebar", aliases);
27
+ expect(file?.replace(/\\/g, "/")).toContain(
28
+ "demo-inertia/resources/js/components/ui/sidebar.tsx",
29
+ );
30
+ });
31
+
32
+ it("resolveId hook returns consumer file for playground importer", async () => {
33
+ const plugin = dslinter({
34
+ scanRoot: demoInertiaRoot,
35
+ consumerViteRoot: demoInertiaRoot,
36
+ });
37
+ const resolved = await plugin.resolveId?.(
38
+ "@/components/ui/sidebar",
39
+ navFooter,
40
+ { ssr: false },
41
+ );
42
+ expect(resolved?.replace(/\\/g, "/")).toContain(
43
+ "demo-inertia/resources/js/components/ui/sidebar.tsx",
44
+ );
45
+ });
46
+
47
+ it("resolves @/ imports via pluginContainer", async () => {
48
+ const server = await createServer({
49
+ root: packageRoot,
50
+ plugins: [
51
+ dslinter({
52
+ scanRoot: demoInertiaRoot,
53
+ consumerViteRoot: demoInertiaRoot,
54
+ }),
55
+ ],
56
+ server: { fs: { allow: [packageRoot, demoInertiaRoot] } },
57
+ });
58
+
59
+ try {
60
+ const resolved = await server.pluginContainer.resolveId(
61
+ "@/components/ui/sidebar",
62
+ navFooter,
63
+ );
64
+ expect(resolved?.id.replace(/\\/g, "/")).toContain(
65
+ "demo-inertia/resources/js/components/ui/sidebar",
66
+ );
67
+ expect(existsSync(resolved!.id.split("?")[0]!)).toBe(true);
68
+ } finally {
69
+ await server.close();
70
+ }
71
+ });
72
+ });
package/vite/plugin.ts CHANGED
@@ -1,29 +1,58 @@
1
- import { resolve } from "node:path";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { hideComponentInDslintConfig } from "../bin/lib/config-hide-component.mjs";
5
+ import { openFileInEditor } from "./openFileInEditor.mjs";
6
+ import { loadConfigFromFile } from "vite";
2
7
  import type { Plugin, UserConfig } from "vite";
8
+ import { resolveServePort } from "../shared/servePort";
9
+ import {
10
+ REPORT_URL_PATH,
11
+ } from "../shared/paths";
12
+ import { resolveReportFilePath } from "../shared/reportPath";
3
13
  import {
4
14
  collectScanModuleRelPaths,
5
15
  embedGlobKeyFromRelPath,
6
16
  } from "./collectScanModules";
17
+ import {
18
+ buildEmbedIndexCss,
19
+ embedSourcePathsRelativeToCss,
20
+ shouldInjectEmbedConsumerSources,
21
+ } from "./embedTailwindSources";
22
+ import {
23
+ importerUnderScanRoot,
24
+ INERTIA_SHIM_IDS,
25
+ ZIGGY_SHIM_ID,
26
+ type FlatAlias,
27
+ } from "./consumerAlias";
28
+ import { loadConsumerAliases } from "./loadConsumerAliases";
29
+ import {
30
+ isWayfinderActionsImport,
31
+ isWayfinderRoutesImport,
32
+ resolveExistingModule,
33
+ resolveWayfinderShim,
34
+ } from "./resolveWayfinderImport";
7
35
 
8
36
  export const VIRTUAL_PLAYGROUND_MODULES_ID = "virtual:dslinter/playground-modules";
9
37
  const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_PLAYGROUND_MODULES_ID}`;
10
38
 
39
+ const pluginDir = dirname(fileURLToPath(import.meta.url));
40
+ const packageRoot = resolve(pluginDir, "..");
41
+ const embedIndexCssPath = resolve(packageRoot, "embed", "index.css");
42
+ const inertiaShimPath = resolve(pluginDir, "shims/inertia-react.tsx");
43
+ const ziggyShimPath = resolve(pluginDir, "shims/ziggy-js.ts");
44
+ const wayfinderRoutesShimPath = resolve(pluginDir, "shims/wayfinder-routes.ts");
45
+ const wayfinderActionsShimPath = resolve(pluginDir, "shims/wayfinder-actions.ts");
46
+
11
47
  export type DslinterVitePluginOptions = {
12
- /** Scan root (repo root passed to `npx dslinter`). Defaults to `DSLINT_SCAN_ROOT` or `process.cwd()`. */
48
+ /** Scan root (repo root passed to `npx dslinter`). Defaults to `DSLINTER_SCAN_ROOT` or `process.cwd()`. */
13
49
  scanRoot?: string;
50
+ /** Host Vite project root for `@/` aliases (Laravel/Inertia). */
51
+ consumerViteRoot?: string;
14
52
  /** Scanner HTTP port for report + SSE proxy in `serve` mode. */
15
53
  servePort?: number;
16
54
  };
17
55
 
18
- function defaultServePort(): number {
19
- const fromEnv = process.env.DSLINT_SERVE_PORT?.trim();
20
- if (fromEnv) {
21
- const n = Number.parseInt(fromEnv, 10);
22
- if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
23
- }
24
- return 7878;
25
- }
26
-
27
56
  function generatePlaygroundModulesSource(
28
57
  scanRoot: string,
29
58
  relPaths: string[],
@@ -63,21 +92,34 @@ export default function dslinter(
63
92
  ): Plugin {
64
93
  const scanRoot = resolve(
65
94
  options.scanRoot ??
66
- process.env.DSLINT_SCAN_ROOT ??
95
+ process.env.DSLINTER_SCAN_ROOT ??
67
96
  process.cwd(),
68
97
  );
69
- const servePort = options.servePort ?? defaultServePort();
70
- let relPaths: string[] = [];
98
+ const consumerViteRoot = resolve(
99
+ options.consumerViteRoot ??
100
+ process.env.DSLINTER_CONSUMER_VITE_ROOT ??
101
+ "",
102
+ );
103
+ const servePort = options.servePort ?? resolveServePort();
104
+ const consumerRoot =
105
+ consumerViteRoot && existsSync(consumerViteRoot) ? consumerViteRoot : null;
106
+ /** Populated synchronously so resolveId works before async configResolved. */
107
+ let relPaths = collectScanModuleRelPaths(scanRoot);
108
+ let consumerAliases: FlatAlias[] = consumerRoot
109
+ ? loadConsumerAliases(consumerRoot, undefined)
110
+ : [];
71
111
 
72
112
  return {
73
113
  name: "dslinter",
74
114
  enforce: "pre",
115
+ /** Run before vite:alias so consumer @/ imports are not rewritten to embed src. */
116
+ order: "pre",
75
117
 
76
118
  config(config, { mode }): UserConfig {
77
119
  const proxy =
78
120
  mode === "serve"
79
121
  ? {
80
- "/dslint-report.json": {
122
+ [REPORT_URL_PATH]: {
81
123
  target: `http://127.0.0.1:${servePort}`,
82
124
  changeOrigin: true,
83
125
  },
@@ -88,10 +130,14 @@ export default function dslinter(
88
130
  }
89
131
  : undefined;
90
132
 
133
+ const allowRoots = [scanRoot];
134
+ if (consumerViteRoot && existsSync(consumerViteRoot)) {
135
+ allowRoots.push(consumerViteRoot);
136
+ }
91
137
  const existingAllow = config.server?.fs?.allow;
92
138
  const fsAllow = Array.isArray(existingAllow)
93
- ? [...existingAllow, scanRoot]
94
- : [scanRoot];
139
+ ? [...existingAllow, ...allowRoots]
140
+ : allowRoots;
95
141
 
96
142
  return {
97
143
  resolve: {
@@ -109,14 +155,63 @@ export default function dslinter(
109
155
  };
110
156
  },
111
157
 
112
- configResolved() {
158
+ async configResolved(config) {
113
159
  relPaths = collectScanModuleRelPaths(scanRoot);
160
+ if (!consumerRoot) {
161
+ consumerAliases = [];
162
+ return;
163
+ }
164
+ try {
165
+ const loaded = await loadConfigFromFile(
166
+ { command: config.command, mode: config.mode },
167
+ undefined,
168
+ consumerRoot,
169
+ );
170
+ consumerAliases = loadConsumerAliases(
171
+ consumerRoot,
172
+ loaded?.config?.resolve?.alias,
173
+ );
174
+ } catch {
175
+ consumerAliases = loadConsumerAliases(consumerRoot, undefined);
176
+ }
114
177
  },
115
178
 
116
- resolveId(id) {
179
+ resolveId(id, importer) {
117
180
  if (id === VIRTUAL_PLAYGROUND_MODULES_ID) {
118
181
  return RESOLVED_VIRTUAL_ID;
119
182
  }
183
+
184
+ if (!importer || !importerUnderScanRoot(importer, scanRoot)) {
185
+ return null;
186
+ }
187
+
188
+ if (INERTIA_SHIM_IDS.has(id) || id.startsWith("@inertiajs/react/")) {
189
+ return inertiaShimPath;
190
+ }
191
+ if (id === ZIGGY_SHIM_ID || id === "ziggy") {
192
+ return ziggyShimPath;
193
+ }
194
+
195
+ if (
196
+ isWayfinderRoutesImport(id) ||
197
+ isWayfinderActionsImport(id)
198
+ ) {
199
+ const onDisk = resolveExistingModule(id, consumerAliases);
200
+ if (onDisk) return onDisk;
201
+ const shim = resolveWayfinderShim(
202
+ id,
203
+ wayfinderRoutesShimPath,
204
+ wayfinderActionsShimPath,
205
+ );
206
+ if (shim) return shim;
207
+ }
208
+
209
+ if (consumerAliases.length > 0) {
210
+ const onDisk = resolveExistingModule(id, consumerAliases);
211
+ if (onDisk) return onDisk;
212
+ }
213
+
214
+ return null;
120
215
  },
121
216
 
122
217
  load(id) {
@@ -124,7 +219,109 @@ export default function dslinter(
124
219
  return generatePlaygroundModulesSource(scanRoot, relPaths);
125
220
  },
126
221
 
222
+ transform(code, id) {
223
+ const normalizedId = id.split("?")[0]!.replace(/\\/g, "/");
224
+ if (!normalizedId.endsWith("/embed/index.css")) return;
225
+
226
+ if (!shouldInjectEmbedConsumerSources(scanRoot, packageRoot)) {
227
+ return null;
228
+ }
229
+
230
+ const consumerSources = embedSourcePathsRelativeToCss(
231
+ scanRoot,
232
+ packageRoot,
233
+ embedIndexCssPath,
234
+ );
235
+ if (consumerSources.length === 0) return null;
236
+
237
+ return {
238
+ code: buildEmbedIndexCss(code, consumerSources),
239
+ map: null,
240
+ };
241
+ },
242
+
127
243
  configureServer(server) {
244
+ const reportFile = resolveReportFilePath(scanRoot);
245
+ const configHidePath = "/dslinter-config/hide-component";
246
+ const openFilePath = "/dslinter/open-file";
247
+
248
+ server.middlewares.use(async (req, res, next) => {
249
+ const url = new URL(req.url ?? "/", "http://localhost");
250
+ const path = url.pathname;
251
+ if (path === openFilePath && (req.method === "POST" || req.method === "GET")) {
252
+ try {
253
+ const file = url.searchParams.get("path")?.trim();
254
+ if (!file) {
255
+ throw new Error("Missing path query parameter");
256
+ }
257
+ const line = Number(url.searchParams.get("line") ?? "1");
258
+ const column = Number(url.searchParams.get("column") ?? "1");
259
+ openFileInEditor({
260
+ file,
261
+ line: Number.isFinite(line) ? line : 1,
262
+ column: Number.isFinite(column) ? column : 1,
263
+ scanRoot,
264
+ });
265
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
266
+ res.statusCode = 200;
267
+ res.end(JSON.stringify({ ok: true }));
268
+ } catch (err) {
269
+ const message = err instanceof Error ? err.message : String(err);
270
+ res.statusCode = 400;
271
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
272
+ res.end(JSON.stringify({ ok: false, error: message }));
273
+ }
274
+ return;
275
+ }
276
+ if (path === configHidePath && req.method === "POST") {
277
+ try {
278
+ const chunks: Buffer[] = [];
279
+ for await (const chunk of req) {
280
+ chunks.push(chunk as Buffer);
281
+ }
282
+ const raw = Buffer.concat(chunks).toString("utf8");
283
+ const body = JSON.parse(raw || "{}") as { name?: string };
284
+ const result = hideComponentInDslintConfig(scanRoot, body.name ?? "");
285
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
286
+ res.statusCode = 200;
287
+ res.end(JSON.stringify({ ok: true, ...result }));
288
+ } catch (err) {
289
+ const message = err instanceof Error ? err.message : String(err);
290
+ res.statusCode = 400;
291
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
292
+ res.end(JSON.stringify({ ok: false, error: message }));
293
+ }
294
+ return;
295
+ }
296
+ if (path !== REPORT_URL_PATH) {
297
+ next();
298
+ return;
299
+ }
300
+ if (!existsSync(reportFile)) {
301
+ next();
302
+ return;
303
+ }
304
+ try {
305
+ const stat = statSync(reportFile);
306
+ const etag = `"${stat.mtimeMs}"`;
307
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
308
+ res.setHeader("Cache-Control", "no-store");
309
+ res.setHeader("ETag", etag);
310
+ if (req.method === "HEAD") {
311
+ res.statusCode = 200;
312
+ res.end();
313
+ return;
314
+ }
315
+ if (req.method === "GET") {
316
+ res.end(readFileSync(reportFile));
317
+ return;
318
+ }
319
+ } catch {
320
+ // fall through to proxy / 404
321
+ }
322
+ next();
323
+ });
324
+
128
325
  const refresh = () => {
129
326
  relPaths = collectScanModuleRelPaths(scanRoot);
130
327
  const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ID);
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { join, resolve } from "node:path";
3
+ import { resolveReportFilePath } from "../shared/reportPath";
4
+
5
+ describe("resolveReportFilePath", () => {
6
+ it("defaults to public/dslinter-report.json under scan root", () => {
7
+ const root = "/app/demo-inertia";
8
+ expect(resolveReportFilePath(root, {})).toBe(
9
+ resolve(root, "public", "dslinter-report.json"),
10
+ );
11
+ });
12
+
13
+ it("uses DSLINTER_REPORT_PATH when set", () => {
14
+ const custom = join("/tmp", "custom-report.json");
15
+ expect(
16
+ resolveReportFilePath("/app", { DSLINTER_REPORT_PATH: custom }),
17
+ ).toBe(resolve(custom));
18
+ });
19
+ });
@@ -0,0 +1,56 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveWithConsumerAliases, type FlatAlias } from "./consumerAlias";
4
+
5
+ const FILE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"] as const;
6
+
7
+ export const WAYFINDER_ROUTES_PREFIX = "@/routes";
8
+ export const WAYFINDER_ACTIONS_PREFIX = "@/actions";
9
+
10
+ export function isWayfinderRoutesImport(id: string): boolean {
11
+ return id === WAYFINDER_ROUTES_PREFIX || id.startsWith(`${WAYFINDER_ROUTES_PREFIX}/`);
12
+ }
13
+
14
+ export function isWayfinderActionsImport(id: string): boolean {
15
+ return id === WAYFINDER_ACTIONS_PREFIX || id.startsWith(`${WAYFINDER_ACTIONS_PREFIX}/`);
16
+ }
17
+
18
+ /** Resolve import to an existing file on disk, trying common extensions. */
19
+ export function resolveExistingModule(
20
+ id: string,
21
+ aliases: FlatAlias[],
22
+ ): string | null {
23
+ const base = resolveWithConsumerAliases(id, aliases);
24
+ if (!base) return null;
25
+
26
+ if (existsSync(base)) {
27
+ try {
28
+ if (!statSync(base).isDirectory()) return base;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ for (const ext of FILE_EXTENSIONS) {
35
+ const candidate = `${base}${ext}`;
36
+ if (existsSync(candidate)) return candidate;
37
+ }
38
+
39
+ for (const ext of FILE_EXTENSIONS) {
40
+ const candidate = join(base, `index${ext}`);
41
+ if (existsSync(candidate)) return candidate;
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ /** When Wayfinder output is missing, map @/routes/* and @/actions/* to dslinter shims. */
48
+ export function resolveWayfinderShim(
49
+ id: string,
50
+ routesShimPath: string,
51
+ actionsShimPath: string,
52
+ ): string | null {
53
+ if (isWayfinderRoutesImport(id)) return routesShimPath;
54
+ if (isWayfinderActionsImport(id)) return actionsShimPath;
55
+ return null;
56
+ }
@@ -0,0 +1,85 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /** Minimal Inertia stubs for dslinter component previews (no Laravel backend). */
4
+
5
+ /** No-op entry bootstrap when `app.tsx` is pulled into the playground graph. */
6
+ export function createInertiaApp(_options: Record<string, unknown>) {
7
+ return undefined;
8
+ }
9
+
10
+ /** No-op layout prop helper used by some auth pages. */
11
+ export function setLayoutProps(_props: Record<string, unknown>) {
12
+ return undefined;
13
+ }
14
+
15
+ const emptyPage = {
16
+ component: "Preview",
17
+ props: {} as Record<string, unknown>,
18
+ url: "/",
19
+ version: null as string | null,
20
+ clearHistory: false,
21
+ encryptHistory: false,
22
+ rememberedState: {} as Record<string, unknown>,
23
+ scrollRegions: [] as unknown[],
24
+ };
25
+
26
+ export function usePage<T extends Record<string, unknown> = Record<string, unknown>>() {
27
+ return {
28
+ ...emptyPage,
29
+ props: {} as T,
30
+ };
31
+ }
32
+
33
+ export function Link({
34
+ href = "#",
35
+ children,
36
+ ...rest
37
+ }: {
38
+ href?: string;
39
+ children?: ReactNode;
40
+ [key: string]: unknown;
41
+ }) {
42
+ return (
43
+ <a href={href} {...rest}>
44
+ {children}
45
+ </a>
46
+ );
47
+ }
48
+
49
+ export function Head({ children }: { children?: ReactNode }) {
50
+ return <>{children}</>;
51
+ }
52
+
53
+ /** Stub for components that use Inertia `<Form>` in playground previews. */
54
+ export function Form({
55
+ children,
56
+ ...rest
57
+ }: {
58
+ children?: ReactNode;
59
+ [key: string]: unknown;
60
+ }) {
61
+ return <form {...rest}>{children}</form>;
62
+ }
63
+
64
+ export const router = {
65
+ visit: () => undefined,
66
+ get: () => undefined,
67
+ post: () => undefined,
68
+ put: () => undefined,
69
+ patch: () => undefined,
70
+ delete: () => undefined,
71
+ reload: () => undefined,
72
+ replace: () => undefined,
73
+ };
74
+
75
+ /** Stub for hooks that fetch JSON via Inertia HTTP helpers in playground previews. */
76
+ export function useHttp() {
77
+ return {
78
+ submit: async (url?: unknown) => {
79
+ const path = String(url ?? "");
80
+ if (path.includes("recovery")) return [];
81
+ if (path.includes("secret")) return { secretKey: "" };
82
+ return { svg: "", url: "" };
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Minimal Wayfinder action stubs for dslinter component previews (no Laravel backend).
3
+ */
4
+
5
+ type ActionMethod = {
6
+ url: (id?: number | string) => string;
7
+ form: (options?: Record<string, unknown>) => Record<string, unknown>;
8
+ };
9
+
10
+ function actionMethod(path = "#"): ActionMethod {
11
+ return {
12
+ url: () => path,
13
+ form: () => ({ action: path, method: "post" }),
14
+ };
15
+ }
16
+
17
+ const controllerHandler: ProxyHandler<Record<string, ActionMethod>> = {
18
+ get(_target, prop) {
19
+ if (typeof prop !== "string") return undefined;
20
+ return actionMethod(`/${prop}`);
21
+ },
22
+ };
23
+
24
+ const controllerStub = new Proxy(
25
+ {} as Record<string, ActionMethod>,
26
+ controllerHandler,
27
+ );
28
+
29
+ /** Default export used as `ProfileController.destroy.form()`. */
30
+ export default controllerStub;
31
+
32
+ /** Named export used as `destroy.url(id)`. */
33
+ export const destroy = actionMethod("/passkeys");
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Minimal Wayfinder route stubs for dslinter component previews (no Laravel backend).
3
+ */
4
+
5
+ type RouteHelper = {
6
+ url: (params?: Record<string, unknown>) => string;
7
+ form: (options?: Record<string, unknown>) => Record<string, unknown>;
8
+ };
9
+
10
+ function routeHelper(path = "#"): RouteHelper {
11
+ return {
12
+ url: () => path,
13
+ form: () => ({ action: path, method: "post" }),
14
+ };
15
+ }
16
+
17
+ export const dashboard = routeHelper("/dashboard");
18
+ export const logout = routeHelper("/logout");
19
+ export const home = routeHelper("/");
20
+ export const login = routeHelper("/login");
21
+ export const register = routeHelper("/register");
22
+
23
+ export const edit = routeHelper("/profile");
24
+ export const confirm = routeHelper("/two-factor/confirm");
25
+ export const enable = routeHelper("/two-factor/enable");
26
+ export const disable = routeHelper("/two-factor/disable");
27
+ export const regenerateRecoveryCodes = routeHelper("/two-factor/recovery-codes");
28
+ export const qrCode = routeHelper("/two-factor/qr-code");
29
+ export const recoveryCodes = routeHelper("/two-factor/recovery-codes");
30
+ export const secretKey = routeHelper("/two-factor/secret-key");
@@ -0,0 +1,12 @@
1
+ /** Stub for ziggy-js in dslinter previews. */
2
+ export default function route(
3
+ _name?: string,
4
+ _params?: Record<string, unknown>,
5
+ _absolute?: boolean,
6
+ ): string {
7
+ return "#";
8
+ }
9
+
10
+ export function routeFn(...args: Parameters<typeof route>): string {
11
+ return route(...args);
12
+ }