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
package/src/types/playground.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
1
2
|
import type { PlaygroundArgs, PlaygroundControl } from "./controls";
|
|
2
3
|
import type { PlaygroundPreviewComponent } from "./preview";
|
|
3
4
|
|
|
@@ -17,5 +18,8 @@ export type PlaygroundEntry = {
|
|
|
17
18
|
controls: PlaygroundControl[];
|
|
18
19
|
/** Optional JSX-ish snippet from current `values` (consumer-defined). */
|
|
19
20
|
usageSnippet?: (values: PlaygroundArgs) => string;
|
|
21
|
+
/** Render live preview from control values (preferred — avoids unstable component types). */
|
|
22
|
+
renderPreview: (values: PlaygroundArgs) => ReactNode;
|
|
23
|
+
/** @deprecated Use `renderPreview` — kept for manual `definePlayground` call sites. */
|
|
20
24
|
Preview: PlaygroundPreviewComponent;
|
|
21
25
|
};
|
package/src/types/report.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface ComponentDefinition {
|
|
|
17
17
|
line: number;
|
|
18
18
|
/** Props destructured from the first parameter, when detectable. */
|
|
19
19
|
declared_props?: string[];
|
|
20
|
+
/** CVA variant option keys per prop (playground select controls). */
|
|
21
|
+
declared_prop_options?: Record<string, string[]>;
|
|
22
|
+
/** Default values from CVA `defaultVariants`. */
|
|
23
|
+
declared_prop_defaults?: Record<string, string>;
|
|
24
|
+
cva_binding_name?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export interface JsxUsage {
|
|
@@ -33,6 +38,8 @@ export interface LintFinding {
|
|
|
33
38
|
path: string;
|
|
34
39
|
line: number | null;
|
|
35
40
|
severity: Severity;
|
|
41
|
+
/** Prop combo label from CVA matrix scan (CI or playground). */
|
|
42
|
+
variant_label?: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
export interface FileScan {
|
|
@@ -78,12 +85,6 @@ export interface UsageSummary {
|
|
|
78
85
|
usage_locations?: UsageLocation[];
|
|
79
86
|
}
|
|
80
87
|
|
|
81
|
-
export interface OwnershipSummary {
|
|
82
|
-
owner: string;
|
|
83
|
-
files: number;
|
|
84
|
-
definitions: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
88
|
/**
|
|
88
89
|
* Simplified prop kind from TypeScript (e.g. demo `merge-playgrounds.mjs`).
|
|
89
90
|
* Dashboard falls back to name heuristics when a key is missing or kind is `unknown`.
|
|
@@ -98,10 +99,14 @@ export interface PlaygroundSpec {
|
|
|
98
99
|
declared_props: string[];
|
|
99
100
|
group?: string | null;
|
|
100
101
|
/**
|
|
101
|
-
* Optional map from prop name to simplified TS kind, filled by
|
|
102
|
-
* (
|
|
102
|
+
* Optional map from prop name to simplified TS kind, filled by the TS enrich step
|
|
103
|
+
* after scanning (`bin/lib/enrich-playgrounds-from-ts.mjs`). Omitted when empty or unavailable.
|
|
103
104
|
*/
|
|
104
105
|
declared_prop_kinds?: Partial<Record<string, DeclaredPropKind>>;
|
|
106
|
+
/** CVA variant option keys per prop (dashboard renders as `<Select>`). */
|
|
107
|
+
declared_prop_options?: Record<string, string[]>;
|
|
108
|
+
/** Default values from CVA `defaultVariants`. */
|
|
109
|
+
declared_prop_defaults?: Record<string, string>;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export type CssTokenCategory =
|
|
@@ -136,14 +141,32 @@ export interface CssTokenSummary {
|
|
|
136
141
|
unused_tokens?: string[];
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/** Dashboard-relevant slice of `.dslinter.json` embedded in each scan report. */
|
|
145
|
+
export interface ReportConfig {
|
|
146
|
+
hidden_components?: string[];
|
|
147
|
+
hidden_paths?: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Agent/MCP-friendly slice of `.dslinter.json` embedded in each scan report. */
|
|
151
|
+
export interface ConfigSnapshot {
|
|
152
|
+
deprecated_components?: string[];
|
|
153
|
+
known_tokens?: string[];
|
|
154
|
+
include_dirs?: string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
139
157
|
export interface WorkspaceReport {
|
|
158
|
+
/** Report JSON schema version (1+). Omitted in legacy reports. */
|
|
159
|
+
schema_version?: number;
|
|
160
|
+
/** ISO 8601 UTC timestamp when the report was generated. */
|
|
161
|
+
generated_at?: string;
|
|
140
162
|
root: string;
|
|
141
163
|
files: FileScan[];
|
|
142
164
|
findings: LintFinding[];
|
|
143
165
|
duplicate_components: DuplicateComponent[];
|
|
144
166
|
usage_by_component: UsageSummary[];
|
|
145
|
-
ownership: OwnershipSummary[];
|
|
146
167
|
scores: GovernanceScores;
|
|
147
168
|
playgrounds?: PlaygroundSpec[];
|
|
148
169
|
css_tokens?: CssTokenSummary;
|
|
170
|
+
config?: ReportConfig;
|
|
171
|
+
config_snapshot?: ConfigSnapshot;
|
|
149
172
|
}
|
|
@@ -2,7 +2,7 @@ import type { PlaygroundEntry, WorkspaceReport } from "dslinter";
|
|
|
2
2
|
import { createPlaygroundRegistry } from "dslinter";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Eager Vite glob — must cover every path in `
|
|
5
|
+
* Eager Vite glob — must cover every path in `dslinter-report.json` → `playgrounds[].rel_path`.
|
|
6
6
|
* Nested paths (e.g. `src/components/ui/button.tsx`) require `**`, not a single `*`.
|
|
7
7
|
*/
|
|
8
8
|
const modules = import.meta.glob("../components/**/*.{tsx,jsx}", {
|
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
* Legacy snippet — prefer `import dslinter from "dslinter/vite"` (one plugin line).
|
|
3
3
|
* `npx dslinter` merges the plugin automatically when a consumer Vite app exists.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
function resolveServePort(): number {
|
|
6
|
+
const rawPort = process.env.DSLINTER_SERVE_PORT ?? process.env.PORT;
|
|
7
|
+
const parsedPort = Number(rawPort);
|
|
8
|
+
|
|
9
|
+
if (Number.isInteger(parsedPort) && parsedPort > 0) {
|
|
10
|
+
return parsedPort;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return 3210;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DSLINTER_SERVE_PORT = resolveServePort();
|
|
6
17
|
|
|
7
18
|
export const dslinterViteSnippet = {
|
|
8
19
|
resolve: {
|
|
@@ -13,12 +24,12 @@ export const dslinterViteSnippet = {
|
|
|
13
24
|
},
|
|
14
25
|
server: {
|
|
15
26
|
proxy: {
|
|
16
|
-
"/
|
|
17
|
-
target: `http://127.0.0.1:${
|
|
27
|
+
"/dslinter-report.json": {
|
|
28
|
+
target: `http://127.0.0.1:${DSLINTER_SERVE_PORT}`,
|
|
18
29
|
changeOrigin: true,
|
|
19
30
|
},
|
|
20
31
|
"/events": {
|
|
21
|
-
target: `http://127.0.0.1:${
|
|
32
|
+
target: `http://127.0.0.1:${DSLINTER_SERVE_PORT}`,
|
|
22
33
|
changeOrigin: true,
|
|
23
34
|
},
|
|
24
35
|
},
|
|
@@ -8,23 +8,26 @@ import {
|
|
|
8
8
|
} from "./collectScanModules";
|
|
9
9
|
|
|
10
10
|
describe("embedGlobKeyFromRelPath", () => {
|
|
11
|
-
it("maps Laravel rel_path to @
|
|
11
|
+
it("maps Laravel rel_path to @dslinter-scan key", () => {
|
|
12
12
|
expect(
|
|
13
13
|
embedGlobKeyFromRelPath(
|
|
14
14
|
"resources/js/Components/Billing/AdditionalEventLimitModal.tsx",
|
|
15
15
|
),
|
|
16
16
|
).toBe(
|
|
17
|
-
"@
|
|
17
|
+
"@dslinter-scan/resources/js/Components/Billing/AdditionalEventLimitModal.tsx",
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
it("strips leading slashes", () => {
|
|
22
22
|
expect(embedGlobKeyFromRelPath("/src/components/Foo.tsx")).toBe(
|
|
23
|
-
"@
|
|
23
|
+
"@dslinter-scan/src/components/Foo.tsx",
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
const caseInsensitiveFs =
|
|
29
|
+
process.platform === "darwin" || process.platform === "win32";
|
|
30
|
+
|
|
28
31
|
describe("collectScanModuleRelPaths", () => {
|
|
29
32
|
it("collects tsx/jsx and skips node_modules", () => {
|
|
30
33
|
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-"));
|
|
@@ -39,4 +42,89 @@ describe("collectScanModuleRelPaths", () => {
|
|
|
39
42
|
const paths = collectScanModuleRelPaths(root);
|
|
40
43
|
expect(paths).toEqual(["resources/js/Components/Button.tsx"]);
|
|
41
44
|
});
|
|
45
|
+
|
|
46
|
+
it("respects .dslinter.json include_dirs", () => {
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-inc-"));
|
|
48
|
+
mkdirSync(join(root, "resources", "js", "components", "ui"), {
|
|
49
|
+
recursive: true,
|
|
50
|
+
});
|
|
51
|
+
mkdirSync(join(root, "resources", "js", "pages"), { recursive: true });
|
|
52
|
+
writeFileSync(
|
|
53
|
+
join(root, "resources", "js", "components", "ui", "button.tsx"),
|
|
54
|
+
"export function Button() { return null; }",
|
|
55
|
+
);
|
|
56
|
+
writeFileSync(
|
|
57
|
+
join(root, "resources", "js", "pages", "Home.tsx"),
|
|
58
|
+
"export default function Home() { return null; }",
|
|
59
|
+
);
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(root, ".dslinter.json"),
|
|
62
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const paths = collectScanModuleRelPaths(root);
|
|
66
|
+
expect(paths).toEqual(["resources/js/components/ui/button.tsx"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.skipIf(!caseInsensitiveFs)(
|
|
70
|
+
"matches include_dirs case-insensitively on case-insensitive filesystems",
|
|
71
|
+
() => {
|
|
72
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-case-"));
|
|
73
|
+
mkdirSync(join(root, "resources", "js", "Components"), { recursive: true });
|
|
74
|
+
writeFileSync(
|
|
75
|
+
join(root, "resources", "js", "Components", "Button.tsx"),
|
|
76
|
+
"export function Button() { return null; }",
|
|
77
|
+
);
|
|
78
|
+
writeFileSync(
|
|
79
|
+
join(root, ".dslinter.json"),
|
|
80
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const paths = collectScanModuleRelPaths(root);
|
|
84
|
+
expect(paths).toEqual(["resources/js/Components/Button.tsx"]);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
it.skipIf(caseInsensitiveFs)(
|
|
89
|
+
"requires include_dirs casing to match on case-sensitive filesystems",
|
|
90
|
+
() => {
|
|
91
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-case-"));
|
|
92
|
+
mkdirSync(join(root, "resources", "js", "Components"), { recursive: true });
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(root, "resources", "js", "Components", "Button.tsx"),
|
|
95
|
+
"export function Button() { return null; }",
|
|
96
|
+
);
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(root, ".dslinter.json"),
|
|
99
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(collectScanModuleRelPaths(root)).toEqual([]);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
it("scopes collection to scanRoot subdirectory", () => {
|
|
107
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scan-sub-"));
|
|
108
|
+
mkdirSync(join(root, "resources", "js", "components"), { recursive: true });
|
|
109
|
+
mkdirSync(join(root, "resources", "js", "layouts", "auth"), {
|
|
110
|
+
recursive: true,
|
|
111
|
+
});
|
|
112
|
+
writeFileSync(
|
|
113
|
+
join(root, "resources", "js", "components", "Button.tsx"),
|
|
114
|
+
"export function Button() { return null; }",
|
|
115
|
+
);
|
|
116
|
+
writeFileSync(
|
|
117
|
+
join(root, "resources", "js", "layouts", "auth", "Split.tsx"),
|
|
118
|
+
"export function Split() { return null; }",
|
|
119
|
+
);
|
|
120
|
+
writeFileSync(
|
|
121
|
+
join(root, ".dslinter.json"),
|
|
122
|
+
JSON.stringify({ include_dirs: ["resources/js"] }),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const paths = collectScanModuleRelPaths(
|
|
126
|
+
join(root, "resources", "js", "components"),
|
|
127
|
+
);
|
|
128
|
+
expect(paths).toEqual(["resources/js/components/Button.tsx"]);
|
|
129
|
+
});
|
|
42
130
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { readdirSync } from "node:fs";
|
|
2
|
-
import { join, relative, resolve } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { embedGlobKeyFromRelPath } from "../src/playground/embedGlobKey";
|
|
4
|
+
|
|
5
|
+
const CONFIG_NAMES = [".dslinter.json", "dslinter.json"];
|
|
3
6
|
|
|
4
7
|
const SKIP_DIR_NAMES = new Set([
|
|
5
8
|
"node_modules",
|
|
@@ -20,41 +23,103 @@ const SKIP_DIR_NAMES = new Set([
|
|
|
20
23
|
|
|
21
24
|
const SOURCE_EXT = /\.(tsx|jsx)$/;
|
|
22
25
|
|
|
26
|
+
function findConfigPath(startDir: string): string | null {
|
|
27
|
+
let dir = resolve(startDir);
|
|
28
|
+
for (;;) {
|
|
29
|
+
for (const name of CONFIG_NAMES) {
|
|
30
|
+
const candidate = join(dir, name);
|
|
31
|
+
if (existsSync(candidate)) return candidate;
|
|
32
|
+
}
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir) break;
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function projectRootForConfig(startDir: string): string {
|
|
41
|
+
const configPath = findConfigPath(startDir);
|
|
42
|
+
return configPath ? dirname(configPath) : resolve(startDir);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readIncludeDirs(projectRoot: string): string[] | null {
|
|
46
|
+
const configPath = findConfigPath(projectRoot);
|
|
47
|
+
if (!configPath) return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8")) as {
|
|
50
|
+
include_dirs?: string[];
|
|
51
|
+
};
|
|
52
|
+
if (Array.isArray(parsed.include_dirs) && parsed.include_dirs.length > 0) {
|
|
53
|
+
return parsed.include_dirs;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function pathMatchesIncludePrefix(relFromProject: string, prefix: string): boolean {
|
|
62
|
+
const norm = prefix.trim().replace(/\\/g, "/").replace(/\/$/, "");
|
|
63
|
+
if (!norm) return false;
|
|
64
|
+
if (process.platform === "darwin" || process.platform === "win32") {
|
|
65
|
+
const rel = relFromProject.toLowerCase();
|
|
66
|
+
const pref = norm.toLowerCase();
|
|
67
|
+
return rel === pref || rel.startsWith(`${pref}/`);
|
|
68
|
+
}
|
|
69
|
+
return relFromProject === norm || relFromProject.startsWith(`${norm}/`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function matchesIncludeDirs(
|
|
73
|
+
relFromProject: string,
|
|
74
|
+
includeDirs: string[] | null,
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!includeDirs) return true;
|
|
77
|
+
return includeDirs.some((dir) =>
|
|
78
|
+
pathMatchesIncludePrefix(relFromProject, dir),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function walkDir(
|
|
83
|
+
dir: string,
|
|
84
|
+
projectRoot: string,
|
|
85
|
+
includeDirs: string[] | null,
|
|
86
|
+
out: string[],
|
|
87
|
+
): void {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const ent of entries) {
|
|
95
|
+
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
96
|
+
const full = join(dir, ent.name);
|
|
97
|
+
if (ent.isDirectory()) {
|
|
98
|
+
if (SKIP_DIR_NAMES.has(ent.name)) continue;
|
|
99
|
+
walkDir(full, projectRoot, includeDirs, out);
|
|
100
|
+
} else if (ent.isFile() && SOURCE_EXT.test(ent.name)) {
|
|
101
|
+
const relFromProject = relative(projectRoot, full).replace(/\\/g, "/");
|
|
102
|
+
if (!matchesIncludeDirs(relFromProject, includeDirs)) continue;
|
|
103
|
+
out.push(relFromProject);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
23
108
|
/**
|
|
24
109
|
* Collect repo-relative posix paths for `.tsx`/`.jsx` files under `scanRoot`.
|
|
110
|
+
* Config and `include_dirs` resolve from the nearest project root; only files
|
|
111
|
+
* under `scanRoot` are walked.
|
|
25
112
|
*/
|
|
26
113
|
export function collectScanModuleRelPaths(scanRoot: string): string[] {
|
|
27
|
-
const
|
|
114
|
+
const scanAbs = resolve(scanRoot);
|
|
115
|
+
const projectRoot = projectRootForConfig(scanAbs);
|
|
116
|
+
const includeDirs = readIncludeDirs(projectRoot);
|
|
28
117
|
const out: string[] = [];
|
|
29
118
|
|
|
30
|
-
|
|
31
|
-
let entries;
|
|
32
|
-
try {
|
|
33
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
34
|
-
} catch {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
for (const ent of entries) {
|
|
38
|
-
if (ent.name.startsWith(".") && ent.name !== ".") continue;
|
|
39
|
-
const full = join(dir, ent.name);
|
|
40
|
-
if (ent.isDirectory()) {
|
|
41
|
-
if (SKIP_DIR_NAMES.has(ent.name)) continue;
|
|
42
|
-
walk(full);
|
|
43
|
-
} else if (ent.isFile() && SOURCE_EXT.test(ent.name)) {
|
|
44
|
-
out.push(relative(root, full).replace(/\\/g, "/"));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
119
|
+
walkDir(scanAbs, projectRoot, includeDirs, out);
|
|
48
120
|
|
|
49
|
-
walk(root);
|
|
50
121
|
out.sort();
|
|
51
122
|
return out;
|
|
52
123
|
}
|
|
53
124
|
|
|
54
|
-
|
|
55
|
-
* Virtual module map key for a scanner `rel_path` (embed convention).
|
|
56
|
-
*/
|
|
57
|
-
export function embedGlobKeyFromRelPath(relPath: string): string {
|
|
58
|
-
const trimmed = relPath.replace(/^\/+/, "");
|
|
59
|
-
return `@dslint-scan/${trimmed}`;
|
|
60
|
-
}
|
|
125
|
+
export { embedGlobKeyFromRelPath };
|
package/vite/consumer.config.mjs
CHANGED
|
@@ -2,10 +2,10 @@ import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Merges the consumer's vite.config with the dslinter plugin.
|
|
5
|
-
* Used by `npx dslinter` when a host Vite app is detected (`
|
|
5
|
+
* Used by `npx dslinter` when a host Vite app is detected (`DSLINTER_VITE_ROOT`).
|
|
6
6
|
*/
|
|
7
7
|
export default defineConfig(async ({ command, mode }) => {
|
|
8
|
-
const viteRoot = process.env.
|
|
8
|
+
const viteRoot = process.env.DSLINTER_VITE_ROOT?.trim() || process.cwd();
|
|
9
9
|
const loaded = await loadConfigFromFile(
|
|
10
10
|
{ command, mode },
|
|
11
11
|
undefined,
|
|
@@ -13,10 +13,13 @@ export default defineConfig(async ({ command, mode }) => {
|
|
|
13
13
|
);
|
|
14
14
|
const userConfig = loaded?.config ?? {};
|
|
15
15
|
const { default: dslinter } = await import("./plugin.ts");
|
|
16
|
+
const scanRoot = process.env.DSLINTER_SCAN_ROOT?.trim() || viteRoot;
|
|
17
|
+
const consumerViteRoot =
|
|
18
|
+
process.env.DSLINTER_CONSUMER_VITE_ROOT?.trim() || viteRoot;
|
|
16
19
|
return mergeConfig(
|
|
17
20
|
userConfig,
|
|
18
21
|
defineConfig({
|
|
19
|
-
plugins: [dslinter()],
|
|
22
|
+
plugins: [dslinter({ scanRoot, consumerViteRoot })],
|
|
20
23
|
}),
|
|
21
24
|
);
|
|
22
25
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
flattenViteAlias,
|
|
4
|
+
importerUnderScanRoot,
|
|
5
|
+
resolveWithConsumerAliases,
|
|
6
|
+
} from "./consumerAlias";
|
|
7
|
+
|
|
8
|
+
describe("flattenViteAlias", () => {
|
|
9
|
+
it("resolves @/ prefix from record alias", () => {
|
|
10
|
+
const aliases = flattenViteAlias(
|
|
11
|
+
{ "@": "/app/resources/js", "@/": "/app/resources/js/" },
|
|
12
|
+
"/app",
|
|
13
|
+
);
|
|
14
|
+
const resolved = resolveWithConsumerAliases(
|
|
15
|
+
"@/Components/Button",
|
|
16
|
+
aliases,
|
|
17
|
+
);
|
|
18
|
+
expect(resolved?.replace(/\\/g, "/")).toContain("resources/js");
|
|
19
|
+
expect(resolved?.replace(/\\/g, "/")).toContain("Components/Button");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("importerUnderScanRoot", () => {
|
|
24
|
+
it("matches files under scan root", () => {
|
|
25
|
+
expect(
|
|
26
|
+
importerUnderScanRoot(
|
|
27
|
+
"/repo/resources/js/Components/Foo.tsx",
|
|
28
|
+
"/repo",
|
|
29
|
+
),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
expect(
|
|
32
|
+
importerUnderScanRoot(
|
|
33
|
+
"/other/pkg/Button.tsx",
|
|
34
|
+
"/repo",
|
|
35
|
+
),
|
|
36
|
+
).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("matches paths with Vite query suffixes", () => {
|
|
40
|
+
expect(
|
|
41
|
+
importerUnderScanRoot(
|
|
42
|
+
"/repo/resources/js/hooks/use-current-url.ts?v=abc",
|
|
43
|
+
"/repo",
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import type { Alias, AliasOptions } from "vite";
|
|
3
|
+
|
|
4
|
+
export type FlatAlias = {
|
|
5
|
+
find: string | RegExp;
|
|
6
|
+
replacement: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function normalizeFind(find: string | RegExp): string | RegExp {
|
|
10
|
+
if (typeof find !== "string") return find;
|
|
11
|
+
if (find.endsWith("/") && !find.includes("*")) {
|
|
12
|
+
return find.slice(0, -1);
|
|
13
|
+
}
|
|
14
|
+
return find;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Flatten Vite `resolve.alias` for use in a custom resolveId hook.
|
|
19
|
+
*/
|
|
20
|
+
export function flattenViteAlias(
|
|
21
|
+
alias: AliasOptions | undefined,
|
|
22
|
+
consumerRoot: string,
|
|
23
|
+
): FlatAlias[] {
|
|
24
|
+
if (!alias) return [];
|
|
25
|
+
const root = resolve(consumerRoot);
|
|
26
|
+
const out: FlatAlias[] = [];
|
|
27
|
+
|
|
28
|
+
const push = (find: string | RegExp, replacement: string) => {
|
|
29
|
+
const rep = isAbsolute(replacement)
|
|
30
|
+
? replacement
|
|
31
|
+
: resolve(root, replacement);
|
|
32
|
+
out.push({ find: normalizeFind(find), replacement: rep });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(alias)) {
|
|
36
|
+
for (const entry of alias) {
|
|
37
|
+
if (typeof entry === "object" && entry !== null && "find" in entry) {
|
|
38
|
+
const e = entry as Alias;
|
|
39
|
+
push(e.find, String(e.replacement));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out.sort((a, b) => {
|
|
43
|
+
const al = typeof a.find === "string" ? a.find.length : 0;
|
|
44
|
+
const bl = typeof b.find === "string" ? b.find.length : 0;
|
|
45
|
+
return bl - al;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [find, replacement] of Object.entries(alias)) {
|
|
50
|
+
push(find, String(replacement));
|
|
51
|
+
}
|
|
52
|
+
return out.sort((a, b) => {
|
|
53
|
+
const al = typeof a.find === "string" ? a.find.length : 0;
|
|
54
|
+
const bl = typeof b.find === "string" ? b.find.length : 0;
|
|
55
|
+
return bl - al;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve `id` using consumer aliases (longest prefix first for strings).
|
|
61
|
+
*/
|
|
62
|
+
export function resolveWithConsumerAliases(
|
|
63
|
+
id: string,
|
|
64
|
+
aliases: FlatAlias[],
|
|
65
|
+
): string | null {
|
|
66
|
+
for (const { find, replacement } of aliases) {
|
|
67
|
+
if (typeof find === "string") {
|
|
68
|
+
if (find.endsWith("/")) {
|
|
69
|
+
if (id.startsWith(find)) {
|
|
70
|
+
const sub = id.slice(find.length);
|
|
71
|
+
return join(replacement, sub);
|
|
72
|
+
}
|
|
73
|
+
const findNoSlash = find.slice(0, -1);
|
|
74
|
+
if (id === findNoSlash || id.startsWith(`${findNoSlash}/`)) {
|
|
75
|
+
const sub = id === findNoSlash ? "" : id.slice(findNoSlash.length + 1);
|
|
76
|
+
return sub ? join(replacement, sub) : replacement;
|
|
77
|
+
}
|
|
78
|
+
} else if (id === find) {
|
|
79
|
+
return replacement;
|
|
80
|
+
} else if (id.startsWith(`${find}/`)) {
|
|
81
|
+
const sub = id.slice(find.length + 1);
|
|
82
|
+
return join(replacement, sub);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const m = id.match(find);
|
|
86
|
+
if (m) {
|
|
87
|
+
return id.replace(find, replacement);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True when `importer` is a file under `scanRoot`. */
|
|
95
|
+
export function importerUnderScanRoot(
|
|
96
|
+
importer: string | undefined,
|
|
97
|
+
scanRoot: string,
|
|
98
|
+
): boolean {
|
|
99
|
+
if (!importer || importer === "\0virtual") return false;
|
|
100
|
+
const root = resolve(scanRoot).replace(/\\/g, "/");
|
|
101
|
+
let norm = importer.replace(/\\/g, "/");
|
|
102
|
+
if (norm.startsWith("\0")) norm = norm.slice(1);
|
|
103
|
+
const q = norm.indexOf("?");
|
|
104
|
+
if (q !== -1) norm = norm.slice(0, q);
|
|
105
|
+
const rootWithSlash = root.endsWith("/") ? root : `${root}/`;
|
|
106
|
+
return norm === root || norm.startsWith(rootWithSlash);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const INERTIA_SHIM_IDS = new Set([
|
|
110
|
+
"@inertiajs/react",
|
|
111
|
+
"@inertiajs/react/server",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
export const ZIGGY_SHIM_ID = "ziggy-js";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { mkdtempSync } from "node:fs";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
buildEmbedIndexCss,
|
|
8
|
+
embedSourcePathsRelativeToCss,
|
|
9
|
+
resolveEmbedConsumerSourceDirs,
|
|
10
|
+
shouldInjectEmbedConsumerSources,
|
|
11
|
+
} from "./embedTailwindSources";
|
|
12
|
+
|
|
13
|
+
const packageRoot = resolve(import.meta.dirname, "..");
|
|
14
|
+
const embedCssPath = join(packageRoot, "embed", "index.css");
|
|
15
|
+
|
|
16
|
+
describe("embedTailwindSources", () => {
|
|
17
|
+
it("maps Laravel include_dirs to paths relative to embed/index.css", () => {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-embed-src-"));
|
|
19
|
+
mkdirSync(join(root, "resources", "js", "components"), { recursive: true });
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(root, ".dslinter.json"),
|
|
22
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const paths = embedSourcePathsRelativeToCss(root, packageRoot, embedCssPath);
|
|
26
|
+
expect(paths.length).toBe(1);
|
|
27
|
+
expect(paths[0]).toMatch(/resources\/js\/components$/);
|
|
28
|
+
expect(resolve(join(packageRoot, "embed"), paths[0]!)).toBe(
|
|
29
|
+
join(root, "resources", "js", "components"),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("falls back to src when include_dirs is absent", () => {
|
|
34
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-embed-fallback-"));
|
|
35
|
+
mkdirSync(join(root, "src", "components"), { recursive: true });
|
|
36
|
+
|
|
37
|
+
const absDirs = resolveEmbedConsumerSourceDirs(root, packageRoot);
|
|
38
|
+
expect(absDirs).toEqual([join(root, "src")]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("skips include_dirs that do not exist on disk", () => {
|
|
42
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-embed-missing-"));
|
|
43
|
+
writeFileSync(
|
|
44
|
+
join(root, ".dslinter.json"),
|
|
45
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(resolveEmbedConsumerSourceDirs(root, packageRoot)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("buildEmbedIndexCss splices consumer @source after dashboard src", () => {
|
|
52
|
+
const base = `@import "tailwindcss";\n@source "../src";\n@import "../src/styles/dashboard-theme.css";\n`;
|
|
53
|
+
const out = buildEmbedIndexCss(base, ["../../../demo/src/components"]);
|
|
54
|
+
expect(out).toContain('@source "../src";');
|
|
55
|
+
expect(out).toContain('@source "../../../demo/src/components";');
|
|
56
|
+
expect(out.indexOf('../src";')).toBeLessThan(
|
|
57
|
+
out.indexOf("demo/src/components"),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shouldInjectEmbedConsumerSources when scan root differs from package root", () => {
|
|
62
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-embed-inject-"));
|
|
63
|
+
mkdirSync(join(root, "resources", "js", "components"), { recursive: true });
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(root, ".dslinter.json"),
|
|
66
|
+
JSON.stringify({ include_dirs: ["resources/js/components"] }),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(shouldInjectEmbedConsumerSources(root, packageRoot)).toBe(true);
|
|
70
|
+
expect(shouldInjectEmbedConsumerSources(packageRoot, packageRoot)).toBe(
|
|
71
|
+
false,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|