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,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
|
|
|
@@ -27,12 +30,26 @@ function maxMtimeInDir(dir, latest = 0) {
|
|
|
27
30
|
return latest;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} [root]
|
|
35
|
+
* @returns {boolean} true when embed SPA sources exist (monorepo / git checkout).
|
|
36
|
+
*/
|
|
37
|
+
export function hasEmbedDashboard(root = packageRoot) {
|
|
38
|
+
return existsSync(join(root, "index.html"));
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Rebuild `dashboard-dist/` when embed sources are newer than the bundle (or dist is missing).
|
|
43
|
+
* Published npm installs omit embed sources; use prebuilt `dashboard-dist/` only.
|
|
32
44
|
* @param {string} root
|
|
33
45
|
*/
|
|
34
46
|
export function ensureDashboardBuilt(root = packageRoot) {
|
|
35
|
-
const
|
|
47
|
+
const distDir = join(root, "dashboard-dist");
|
|
48
|
+
if (!hasEmbedDashboard(root)) {
|
|
49
|
+
return dashboardDirIfReady(distDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const distIndex = join(distDir, "index.html");
|
|
36
53
|
const force =
|
|
37
54
|
process.env.DSLINTER_REBUILD_DASHBOARD === "1" ||
|
|
38
55
|
process.env.DSLINTER_REBUILD_DASHBOARD?.toLowerCase() === "true";
|
|
@@ -40,12 +57,9 @@ export function ensureDashboardBuilt(root = packageRoot) {
|
|
|
40
57
|
let needsBuild = force || !existsSync(distIndex);
|
|
41
58
|
if (!needsBuild) {
|
|
42
59
|
const distMtime = statSync(distIndex).mtimeMs;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
needsBuild = true;
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
60
|
+
const embedDir = join(root, "embed");
|
|
61
|
+
if (existsSync(embedDir) && maxMtimeInDir(embedDir) > distMtime) {
|
|
62
|
+
needsBuild = true;
|
|
49
63
|
}
|
|
50
64
|
const configPath = join(root, "vite.config.ts");
|
|
51
65
|
if (existsSync(configPath) && statSync(configPath).mtimeMs > distMtime) {
|
|
@@ -53,7 +67,7 @@ export function ensureDashboardBuilt(root = packageRoot) {
|
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
if (!needsBuild) return dashboardDirIfReady(
|
|
70
|
+
if (!needsBuild) return dashboardDirIfReady(distDir);
|
|
57
71
|
|
|
58
72
|
process.stderr.write("[dslinter] Building dashboard bundle (dashboard-dist)…\n");
|
|
59
73
|
const result = spawnSync("npm", ["run", "build:dashboard"], {
|
|
@@ -64,12 +78,7 @@ export function ensureDashboardBuilt(root = packageRoot) {
|
|
|
64
78
|
if (result.status !== 0) {
|
|
65
79
|
throw new Error("dslinter: dashboard build failed");
|
|
66
80
|
}
|
|
67
|
-
return dashboardDirIfReady(
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** @returns {boolean} */
|
|
71
|
-
export function hasEmbedDashboard() {
|
|
72
|
-
return existsSync(join(packageRoot, "index.html"));
|
|
81
|
+
return dashboardDirIfReady(distDir);
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
const VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.js", "vite.config.mjs", "vite.config.cjs"];
|
|
@@ -97,19 +106,78 @@ export function findViteRoot(startDir) {
|
|
|
97
106
|
*/
|
|
98
107
|
export function defaultReportPath(scanPath, outputFlag) {
|
|
99
108
|
if (outputFlag) return resolve(outputFlag);
|
|
100
|
-
|
|
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
|
+
}
|
|
101
174
|
}
|
|
102
175
|
|
|
103
176
|
/**
|
|
104
177
|
* @returns {number}
|
|
105
178
|
*/
|
|
106
179
|
export function defaultServePort() {
|
|
107
|
-
|
|
108
|
-
if (fromEnv) {
|
|
109
|
-
const n = Number.parseInt(fromEnv, 10);
|
|
110
|
-
if (Number.isFinite(n) && n > 0 && n <= 65535) return n;
|
|
111
|
-
}
|
|
112
|
-
return 7878;
|
|
180
|
+
return resolveServePort();
|
|
113
181
|
}
|
|
114
182
|
|
|
115
183
|
/**
|
|
@@ -131,7 +199,7 @@ function dashboardDirIfReady(dir) {
|
|
|
131
199
|
*
|
|
132
200
|
* Resolution order:
|
|
133
201
|
* 1. Skip when `DSLINTER_NO_BUNDLED_DASHBOARD=1`
|
|
134
|
-
* 2. `
|
|
202
|
+
* 2. `DSLINTER_DASHBOARD_STATIC` — absolute or cwd-relative (temp/gitignored dirs ok)
|
|
135
203
|
* 3. `dashboard-dist/` next to the installed `dslinter` package
|
|
136
204
|
*
|
|
137
205
|
* @returns {string | null}
|
|
@@ -140,7 +208,7 @@ export function resolveBundledDashboardDir() {
|
|
|
140
208
|
const optOut = process.env.DSLINTER_NO_BUNDLED_DASHBOARD?.trim();
|
|
141
209
|
if (optOut === "1" || optOut?.toLowerCase() === "true") return null;
|
|
142
210
|
|
|
143
|
-
const fromEnv =
|
|
211
|
+
const fromEnv = readEnv("DASHBOARD_STATIC");
|
|
144
212
|
if (fromEnv) {
|
|
145
213
|
const dir = isAbsolute(fromEnv) ? normalize(fromEnv) : resolve(process.cwd(), fromEnv);
|
|
146
214
|
return dashboardDirIfReady(dir);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
defaultReportPath,
|
|
7
|
+
ensureDashboardBuilt,
|
|
8
|
+
hasEmbedDashboard,
|
|
9
|
+
} from "./project-root.mjs";
|
|
10
|
+
|
|
11
|
+
describe("ensureDashboardBuilt (published install layout)", () => {
|
|
12
|
+
it("returns prebuilt dashboard-dist without spawning build when embed sources are absent", () => {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-published-"));
|
|
14
|
+
mkdirSync(join(root, "dashboard-dist"), { recursive: true });
|
|
15
|
+
writeFileSync(join(root, "dashboard-dist", "index.html"), "<!doctype html>");
|
|
16
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
17
|
+
writeFileSync(join(root, "src", "index.ts"), "export {};\n");
|
|
18
|
+
|
|
19
|
+
expect(hasEmbedDashboard(root)).toBe(false);
|
|
20
|
+
|
|
21
|
+
const dist = ensureDashboardBuilt(root);
|
|
22
|
+
expect(dist).toBeTruthy();
|
|
23
|
+
expect(dist).toContain("dashboard-dist");
|
|
24
|
+
});
|
|
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
|
+
});
|