dslinter 0.1.5 → 0.2.0
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 +112 -0
- package/README.md +54 -27
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +92 -24
- package/bin/lib/project-root.test.mjs +52 -0
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +163 -0
- package/bin/lib/scaffold-config.test.mjs +43 -0
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +56 -13
- package/bin/modes/init.mjs +35 -47
- package/bin/modes/init.test.mjs +16 -0
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +209 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +51 -3
- package/vite/collectScanModules.ts +85 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-BPPtPsYh.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-Dp3bAQxH.js +0 -1
- package/dashboard-dist/assets/index-DsjwnDdX.js +0 -206
- package/dashboard-dist/assets/index-jaCmZJlW.css +0 -1
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- 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 {
|
|
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 `
|
|
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.
|
|
95
|
+
process.env.DSLINTER_SCAN_ROOT ??
|
|
67
96
|
process.cwd(),
|
|
68
97
|
);
|
|
69
|
-
const
|
|
70
|
-
|
|
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
|
-
|
|
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,
|
|
94
|
-
:
|
|
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
|
+
}
|