dslinter 0.1.1 → 0.1.3
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 +8 -0
- package/README.md +43 -0
- package/bin/dslinter.mjs +6 -0
- package/bin/modes/init.mjs +101 -0
- package/dashboard-dist/assets/index-DGUG_3SK.js +205 -0
- package/dashboard-dist/index.html +1 -1
- package/index.cjs +52 -52
- package/package.json +7 -6
- package/src/components/ComponentInspectPane.tsx +36 -0
- package/src/index.ts +22 -1
- package/src/playground/buildPlaygroundEntriesFromReport.ts +47 -6
- package/src/playground/createPlaygroundRegistry.ts +61 -0
- package/src/playground/playgroundJoin.test.ts +82 -0
- package/src/playground/playgroundJoin.ts +145 -0
- package/src/shell/DashboardLayout.tsx +11 -0
- package/templates/playground/buildRegistry.laravel.ts +26 -0
- package/templates/playground/buildRegistry.ts +26 -0
- package/templates/vite.dslint-scan-alias.snippet.ts +21 -0
- package/templates/vite.dslinter.snippet.ts +28 -0
- package/dashboard-dist/assets/index-B6zsYv3h.js +0 -206
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WorkspaceReport } from "../types/report";
|
|
3
|
+
import {
|
|
4
|
+
defaultConsumerGlobKeyFromRelPath,
|
|
5
|
+
diagnosePlaygroundJoinSkips,
|
|
6
|
+
} from "./playgroundJoin";
|
|
7
|
+
import { buildPlaygroundEntriesFromReportWithSkips } from "./buildPlaygroundEntriesFromReport";
|
|
8
|
+
|
|
9
|
+
describe("defaultConsumerGlobKeyFromRelPath", () => {
|
|
10
|
+
it("maps nested ui paths for src/playground registry", () => {
|
|
11
|
+
expect(defaultConsumerGlobKeyFromRelPath("src/components/ui/button.tsx")).toBe(
|
|
12
|
+
"../components/ui/button.tsx",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("maps flat component paths", () => {
|
|
17
|
+
expect(defaultConsumerGlobKeyFromRelPath("src/components/Button.tsx")).toBe(
|
|
18
|
+
"../components/Button.tsx",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("maps Laravel resources/js paths", () => {
|
|
23
|
+
expect(
|
|
24
|
+
defaultConsumerGlobKeyFromRelPath(
|
|
25
|
+
"resources/js/Components/Icons/Activity.tsx",
|
|
26
|
+
),
|
|
27
|
+
).toBe("../Components/Icons/Activity.tsx");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("diagnosePlaygroundJoinSkips", () => {
|
|
32
|
+
const report: WorkspaceReport = {
|
|
33
|
+
root: "/repo",
|
|
34
|
+
files: [],
|
|
35
|
+
findings: [],
|
|
36
|
+
scores: {
|
|
37
|
+
system_health: 0,
|
|
38
|
+
token_adoption: 0,
|
|
39
|
+
component_adoption: 0,
|
|
40
|
+
ux_consistency: 0,
|
|
41
|
+
},
|
|
42
|
+
ownership: [],
|
|
43
|
+
duplicate_components: [],
|
|
44
|
+
usage_by_component: [],
|
|
45
|
+
playgrounds: [
|
|
46
|
+
{
|
|
47
|
+
id: "Button",
|
|
48
|
+
export_name: "Button",
|
|
49
|
+
rel_path: "src/components/ui/button.tsx",
|
|
50
|
+
declared_props: [],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it("flags module_not_found when glob key is missing", () => {
|
|
56
|
+
const skipped = diagnosePlaygroundJoinSkips(report, {}, {
|
|
57
|
+
globKeyFromRelPath: defaultConsumerGlobKeyFromRelPath,
|
|
58
|
+
});
|
|
59
|
+
expect(skipped).toHaveLength(1);
|
|
60
|
+
expect(skipped[0]?.reason).toBe("module_not_found");
|
|
61
|
+
expect(skipped[0]?.globKey).toBe("../components/ui/button.tsx");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("joins when module and export exist", () => {
|
|
65
|
+
const key = defaultConsumerGlobKeyFromRelPath("src/components/ui/button.tsx");
|
|
66
|
+
const modules = {
|
|
67
|
+
[key]: {
|
|
68
|
+
Button: function Button() {
|
|
69
|
+
return null;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const { entries, skipped } = buildPlaygroundEntriesFromReportWithSkips(
|
|
74
|
+
report,
|
|
75
|
+
modules,
|
|
76
|
+
{ globKeyFromRelPath: defaultConsumerGlobKeyFromRelPath, logJoinSkips: false },
|
|
77
|
+
);
|
|
78
|
+
expect(skipped).toHaveLength(0);
|
|
79
|
+
expect(entries).toHaveLength(1);
|
|
80
|
+
expect(entries[0]?.id).toBe("Button");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { PlaygroundSpec, WorkspaceReport } from "../types/report";
|
|
2
|
+
import type { BuildPlaygroundModules, BuildPlaygroundOptions } from "./buildPlaygroundEntriesFromReport";
|
|
3
|
+
|
|
4
|
+
export type PlaygroundJoinSkipReason = "module_not_found" | "export_not_found";
|
|
5
|
+
|
|
6
|
+
export type PlaygroundJoinSkip = {
|
|
7
|
+
export_name: string;
|
|
8
|
+
rel_path: string;
|
|
9
|
+
globKey: string;
|
|
10
|
+
reason: PlaygroundJoinSkipReason;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONSUMER_STRIP_PREFIXES = ["src/", "resources/js/"] as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build `globKeyFromRelPath` for a registry file one level below a source root
|
|
17
|
+
* (e.g. `src/playground/` or `resources/js/playground/`).
|
|
18
|
+
*/
|
|
19
|
+
export function createConsumerGlobKeyFromRelPath(
|
|
20
|
+
options: {
|
|
21
|
+
/** Path prefixes removed from report `rel_path` before prepending `relativePrefix`. */
|
|
22
|
+
stripPrefixes?: readonly string[];
|
|
23
|
+
/** Prepended to the stripped path (default `../`). */
|
|
24
|
+
relativePrefix?: string;
|
|
25
|
+
} = {},
|
|
26
|
+
): (relPath: string) => string {
|
|
27
|
+
const stripPrefixes = options.stripPrefixes ?? DEFAULT_CONSUMER_STRIP_PREFIXES;
|
|
28
|
+
const relativePrefix = options.relativePrefix ?? "../";
|
|
29
|
+
return (relPath: string) => {
|
|
30
|
+
let trimmed = relPath.replace(/^\/+/, "");
|
|
31
|
+
for (const prefix of stripPrefixes) {
|
|
32
|
+
if (trimmed.startsWith(prefix)) {
|
|
33
|
+
trimmed = trimmed.slice(prefix.length);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return `${relativePrefix}${trimmed}`;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Maps report `rel_path` to a Vite `import.meta.glob` key when the registry lives in
|
|
43
|
+
* `src/playground/` or `resources/js/playground/` next to your components.
|
|
44
|
+
*
|
|
45
|
+
* Examples:
|
|
46
|
+
* - `src/components/ui/button.tsx` → `../components/ui/button.tsx`
|
|
47
|
+
* - `resources/js/Components/Icons/Activity.tsx` → `../Components/Icons/Activity.tsx`
|
|
48
|
+
*/
|
|
49
|
+
export function defaultConsumerGlobKeyFromRelPath(relPath: string): string {
|
|
50
|
+
return createConsumerGlobKeyFromRelPath()(relPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function defaultEmbedGlobKeyFromRelPath(relPath: string): string {
|
|
54
|
+
const trimmed = relPath.replace(/^\/+/, "");
|
|
55
|
+
return `@dslint-scan/${trimmed}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getExport(
|
|
59
|
+
mod: Record<string, unknown>,
|
|
60
|
+
exportName: string,
|
|
61
|
+
): unknown {
|
|
62
|
+
const x = mod[exportName];
|
|
63
|
+
return typeof x === "function" ? x : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* For each playground spec, explain why it would not join to `modules`.
|
|
68
|
+
*/
|
|
69
|
+
export function diagnosePlaygroundJoinSkips(
|
|
70
|
+
report: WorkspaceReport | null | undefined,
|
|
71
|
+
modules: BuildPlaygroundModules,
|
|
72
|
+
options: Pick<BuildPlaygroundOptions, "globKeyFromRelPath"> = {},
|
|
73
|
+
): PlaygroundJoinSkip[] {
|
|
74
|
+
const specs = report?.playgrounds;
|
|
75
|
+
if (!specs?.length) return [];
|
|
76
|
+
|
|
77
|
+
const globKeyFromRelPath =
|
|
78
|
+
options.globKeyFromRelPath ?? defaultEmbedGlobKeyFromRelPath;
|
|
79
|
+
|
|
80
|
+
const skipped: PlaygroundJoinSkip[] = [];
|
|
81
|
+
for (const spec of specs) {
|
|
82
|
+
const globKey = globKeyFromRelPath(spec.rel_path);
|
|
83
|
+
const mod = modules[globKey];
|
|
84
|
+
if (!mod) {
|
|
85
|
+
skipped.push({
|
|
86
|
+
export_name: spec.export_name,
|
|
87
|
+
rel_path: spec.rel_path,
|
|
88
|
+
globKey,
|
|
89
|
+
reason: "module_not_found",
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!getExport(mod, spec.export_name)) {
|
|
94
|
+
skipped.push({
|
|
95
|
+
export_name: spec.export_name,
|
|
96
|
+
rel_path: spec.rel_path,
|
|
97
|
+
globKey,
|
|
98
|
+
reason: "export_not_found",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return skipped;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Dev-only: log skipped playground joins (module glob / export name mismatches).
|
|
107
|
+
*/
|
|
108
|
+
export function logPlaygroundJoinSkips(
|
|
109
|
+
skipped: PlaygroundJoinSkip[],
|
|
110
|
+
options?: { label?: string },
|
|
111
|
+
): void {
|
|
112
|
+
if (!skipped.length) return;
|
|
113
|
+
if (typeof import.meta !== "undefined" && !import.meta.env?.DEV) return;
|
|
114
|
+
|
|
115
|
+
const label = options?.label ?? "[dslinter] playground preview";
|
|
116
|
+
console.warn(
|
|
117
|
+
`${label}: ${skipped.length} component(s) have a scan row but no live preview.`,
|
|
118
|
+
);
|
|
119
|
+
for (const s of skipped.slice(0, 12)) {
|
|
120
|
+
const hint =
|
|
121
|
+
s.reason === "module_not_found"
|
|
122
|
+
? `add import.meta.glob key "${s.globKey}" (from rel_path "${s.rel_path}")`
|
|
123
|
+
: `export function ${s.export_name} from "${s.rel_path}"`;
|
|
124
|
+
console.warn(` - ${s.export_name}: ${hint}`);
|
|
125
|
+
}
|
|
126
|
+
if (skipped.length > 12) {
|
|
127
|
+
console.warn(` … and ${skipped.length - 12} more`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function findPlaygroundSpec(
|
|
132
|
+
report: WorkspaceReport | null | undefined,
|
|
133
|
+
componentId: string,
|
|
134
|
+
): PlaygroundSpec | undefined {
|
|
135
|
+
return report?.playgrounds?.find(
|
|
136
|
+
(p) => p.export_name === componentId || p.id === componentId,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function findPlaygroundJoinSkip(
|
|
141
|
+
skipped: PlaygroundJoinSkip[] | undefined,
|
|
142
|
+
componentId: string,
|
|
143
|
+
): PlaygroundJoinSkip | undefined {
|
|
144
|
+
return skipped?.find((s) => s.export_name === componentId);
|
|
145
|
+
}
|
|
@@ -20,6 +20,10 @@ import { TokensPane } from "../components/TokensPane";
|
|
|
20
20
|
import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
|
|
21
21
|
import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
|
|
22
22
|
import { resolvePlaygroundEntry } from "../playground/buildPlaygroundEntriesFromReport";
|
|
23
|
+
import {
|
|
24
|
+
findPlaygroundJoinSkip,
|
|
25
|
+
type PlaygroundJoinSkip,
|
|
26
|
+
} from "../playground/playgroundJoin";
|
|
23
27
|
import { useHashRoute } from "./useHashRoute";
|
|
24
28
|
|
|
25
29
|
const STORAGE_KEY = "dslinter-dashboard-theme";
|
|
@@ -121,6 +125,8 @@ export function useDashboardTheme(): DashboardThemeContextValue {
|
|
|
121
125
|
|
|
122
126
|
export type DashboardLayoutProps = {
|
|
123
127
|
playgroundEntries: PlaygroundEntry[];
|
|
128
|
+
/** Join failures from `buildPlaygroundEntriesFromReportWithSkips` — powers inspect-pane hints. */
|
|
129
|
+
playgroundJoinSkips?: PlaygroundJoinSkip[];
|
|
124
130
|
tokenCatalog?: TokenCatalog;
|
|
125
131
|
/** Custom intro shown above the governance inventory on `#!/governance`; defaults to package copy. */
|
|
126
132
|
overview?: ReactNode;
|
|
@@ -136,6 +142,7 @@ export type DashboardLayoutProps = {
|
|
|
136
142
|
|
|
137
143
|
function DashboardLayoutInner({
|
|
138
144
|
playgroundEntries,
|
|
145
|
+
playgroundJoinSkips,
|
|
139
146
|
tokenCatalog,
|
|
140
147
|
overview,
|
|
141
148
|
reportUrl,
|
|
@@ -202,6 +209,10 @@ function DashboardLayoutInner({
|
|
|
202
209
|
workspaceReport={dslinterReport.report}
|
|
203
210
|
reportReady={reportReady}
|
|
204
211
|
hasPlaygroundSpec={hasPlaygroundSpec}
|
|
212
|
+
playgroundJoinSkip={findPlaygroundJoinSkip(
|
|
213
|
+
playgroundJoinSkips,
|
|
214
|
+
componentId,
|
|
215
|
+
)}
|
|
205
216
|
onBackToGovernance={() => navigate({ view: "governance" })}
|
|
206
217
|
/>
|
|
207
218
|
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlaygroundEntry, WorkspaceReport } from "dslinter";
|
|
2
|
+
import { createPlaygroundRegistry } from "dslinter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registry lives in `resources/js/playground/`.
|
|
6
|
+
* Glob is relative to this file — covers Inertia/React components under `resources/js/`.
|
|
7
|
+
*/
|
|
8
|
+
const modules = import.meta.glob("../**/*.{tsx,jsx}", {
|
|
9
|
+
eager: true,
|
|
10
|
+
}) as Record<string, Record<string, unknown>>;
|
|
11
|
+
|
|
12
|
+
const buildWithSkips = createPlaygroundRegistry(modules, {
|
|
13
|
+
stripPrefixes: ["resources/js/", "src/"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function buildPlaygroundEntries(
|
|
17
|
+
report: WorkspaceReport | null | undefined,
|
|
18
|
+
): PlaygroundEntry[] {
|
|
19
|
+
return buildWithSkips(report).entries;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getPlaygroundJoinSkips(
|
|
23
|
+
report: WorkspaceReport | null | undefined,
|
|
24
|
+
) {
|
|
25
|
+
return buildWithSkips(report).skipped;
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PlaygroundEntry, WorkspaceReport } from "dslinter";
|
|
2
|
+
import { createPlaygroundRegistry } from "dslinter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eager Vite glob — must cover every path in `dslint-report.json` → `playgrounds[].rel_path`.
|
|
6
|
+
* Nested paths (e.g. `src/components/ui/button.tsx`) require `**`, not a single `*`.
|
|
7
|
+
*/
|
|
8
|
+
const modules = import.meta.glob("../components/**/*.{tsx,jsx}", {
|
|
9
|
+
eager: true,
|
|
10
|
+
}) as Record<string, Record<string, unknown>>;
|
|
11
|
+
|
|
12
|
+
const buildWithSkips = createPlaygroundRegistry(modules);
|
|
13
|
+
|
|
14
|
+
/** Live previews for the dashboard (`DashboardLayout` → `playgroundEntries`). */
|
|
15
|
+
export function buildPlaygroundEntries(
|
|
16
|
+
report: WorkspaceReport | null | undefined,
|
|
17
|
+
): PlaygroundEntry[] {
|
|
18
|
+
return buildWithSkips(report).entries;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Skipped joins (module glob / export mismatch) — useful for debugging previews. */
|
|
22
|
+
export function getPlaygroundJoinSkips(
|
|
23
|
+
report: WorkspaceReport | null | undefined,
|
|
24
|
+
) {
|
|
25
|
+
return buildWithSkips(report).skipped;
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add to `vite.config.ts` when using the `@dslint-scan` embed-style glob in your App:
|
|
3
|
+
*
|
|
4
|
+
* import path from "node:path";
|
|
5
|
+
* const scanRoot = path.resolve(process.env.DSLINT_SCAN_ROOT ?? ".");
|
|
6
|
+
*
|
|
7
|
+
* resolve: {
|
|
8
|
+
* alias: {
|
|
9
|
+
* "@dslint-scan": scanRoot,
|
|
10
|
+
* },
|
|
11
|
+
* },
|
|
12
|
+
* server: {
|
|
13
|
+
* fs: { allow: [scanRoot] },
|
|
14
|
+
* },
|
|
15
|
+
*
|
|
16
|
+
* App:
|
|
17
|
+
* const modules = import.meta.glob("@dslint-scan/**/*.{tsx,jsx}", { eager: true });
|
|
18
|
+
* buildPlaygroundEntriesFromReport(report, modules);
|
|
19
|
+
*
|
|
20
|
+
* Prefer `npx dslinter init --laravel` (relative glob) when you have your own Vite app.
|
|
21
|
+
*/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add to your existing `vite.config.ts` when using `npx dslinter` dev mode.
|
|
3
|
+
* Adjust paths if your app layout differs from `src/` + `public/dslint-report.json`.
|
|
4
|
+
*/
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const DSLINT_SERVE_PORT = Number(process.env.DSLINT_SERVE_PORT ?? "7878");
|
|
8
|
+
|
|
9
|
+
// Inside defineConfig(({ mode }) => ({ ... })):
|
|
10
|
+
export const dslinterViteSnippet = {
|
|
11
|
+
resolve: {
|
|
12
|
+
dedupe: ["react", "react-dom"],
|
|
13
|
+
},
|
|
14
|
+
server: {
|
|
15
|
+
proxy:
|
|
16
|
+
// mode === "serve" when started via `npx dslinter` (not plain `vite`)
|
|
17
|
+
{
|
|
18
|
+
"/dslint-report.json": {
|
|
19
|
+
target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
|
|
20
|
+
changeOrigin: true,
|
|
21
|
+
},
|
|
22
|
+
"/events": {
|
|
23
|
+
target: `http://127.0.0.1:${DSLINT_SERVE_PORT}`,
|
|
24
|
+
changeOrigin: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|