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.
- package/CHANGELOG.md +72 -0
- package/README.md +50 -29
- 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 +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- 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 +128 -9
- package/bin/lib/scaffold-config.test.mjs +24 -2
- 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 +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- 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 +212 -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 +91 -3
- package/vite/collectScanModules.ts +94 -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-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
const reactTypesRoot = join(packageRoot, "node_modules/@types");
|
|
9
|
+
import {
|
|
10
|
+
classifyPropType,
|
|
11
|
+
createCheckerProgram,
|
|
12
|
+
extractFiniteStringUnion,
|
|
13
|
+
findComponentParamType,
|
|
14
|
+
inferPlaygroundPropMetadata,
|
|
15
|
+
} from "./infer-prop-types-from-ts.mjs";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} root
|
|
19
|
+
* @param {Record<string, string>} files
|
|
20
|
+
*/
|
|
21
|
+
function writeProject(root, files) {
|
|
22
|
+
mkdirSync(root, { recursive: true });
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(root, "tsconfig.json"),
|
|
25
|
+
JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
compilerOptions: {
|
|
28
|
+
strict: true,
|
|
29
|
+
jsx: "react-jsx",
|
|
30
|
+
module: "ESNext",
|
|
31
|
+
moduleResolution: "bundler",
|
|
32
|
+
noEmit: true,
|
|
33
|
+
skipLibCheck: true,
|
|
34
|
+
typeRoots: [reactTypesRoot],
|
|
35
|
+
},
|
|
36
|
+
include: ["**/*.tsx"],
|
|
37
|
+
},
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
43
|
+
const abs = join(root, rel);
|
|
44
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
45
|
+
writeFileSync(abs, content);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("infer-prop-types-from-ts", () => {
|
|
50
|
+
it("extracts finite string union literals", () => {
|
|
51
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ts-"));
|
|
52
|
+
try {
|
|
53
|
+
writeProject(root, {
|
|
54
|
+
"Union.tsx": `
|
|
55
|
+
export function Union({ mode }: { mode: "text" | "email" | "password" }) {
|
|
56
|
+
return <input type={mode} />;
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
});
|
|
60
|
+
const bundle = createCheckerProgram(root);
|
|
61
|
+
expect(bundle).not.toBeNull();
|
|
62
|
+
const sf = bundle.program.getSourceFile(join(root, "Union.tsx"));
|
|
63
|
+
expect(sf).toBeDefined();
|
|
64
|
+
const paramType = findComponentParamType(bundle.checker, sf, "Union");
|
|
65
|
+
expect(paramType).toBeDefined();
|
|
66
|
+
const sym = bundle.checker.getPropertyOfType(paramType, "mode");
|
|
67
|
+
expect(sym).toBeDefined();
|
|
68
|
+
const propType = bundle.checker.getTypeOfSymbol(sym);
|
|
69
|
+
expect(extractFiniteStringUnion(bundle.checker, propType)).toEqual([
|
|
70
|
+
"email",
|
|
71
|
+
"password",
|
|
72
|
+
"text",
|
|
73
|
+
]);
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects unions that include plain string", () => {
|
|
80
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ts-"));
|
|
81
|
+
try {
|
|
82
|
+
writeProject(root, {
|
|
83
|
+
"Wide.tsx": `
|
|
84
|
+
export function Wide({ label }: { label: string | "foo" }) {
|
|
85
|
+
return <span>{label}</span>;
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
88
|
+
});
|
|
89
|
+
const bundle = createCheckerProgram(root);
|
|
90
|
+
const sf = bundle.program.getSourceFile(join(root, "Wide.tsx"));
|
|
91
|
+
const paramType = findComponentParamType(bundle.checker, sf, "Wide");
|
|
92
|
+
const sym = bundle.checker.getPropertyOfType(paramType, "label");
|
|
93
|
+
const propType = bundle.checker.getTypeOfSymbol(sym);
|
|
94
|
+
expect(extractFiniteStringUnion(bundle.checker, propType)).toBeNull();
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("finds param type for function + export { Name }", () => {
|
|
101
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ts-"));
|
|
102
|
+
try {
|
|
103
|
+
writeProject(root, {
|
|
104
|
+
"Input.tsx": `
|
|
105
|
+
function Input({ type }: { type?: "text" | "password" }) {
|
|
106
|
+
return <input type={type} />;
|
|
107
|
+
}
|
|
108
|
+
export { Input };
|
|
109
|
+
`,
|
|
110
|
+
});
|
|
111
|
+
const bundle = createCheckerProgram(root);
|
|
112
|
+
const sf = bundle.program.getSourceFile(join(root, "Input.tsx"));
|
|
113
|
+
expect(findComponentParamType(bundle.checker, sf, "Input")).toBeDefined();
|
|
114
|
+
} finally {
|
|
115
|
+
rmSync(root, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("infers type select options for ComponentProps input wrapper", () => {
|
|
120
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ts-"));
|
|
121
|
+
try {
|
|
122
|
+
writeProject(root, {
|
|
123
|
+
"input.tsx": `
|
|
124
|
+
import * as React from "react";
|
|
125
|
+
|
|
126
|
+
function Input({ className, type, placeholder, ...props }: React.ComponentProps<"input">) {
|
|
127
|
+
return <input type={type} placeholder={placeholder} className={className} {...props} />;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { Input };
|
|
131
|
+
`,
|
|
132
|
+
});
|
|
133
|
+
const bundle = createCheckerProgram(root);
|
|
134
|
+
const meta = inferPlaygroundPropMetadata(
|
|
135
|
+
bundle.checker,
|
|
136
|
+
bundle.program,
|
|
137
|
+
root,
|
|
138
|
+
"input.tsx",
|
|
139
|
+
"Input",
|
|
140
|
+
["className", "type", "placeholder"],
|
|
141
|
+
);
|
|
142
|
+
expect(meta.declared_prop_kinds.type).toBe("string");
|
|
143
|
+
expect(meta.declared_prop_options.type).toBeDefined();
|
|
144
|
+
expect(meta.declared_prop_options.type.length).toBeGreaterThanOrEqual(2);
|
|
145
|
+
expect(meta.declared_prop_options.type).toContain("text");
|
|
146
|
+
expect(meta.declared_prop_options.type).toContain("password");
|
|
147
|
+
expect(meta.declared_prop_defaults.type).toBe("text");
|
|
148
|
+
} finally {
|
|
149
|
+
rmSync(root, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("classifies boolean props", () => {
|
|
154
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ts-"));
|
|
155
|
+
try {
|
|
156
|
+
writeProject(root, {
|
|
157
|
+
"Toggle.tsx": `
|
|
158
|
+
export function Toggle({ disabled }: { disabled?: boolean }) {
|
|
159
|
+
return <button disabled={disabled} />;
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
});
|
|
163
|
+
const bundle = createCheckerProgram(root);
|
|
164
|
+
const sf = bundle.program.getSourceFile(join(root, "Toggle.tsx"));
|
|
165
|
+
const paramType = findComponentParamType(bundle.checker, sf, "Toggle");
|
|
166
|
+
const sym = bundle.checker.getPropertyOfType(paramType, "disabled");
|
|
167
|
+
expect(classifyPropType(bundle.checker, bundle.checker.getTypeOfSymbol(sym))).toBe(
|
|
168
|
+
"boolean",
|
|
169
|
+
);
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(root, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
package/bin/lib/parse-args.mjs
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Parse dslinter CLI argv into mode + scanner args (no subprocess).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { resolveScanAndProjectRoots } from "./resolve-project.mjs";
|
|
6
|
+
|
|
5
7
|
const MODE_FLAGS = new Set(["--report", "--watch", "--build"]);
|
|
8
|
+
const YES_FLAGS = new Set(["--yes", "-y"]);
|
|
6
9
|
|
|
7
10
|
/** @param {string | undefined} raw */
|
|
8
11
|
function parseServePort(raw) {
|
|
@@ -17,16 +20,21 @@ function parseServePort(raw) {
|
|
|
17
20
|
* mode: "dev" | "report" | "watch" | "build" | "scanner";
|
|
18
21
|
* scannerArgs: string[];
|
|
19
22
|
* scanPath: string;
|
|
23
|
+
* projectRoot: string;
|
|
20
24
|
* outputPath: string | null;
|
|
21
25
|
* servePort: number | null;
|
|
26
|
+
* yes: boolean;
|
|
27
|
+
* explicitScanPath: string | null;
|
|
22
28
|
* }}
|
|
23
29
|
*/
|
|
24
30
|
export function parseDslinterArgs(argv) {
|
|
25
31
|
const modes = [];
|
|
26
32
|
const scannerArgs = [];
|
|
33
|
+
let yes = false;
|
|
27
34
|
|
|
28
35
|
for (const arg of argv) {
|
|
29
36
|
if (MODE_FLAGS.has(arg)) modes.push(arg.slice(2));
|
|
37
|
+
else if (YES_FLAGS.has(arg)) yes = true;
|
|
30
38
|
else scannerArgs.push(arg);
|
|
31
39
|
}
|
|
32
40
|
|
|
@@ -67,13 +75,17 @@ export function parseDslinterArgs(argv) {
|
|
|
67
75
|
mode = "dev";
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
const
|
|
78
|
+
const explicitScanPath = positional ?? null;
|
|
79
|
+
const { scanPath, projectRoot } = resolveScanAndProjectRoots(explicitScanPath);
|
|
71
80
|
|
|
72
81
|
return {
|
|
73
82
|
mode,
|
|
74
83
|
scannerArgs,
|
|
75
84
|
scanPath,
|
|
85
|
+
projectRoot,
|
|
76
86
|
outputPath,
|
|
77
87
|
servePort,
|
|
88
|
+
yes,
|
|
89
|
+
explicitScanPath,
|
|
78
90
|
};
|
|
79
91
|
}
|
|
@@ -24,9 +24,15 @@ describe("parseDslinterArgs", () => {
|
|
|
24
24
|
const p = parseDslinterArgs(["--report", "demo", "-p", "--json"]);
|
|
25
25
|
expect(p.mode).toBe("report");
|
|
26
26
|
expect(p.scannerArgs).toEqual(["demo", "-p", "--json"]);
|
|
27
|
-
expect(p.scanPath).
|
|
27
|
+
expect(p.scanPath).toContain("demo");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("parses --yes flag", () => {
|
|
31
|
+
const p = parseDslinterArgs(["--yes"]);
|
|
32
|
+
expect(p.yes).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
|
|
30
36
|
it("uses scanner mode for --serve only", () => {
|
|
31
37
|
expect(parseDslinterArgs([".", "--serve", "7878"]).mode).toBe("scanner");
|
|
32
38
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const REPORT_FILE_NAME = "dslinter-report.json";
|
|
2
|
+
export const REPORT_URL_PATH = "/dslinter-report.json";
|
|
3
|
+
|
|
4
|
+
export const CONFIG_FILE_NAMES = [".dslinter.json", "dslinter.json"];
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG_FILE_NAME = ".dslinter.json";
|
|
7
|
+
|
|
8
|
+
export const IGNORE_FILE_NAME = ".dslinterignore";
|
package/bin/lib/project-root.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
4
|
-
import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { resolveServePort } from "./constants.mjs";
|
|
7
|
+
import { readEnv } from "./env.mjs";
|
|
8
|
+
import { REPORT_FILE_NAME } from "./paths.mjs";
|
|
6
9
|
|
|
7
10
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
11
|
|
|
@@ -103,19 +106,78 @@ export function findViteRoot(startDir) {
|
|
|
103
106
|
*/
|
|
104
107
|
export function defaultReportPath(scanPath, outputFlag) {
|
|
105
108
|
if (outputFlag) return resolve(outputFlag);
|
|
106
|
-
|
|
109
|
+
const scanAbs = resolve(scanPath);
|
|
110
|
+
const viteRoot = findViteRoot(scanAbs);
|
|
111
|
+
if (viteRoot && resolve(viteRoot) !== scanAbs) {
|
|
112
|
+
return resolve(viteRoot, "public", REPORT_FILE_NAME);
|
|
113
|
+
}
|
|
114
|
+
return resolve(scanAbs, "public", REPORT_FILE_NAME);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Log when scan was promoted from a subdirectory to the project root.
|
|
119
|
+
* @param {{ promoted: boolean; originalPath?: string; scanPath: string }} info
|
|
120
|
+
* @deprecated Subdirectory scans are no longer promoted; use {@link logScanScopeHint}.
|
|
121
|
+
*/
|
|
122
|
+
export function logScanRootPromotion(info) {
|
|
123
|
+
if (!info.promoted || !info.originalPath) return;
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
`dslinter: using project root ${info.scanPath} (was ${info.originalPath}).\n`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Log when scanning a subdirectory while config/report use the project root.
|
|
131
|
+
* @param {{
|
|
132
|
+
* scanPath: string;
|
|
133
|
+
* projectRoot: string;
|
|
134
|
+
* explicitScanPath: string | null;
|
|
135
|
+
* }} info
|
|
136
|
+
*/
|
|
137
|
+
export function logScanScopeHint(info) {
|
|
138
|
+
const scanAbs = resolve(info.scanPath);
|
|
139
|
+
const projectAbs = resolve(info.projectRoot);
|
|
140
|
+
if (scanAbs === projectAbs) return;
|
|
141
|
+
|
|
142
|
+
const implicit =
|
|
143
|
+
info.explicitScanPath == null ||
|
|
144
|
+
info.explicitScanPath === "" ||
|
|
145
|
+
info.explicitScanPath === ".";
|
|
146
|
+
if (!implicit) return;
|
|
147
|
+
|
|
148
|
+
const rel =
|
|
149
|
+
relative(projectAbs, scanAbs).replace(/\\/g, "/") || scanAbs;
|
|
150
|
+
process.stderr.write(
|
|
151
|
+
`dslinter: scanning ${rel} (project root: ${projectAbs}). Run from repo root for a full-repo scan.\n`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @deprecated Use {@link logScanRootPromotion} after {@link promoteScanToProjectRoot}.
|
|
157
|
+
* @param {string} scanPath absolute or relative scan path
|
|
158
|
+
* @param {{ outputPath?: string | null }} [opts]
|
|
159
|
+
*/
|
|
160
|
+
export function warnIfSubdirectoryScan(scanPath, opts = {}) {
|
|
161
|
+
const scanAbs = resolve(scanPath);
|
|
162
|
+
const viteRoot = findViteRoot(scanAbs);
|
|
163
|
+
if (!viteRoot) return;
|
|
164
|
+
const viteAbs = resolve(viteRoot);
|
|
165
|
+
if (scanAbs === viteAbs) return;
|
|
166
|
+
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
"dslinter: using project root for scan (subdirectory paths shorten playground rel_path).\n",
|
|
169
|
+
);
|
|
170
|
+
if (!opts.outputPath) {
|
|
171
|
+
const reportAt = defaultReportPath(viteAbs, null);
|
|
172
|
+
process.stderr.write(`dslinter: report → ${reportAt}\n`);
|
|
173
|
+
}
|
|
107
174
|
}
|
|
108
175
|
|
|
109
176
|
/**
|
|
110
177
|
* @returns {number}
|
|
111
178
|
*/
|
|
112
179
|
export function defaultServePort() {
|
|
113
|
-
|
|
114
|
-
if (fromEnv) {
|
|
115
|
-
const n = Number.parseInt(fromEnv, 10);
|
|
116
|
-
if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
|
|
117
|
-
}
|
|
118
|
-
return 7878;
|
|
180
|
+
return resolveServePort();
|
|
119
181
|
}
|
|
120
182
|
|
|
121
183
|
/**
|
|
@@ -137,7 +199,7 @@ function dashboardDirIfReady(dir) {
|
|
|
137
199
|
*
|
|
138
200
|
* Resolution order:
|
|
139
201
|
* 1. Skip when `DSLINTER_NO_BUNDLED_DASHBOARD=1`
|
|
140
|
-
* 2. `
|
|
202
|
+
* 2. `DSLINTER_DASHBOARD_STATIC` — absolute or cwd-relative (temp/gitignored dirs ok)
|
|
141
203
|
* 3. `dashboard-dist/` next to the installed `dslinter` package
|
|
142
204
|
*
|
|
143
205
|
* @returns {string | null}
|
|
@@ -146,7 +208,7 @@ export function resolveBundledDashboardDir() {
|
|
|
146
208
|
const optOut = process.env.DSLINTER_NO_BUNDLED_DASHBOARD?.trim();
|
|
147
209
|
if (optOut === "1" || optOut?.toLowerCase() === "true") return null;
|
|
148
210
|
|
|
149
|
-
const fromEnv =
|
|
211
|
+
const fromEnv = readEnv("DASHBOARD_STATIC");
|
|
150
212
|
if (fromEnv) {
|
|
151
213
|
const dir = isAbsolute(fromEnv) ? normalize(fromEnv) : resolve(process.cwd(), fromEnv);
|
|
152
214
|
return dashboardDirIfReady(dir);
|
|
@@ -2,7 +2,11 @@ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
defaultReportPath,
|
|
7
|
+
ensureDashboardBuilt,
|
|
8
|
+
hasEmbedDashboard,
|
|
9
|
+
} from "./project-root.mjs";
|
|
6
10
|
|
|
7
11
|
describe("ensureDashboardBuilt (published install layout)", () => {
|
|
8
12
|
it("returns prebuilt dashboard-dist without spawning build when embed sources are absent", () => {
|
|
@@ -19,3 +23,30 @@ describe("ensureDashboardBuilt (published install layout)", () => {
|
|
|
19
23
|
expect(dist).toContain("dashboard-dist");
|
|
20
24
|
});
|
|
21
25
|
});
|
|
26
|
+
|
|
27
|
+
describe("defaultReportPath", () => {
|
|
28
|
+
it("writes to project public/ when scan path is a subdirectory of vite root", () => {
|
|
29
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-laravel-"));
|
|
30
|
+
const components = join(root, "resources", "js", "Components");
|
|
31
|
+
mkdirSync(components, { recursive: true });
|
|
32
|
+
writeFileSync(join(root, "vite.config.js"), "export default {};\n");
|
|
33
|
+
|
|
34
|
+
const reportPath = defaultReportPath(components, null);
|
|
35
|
+
expect(reportPath).toBe(join(root, "public", "dslinter-report.json"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("uses scan path public/ when scan path is the vite root", () => {
|
|
39
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-vite-root-"));
|
|
40
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
41
|
+
writeFileSync(join(root, "vite.config.ts"), "export default {};\n");
|
|
42
|
+
|
|
43
|
+
const reportPath = defaultReportPath(root, null);
|
|
44
|
+
expect(reportPath).toBe(join(root, "public", "dslinter-report.json"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("honors explicit --output", () => {
|
|
48
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-out-"));
|
|
49
|
+
const custom = join(root, "custom-report.json");
|
|
50
|
+
expect(defaultReportPath(root, custom)).toBe(custom);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { envIs } from "./env.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isInteractiveTTY() {
|
|
9
|
+
const ci = process.env.CI === "true" || process.env.CI === "1";
|
|
10
|
+
if (ci) return false;
|
|
11
|
+
if (envIs("NO_PROMPT")) return false;
|
|
12
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} question
|
|
17
|
+
* @param {{ defaultYes?: boolean }} [opts]
|
|
18
|
+
* @returns {Promise<boolean>}
|
|
19
|
+
*/
|
|
20
|
+
export async function confirmYesNo(question, opts = {}) {
|
|
21
|
+
const defaultYes = opts.defaultYes !== false;
|
|
22
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
23
|
+
const rl = createInterface({ input, output });
|
|
24
|
+
try {
|
|
25
|
+
const answer = (await rl.question(`${question} ${hint} `)).trim().toLowerCase();
|
|
26
|
+
if (!answer) return defaultYes;
|
|
27
|
+
return answer === "y" || answer === "yes";
|
|
28
|
+
} finally {
|
|
29
|
+
rl.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { findViteRoot } from "./project-root.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Walk up from `cwd` and return the best project root for scanning.
|
|
7
|
+
* 1. Nearest ancestor with vite.config.*
|
|
8
|
+
* 2. Else nearest ancestor with resources/js/ (Laravel)
|
|
9
|
+
* 3. Else cwd
|
|
10
|
+
* @param {string} [cwd]
|
|
11
|
+
* @returns {string} absolute path
|
|
12
|
+
*/
|
|
13
|
+
export function resolveProjectRoot(cwd = process.cwd()) {
|
|
14
|
+
let dir = resolve(cwd);
|
|
15
|
+
let laravelCandidate = null;
|
|
16
|
+
|
|
17
|
+
for (;;) {
|
|
18
|
+
const viteRoot = findViteRoot(dir);
|
|
19
|
+
if (viteRoot) return resolve(viteRoot);
|
|
20
|
+
|
|
21
|
+
if (!laravelCandidate && existsSync(join(dir, "resources", "js"))) {
|
|
22
|
+
laravelCandidate = dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const parent = dirname(dir);
|
|
26
|
+
if (parent === dir) break;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return laravelCandidate ?? resolve(cwd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the scan path (file-walk boundary): explicit positional relative to cwd;
|
|
35
|
+
* otherwise cwd. `"."` is literal cwd, not project root.
|
|
36
|
+
* @param {string | null | undefined} explicitPath user positional or null for default
|
|
37
|
+
* @param {string} [cwd]
|
|
38
|
+
* @returns {string} absolute path
|
|
39
|
+
*/
|
|
40
|
+
export function resolveScanPath(explicitPath, cwd = process.cwd()) {
|
|
41
|
+
if (explicitPath != null && explicitPath !== "") {
|
|
42
|
+
return resolve(cwd, explicitPath);
|
|
43
|
+
}
|
|
44
|
+
return resolve(cwd);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scan root (walk boundary) and project root (config, CSS, report parent).
|
|
49
|
+
* @param {string | null | undefined} explicitPath
|
|
50
|
+
* @param {string} [cwd]
|
|
51
|
+
* @returns {{ scanPath: string; projectRoot: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function resolveScanAndProjectRoots(explicitPath, cwd = process.cwd()) {
|
|
54
|
+
const scanPath = resolveScanPath(explicitPath, cwd);
|
|
55
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
56
|
+
return { scanPath, projectRoot };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Replace or insert the scanner positional path in argv.
|
|
61
|
+
* @param {string[]} scannerArgs
|
|
62
|
+
* @param {string} scanPath absolute scan path
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
export function withScannerScanPath(scannerArgs, scanPath) {
|
|
66
|
+
const out = [];
|
|
67
|
+
let replaced = false;
|
|
68
|
+
for (const arg of scannerArgs) {
|
|
69
|
+
if (!replaced && !arg.startsWith("-")) {
|
|
70
|
+
out.push(scanPath);
|
|
71
|
+
replaced = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
out.push(arg);
|
|
75
|
+
}
|
|
76
|
+
if (!replaced) out.unshift(scanPath);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, 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 {
|
|
6
|
+
resolveProjectRoot,
|
|
7
|
+
resolveScanAndProjectRoots,
|
|
8
|
+
resolveScanPath,
|
|
9
|
+
} from "./resolve-project.mjs";
|
|
10
|
+
|
|
11
|
+
describe("resolveProjectRoot", () => {
|
|
12
|
+
it("prefers vite root over laravel layout when both exist", () => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-resolve-vite-"));
|
|
14
|
+
mkdirSync(join(root, "resources", "js"), { recursive: true });
|
|
15
|
+
writeFileSync(join(root, "vite.config.js"), "export default {};\n");
|
|
16
|
+
expect(resolveProjectRoot(root)).toBe(root);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("uses resources/js ancestor when no vite config", () => {
|
|
20
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-resolve-laravel-"));
|
|
21
|
+
mkdirSync(join(root, "resources", "js", "Components"), { recursive: true });
|
|
22
|
+
const sub = join(root, "resources", "js", "Components");
|
|
23
|
+
expect(resolveProjectRoot(sub)).toBe(root);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("resolveScanPath", () => {
|
|
28
|
+
it("defaults to cwd when no explicit path", () => {
|
|
29
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-default-"));
|
|
30
|
+
const sub = join(root, "src", "components");
|
|
31
|
+
mkdirSync(sub, { recursive: true });
|
|
32
|
+
expect(resolveScanPath(null, sub)).toBe(sub);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("treats '.' as literal cwd", () => {
|
|
36
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-dot-"));
|
|
37
|
+
const sub = join(root, "resources", "js", "components");
|
|
38
|
+
mkdirSync(sub, { recursive: true });
|
|
39
|
+
writeFileSync(join(root, "vite.config.js"), "export default {};\n");
|
|
40
|
+
expect(resolveScanPath(".", sub)).toBe(sub);
|
|
41
|
+
expect(resolveScanPath(".", sub)).not.toBe(root);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("honors explicit positional path", () => {
|
|
45
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-explicit-"));
|
|
46
|
+
const demo = join(root, "demo");
|
|
47
|
+
mkdirSync(demo, { recursive: true });
|
|
48
|
+
expect(resolveScanPath("demo", root)).toBe(demo);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("resolveScanAndProjectRoots", () => {
|
|
53
|
+
it("keeps scan at cwd and project at vite root from subdirectory", () => {
|
|
54
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-project-"));
|
|
55
|
+
const components = join(root, "resources", "js", "components");
|
|
56
|
+
mkdirSync(components, { recursive: true });
|
|
57
|
+
writeFileSync(join(root, "vite.config.js"), "export default {};\n");
|
|
58
|
+
|
|
59
|
+
const result = resolveScanAndProjectRoots(null, components);
|
|
60
|
+
expect(result.scanPath).toBe(components);
|
|
61
|
+
expect(result.projectRoot).toBe(root);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses cwd for '.' while project root is vite root", () => {
|
|
65
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-dot-project-"));
|
|
66
|
+
const components = join(root, "resources", "js", "components");
|
|
67
|
+
mkdirSync(components, { recursive: true });
|
|
68
|
+
writeFileSync(join(root, "vite.config.js"), "export default {};\n");
|
|
69
|
+
|
|
70
|
+
const result = resolveScanAndProjectRoots(".", components);
|
|
71
|
+
expect(result.scanPath).toBe(components);
|
|
72
|
+
expect(result.projectRoot).toBe(root);
|
|
73
|
+
});
|
|
74
|
+
});
|