dslinter 0.1.13 → 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 +43 -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 +96 -8
- package/bin/lib/scaffold-config.test.mjs +12 -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 +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-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,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-scan enrichment: fill playground prop kinds/options from TypeScript.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
createCheckerProgram,
|
|
8
|
+
inferDeclaredPropsFromTsx,
|
|
9
|
+
inferPlaygroundPropMetadata,
|
|
10
|
+
} from "./infer-prop-types-from-ts.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {Record<string, unknown>} report
|
|
14
|
+
* @param {string} projectRoot
|
|
15
|
+
* @returns {boolean} true when any playground row changed
|
|
16
|
+
*/
|
|
17
|
+
export function enrichWorkspaceReport(report, projectRoot) {
|
|
18
|
+
const playgrounds = report.playgrounds;
|
|
19
|
+
if (!Array.isArray(playgrounds) || playgrounds.length === 0) return false;
|
|
20
|
+
|
|
21
|
+
const checkerBundle = createCheckerProgram(projectRoot);
|
|
22
|
+
if (!checkerBundle) return false;
|
|
23
|
+
|
|
24
|
+
const { program, checker } = checkerBundle;
|
|
25
|
+
let changed = false;
|
|
26
|
+
|
|
27
|
+
for (const spec of playgrounds) {
|
|
28
|
+
if (!spec || typeof spec !== "object") continue;
|
|
29
|
+
|
|
30
|
+
let declaredProps = Array.isArray(spec.declared_props) ? [...spec.declared_props] : [];
|
|
31
|
+
const relPath = typeof spec.rel_path === "string" ? spec.rel_path : "";
|
|
32
|
+
const exportName = typeof spec.export_name === "string" ? spec.export_name : spec.id;
|
|
33
|
+
|
|
34
|
+
if (typeof exportName !== "string" || !relPath) continue;
|
|
35
|
+
|
|
36
|
+
const inferred = inferDeclaredPropsFromTsx(projectRoot, relPath, exportName);
|
|
37
|
+
if (inferred.length && (!declaredProps.length || inferred.length > declaredProps.length)) {
|
|
38
|
+
declaredProps = inferred;
|
|
39
|
+
spec.declared_props = declaredProps;
|
|
40
|
+
changed = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const before = JSON.stringify({
|
|
44
|
+
kinds: spec.declared_prop_kinds ?? {},
|
|
45
|
+
options: spec.declared_prop_options ?? {},
|
|
46
|
+
defaults: spec.declared_prop_defaults ?? {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const meta = inferPlaygroundPropMetadata(
|
|
50
|
+
checker,
|
|
51
|
+
program,
|
|
52
|
+
projectRoot,
|
|
53
|
+
relPath,
|
|
54
|
+
exportName,
|
|
55
|
+
declaredProps,
|
|
56
|
+
{
|
|
57
|
+
declared_prop_kinds: spec.declared_prop_kinds,
|
|
58
|
+
declared_prop_options: spec.declared_prop_options,
|
|
59
|
+
declared_prop_defaults: spec.declared_prop_defaults,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (Object.keys(meta.declared_prop_kinds).length) {
|
|
64
|
+
spec.declared_prop_kinds = meta.declared_prop_kinds;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(meta.declared_prop_options).length) {
|
|
67
|
+
spec.declared_prop_options = meta.declared_prop_options;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(meta.declared_prop_defaults).length) {
|
|
70
|
+
spec.declared_prop_defaults = meta.declared_prop_defaults;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const after = JSON.stringify({
|
|
74
|
+
kinds: spec.declared_prop_kinds ?? {},
|
|
75
|
+
options: spec.declared_prop_options ?? {},
|
|
76
|
+
defaults: spec.declared_prop_defaults ?? {},
|
|
77
|
+
});
|
|
78
|
+
if (before !== after) changed = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return changed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} reportPath
|
|
86
|
+
* @param {string} projectRoot
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function enrichReportFile(reportPath, projectRoot) {
|
|
90
|
+
let report;
|
|
91
|
+
try {
|
|
92
|
+
report = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const changed = enrichWorkspaceReport(report, projectRoot);
|
|
98
|
+
if (!changed) return false;
|
|
99
|
+
|
|
100
|
+
const json = `${JSON.stringify(report, null, 2)}\n`;
|
|
101
|
+
const dir = dirname(reportPath);
|
|
102
|
+
const tmp = join(dir, `.dslinter-enrich-${process.pid}.tmp`);
|
|
103
|
+
writeFileSync(tmp, json);
|
|
104
|
+
renameSync(tmp, reportPath);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} reportPath
|
|
110
|
+
* @param {string} projectRoot
|
|
111
|
+
*/
|
|
112
|
+
export function enrichReportFileBestEffort(reportPath, projectRoot) {
|
|
113
|
+
try {
|
|
114
|
+
enrichReportFile(reportPath, projectRoot);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (process.env.DSLINTER_QUIET !== "1") {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
process.stderr.write(`dslinter: TS playground enrich skipped (${msg})\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** @typedef {{ projectRoot: string; reportPath: string; logPrefix?: string }} EnrichOptions */
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* CLI-facing enrichment used by report, build, dev, and watch modes.
|
|
127
|
+
*
|
|
128
|
+
* @param {EnrichOptions} opts
|
|
129
|
+
* @returns {Promise<boolean>} true when enrichment ran and wrote the report
|
|
130
|
+
*/
|
|
131
|
+
export async function enrichPlaygroundsFromTs({
|
|
132
|
+
projectRoot,
|
|
133
|
+
reportPath,
|
|
134
|
+
logPrefix = "dslinter",
|
|
135
|
+
}) {
|
|
136
|
+
if (!createCheckerProgram(projectRoot)) {
|
|
137
|
+
if (process.env.DSLINTER_DEBUG?.trim() === "1") {
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`${logPrefix}: skip playground TS enrichment (no tsconfig.json)\n`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
return enrichReportFile(reportPath, projectRoot);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (process.env.DSLINTER_DEBUG?.trim() === "1") {
|
|
149
|
+
process.stderr.write(
|
|
150
|
+
`${logPrefix}: skip playground TS enrichment (${err instanceof Error ? err.message : err})\n`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Poll the report file and re-run TS enrichment after the scanner writes JSON.
|
|
159
|
+
*
|
|
160
|
+
* @param {EnrichOptions & { pollMs?: number }} opts
|
|
161
|
+
* @returns {() => void} stop function
|
|
162
|
+
*/
|
|
163
|
+
export function watchEnrichPlaygroundsFromTs({
|
|
164
|
+
projectRoot,
|
|
165
|
+
reportPath,
|
|
166
|
+
logPrefix = "dslinter",
|
|
167
|
+
pollMs = 300,
|
|
168
|
+
}) {
|
|
169
|
+
let lastMtimeMs = 0;
|
|
170
|
+
let running = false;
|
|
171
|
+
let stopped = false;
|
|
172
|
+
|
|
173
|
+
const tick = async () => {
|
|
174
|
+
if (stopped || running) return;
|
|
175
|
+
running = true;
|
|
176
|
+
try {
|
|
177
|
+
const { statSync } = await import("node:fs");
|
|
178
|
+
let mtimeMs;
|
|
179
|
+
try {
|
|
180
|
+
mtimeMs = statSync(reportPath).mtimeMs;
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (mtimeMs <= lastMtimeMs) return;
|
|
185
|
+
lastMtimeMs = mtimeMs;
|
|
186
|
+
await enrichPlaygroundsFromTs({ projectRoot, reportPath, logPrefix });
|
|
187
|
+
} finally {
|
|
188
|
+
running = false;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const interval = setInterval(() => {
|
|
193
|
+
void tick();
|
|
194
|
+
}, pollMs);
|
|
195
|
+
void tick();
|
|
196
|
+
|
|
197
|
+
return () => {
|
|
198
|
+
stopped = true;
|
|
199
|
+
clearInterval(interval);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { enrichReportFile } from "./enrich-playgrounds-from-ts.mjs";
|
|
7
|
+
|
|
8
|
+
describe("enrich-playgrounds-from-ts", () => {
|
|
9
|
+
it("writes declared_prop_options for Input.type into report file", () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-enrich-"));
|
|
11
|
+
const reportPath = join(root, "report.json");
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(
|
|
14
|
+
reportPath,
|
|
15
|
+
JSON.stringify(
|
|
16
|
+
{
|
|
17
|
+
root,
|
|
18
|
+
playgrounds: [
|
|
19
|
+
{
|
|
20
|
+
id: "Input",
|
|
21
|
+
export_name: "Input",
|
|
22
|
+
rel_path: "input.tsx",
|
|
23
|
+
declared_props: ["type"],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
null,
|
|
28
|
+
2,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(root, "tsconfig.json"),
|
|
34
|
+
JSON.stringify(
|
|
35
|
+
{
|
|
36
|
+
compilerOptions: {
|
|
37
|
+
strict: true,
|
|
38
|
+
jsx: "react-jsx",
|
|
39
|
+
module: "ESNext",
|
|
40
|
+
moduleResolution: "bundler",
|
|
41
|
+
noEmit: true,
|
|
42
|
+
skipLibCheck: true,
|
|
43
|
+
typeRoots: [
|
|
44
|
+
join(process.cwd(), "node_modules/@types"),
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
include: ["**/*.tsx"],
|
|
48
|
+
},
|
|
49
|
+
null,
|
|
50
|
+
2,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(root, "input.tsx"),
|
|
56
|
+
`
|
|
57
|
+
import * as React from "react";
|
|
58
|
+
function Input({ type }: React.ComponentProps<"input">) {
|
|
59
|
+
return <input type={type} />;
|
|
60
|
+
}
|
|
61
|
+
export { Input };
|
|
62
|
+
`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(enrichReportFile(reportPath, root)).toBe(true);
|
|
66
|
+
const report = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
67
|
+
const input = report.playgrounds[0];
|
|
68
|
+
expect(input.declared_prop_options.type).toContain("email");
|
|
69
|
+
expect(input.declared_prop_defaults.type).toBe("text");
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry for Rust watch post-write hook and manual enrichment.
|
|
4
|
+
* Usage: node enrich-report-cli.mjs <reportPath> <projectRoot>
|
|
5
|
+
*/
|
|
6
|
+
import { enrichReportFileBestEffort } from "./enrich-playgrounds-from-ts.mjs";
|
|
7
|
+
|
|
8
|
+
const [reportPath, projectRoot] = process.argv.slice(2);
|
|
9
|
+
if (!reportPath || !projectRoot) {
|
|
10
|
+
process.stderr.write("usage: enrich-report-cli.mjs <reportPath> <projectRoot>\n");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
enrichReportFileBestEffort(reportPath, projectRoot);
|
package/bin/lib/env.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @typedef {import("node:process").ProcessEnv} ProcessEnv */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read `DSLINTER_*` environment variables.
|
|
5
|
+
* @param {string} name Suffix after the prefix, e.g. `"SERVE_PORT"`.
|
|
6
|
+
* @param {ProcessEnv} [env]
|
|
7
|
+
* @returns {string | undefined}
|
|
8
|
+
*/
|
|
9
|
+
export function readEnv(name, env = process.env) {
|
|
10
|
+
return env[`DSLINTER_${name}`]?.trim() || undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @param {string} [value]
|
|
16
|
+
* @param {ProcessEnv} [env]
|
|
17
|
+
*/
|
|
18
|
+
export function envIs(name, value = "1", env = process.env) {
|
|
19
|
+
return readEnv(name, env) === value;
|
|
20
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript checker helpers for playground prop kinds and finite string unions.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
|
|
8
|
+
/** @typedef {"boolean" | "string" | "number"} DeclaredPropKind */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} projectRoot
|
|
12
|
+
* @returns {{ program: import("typescript").Program; checker: import("typescript").TypeChecker } | null}
|
|
13
|
+
*/
|
|
14
|
+
export function createCheckerProgram(projectRoot) {
|
|
15
|
+
const configPath = resolve(projectRoot, "tsconfig.json");
|
|
16
|
+
if (!existsSync(configPath)) return null;
|
|
17
|
+
|
|
18
|
+
const readJson = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
19
|
+
if (readJson.error) return null;
|
|
20
|
+
|
|
21
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
22
|
+
readJson.config,
|
|
23
|
+
ts.sys,
|
|
24
|
+
projectRoot,
|
|
25
|
+
undefined,
|
|
26
|
+
configPath,
|
|
27
|
+
);
|
|
28
|
+
if (parsed.errors.length) return null;
|
|
29
|
+
|
|
30
|
+
const program = ts.createProgram({
|
|
31
|
+
rootNames: parsed.fileNames,
|
|
32
|
+
options: { ...parsed.options, noCheck: false },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { program, checker: program.getTypeChecker() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {import("typescript").Node} node
|
|
40
|
+
*/
|
|
41
|
+
function hasExportModifier(node) {
|
|
42
|
+
return node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {import("typescript").SourceFile} sf
|
|
47
|
+
* @returns {Set<string>}
|
|
48
|
+
*/
|
|
49
|
+
function collectNamedExportNames(sf) {
|
|
50
|
+
/** @type {Set<string>} */
|
|
51
|
+
const names = new Set();
|
|
52
|
+
|
|
53
|
+
function visit(node) {
|
|
54
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
55
|
+
for (const el of node.exportClause.elements) {
|
|
56
|
+
names.add(el.name.text);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
ts.forEachChild(node, visit);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
visit(sf);
|
|
63
|
+
return names;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {import("typescript").CallExpression} call
|
|
68
|
+
* @returns {import("typescript").ParameterDeclaration | undefined}
|
|
69
|
+
*/
|
|
70
|
+
function firstParamFromForwardRefCall(call) {
|
|
71
|
+
const callee = call.expression;
|
|
72
|
+
const isForwardRef =
|
|
73
|
+
ts.isIdentifier(callee) &&
|
|
74
|
+
(callee.text === "forwardRef" || callee.text.endsWith(".forwardRef"));
|
|
75
|
+
if (!isForwardRef) return undefined;
|
|
76
|
+
|
|
77
|
+
const arg = call.arguments[0];
|
|
78
|
+
if (!arg || (!ts.isArrowFunction(arg) && !ts.isFunctionExpression(arg))) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return arg.parameters[0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the first parameter type for a component export.
|
|
86
|
+
*
|
|
87
|
+
* @param {import("typescript").TypeChecker} checker
|
|
88
|
+
* @param {import("typescript").SourceFile} sf
|
|
89
|
+
* @param {string} exportName
|
|
90
|
+
* @returns {import("typescript").Type | undefined}
|
|
91
|
+
*/
|
|
92
|
+
export function findComponentParamType(checker, sf, exportName) {
|
|
93
|
+
const namedExports = collectNamedExportNames(sf);
|
|
94
|
+
|
|
95
|
+
/** @type {import("typescript").Type | undefined} */
|
|
96
|
+
let found;
|
|
97
|
+
|
|
98
|
+
function isTargetExport(hasDirectExportModifier) {
|
|
99
|
+
return hasDirectExportModifier || namedExports.has(exportName);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function visit(node) {
|
|
103
|
+
if (found !== undefined) return;
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
ts.isFunctionDeclaration(node) &&
|
|
107
|
+
node.name?.text === exportName &&
|
|
108
|
+
isTargetExport(hasExportModifier(node))
|
|
109
|
+
) {
|
|
110
|
+
const p0 = node.parameters[0];
|
|
111
|
+
if (p0) found = checker.getTypeAtLocation(p0);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ts.isVariableStatement(node)) {
|
|
116
|
+
if (!isTargetExport(hasExportModifier(node))) return;
|
|
117
|
+
|
|
118
|
+
for (const decl of node.declarationList.declarations) {
|
|
119
|
+
if (!ts.isIdentifier(decl.name) || decl.name.text !== exportName || !decl.initializer) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const init = decl.initializer;
|
|
124
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) {
|
|
125
|
+
const p0 = init.parameters[0];
|
|
126
|
+
if (p0) {
|
|
127
|
+
found = checker.getTypeAtLocation(p0);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (ts.isCallExpression(init)) {
|
|
133
|
+
const p0 = firstParamFromForwardRefCall(init);
|
|
134
|
+
if (p0) {
|
|
135
|
+
found = checker.getTypeAtLocation(p0);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ts.forEachChild(node, visit);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
visit(sf);
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {import("typescript").TypeChecker} checker
|
|
151
|
+
* @param {import("typescript").Type} type
|
|
152
|
+
* @returns {DeclaredPropKind | null}
|
|
153
|
+
*/
|
|
154
|
+
export function classifyPropType(checker, type) {
|
|
155
|
+
const nn = checker.getNonNullableType(type);
|
|
156
|
+
if (nn.isUnion()) {
|
|
157
|
+
const parts = nn.types.map((u) =>
|
|
158
|
+
classifyPropType(checker, checker.getNonNullableType(u)),
|
|
159
|
+
);
|
|
160
|
+
const ok = parts.filter((p) => p !== null);
|
|
161
|
+
if (!ok.length) return null;
|
|
162
|
+
const set = new Set(ok);
|
|
163
|
+
if (set.size === 1) return [...set][0];
|
|
164
|
+
if ([...set].every((x) => x === "string")) return "string";
|
|
165
|
+
if ([...set].every((x) => x === "number")) return "number";
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (nn.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return "boolean";
|
|
169
|
+
if (nn.flags & (ts.TypeFlags.Enum | ts.TypeFlags.EnumLiteral)) return "string";
|
|
170
|
+
if (nn.flags & (ts.TypeFlags.Number | ts.TypeFlags.NumberLike)) return "number";
|
|
171
|
+
if (nn.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLike)) return "string";
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {import("typescript").TypeChecker} checker
|
|
177
|
+
* @param {import("typescript").Type} type
|
|
178
|
+
*/
|
|
179
|
+
function isPlainStringType(checker, type) {
|
|
180
|
+
const nn = checker.getNonNullableType(type);
|
|
181
|
+
return (
|
|
182
|
+
(nn.flags & ts.TypeFlags.String) !== 0 &&
|
|
183
|
+
(nn.flags & ts.TypeFlags.StringLiteral) === 0
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* React and other libs use `string & {}` as an open-string catch-all alongside literals.
|
|
189
|
+
*
|
|
190
|
+
* @param {import("typescript").TypeChecker} checker
|
|
191
|
+
* @param {import("typescript").Type} type
|
|
192
|
+
*/
|
|
193
|
+
function isOpenStringCatchAllType(checker, type) {
|
|
194
|
+
const nn = checker.getNonNullableType(type);
|
|
195
|
+
if (isPlainStringType(checker, nn)) return true;
|
|
196
|
+
if (!nn.isIntersection?.()) return false;
|
|
197
|
+
const parts = nn.types.map((t) => checker.getNonNullableType(t));
|
|
198
|
+
const hasString = parts.some((p) => (p.flags & ts.TypeFlags.String) !== 0);
|
|
199
|
+
const hasEmptyObject = parts.some((p) => checker.typeToString(p) === "{}");
|
|
200
|
+
return hasString && hasEmptyObject;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract a finite union of string literals suitable for a select control.
|
|
205
|
+
*
|
|
206
|
+
* @param {import("typescript").TypeChecker} checker
|
|
207
|
+
* @param {import("typescript").Type} type
|
|
208
|
+
* @param {{ max?: number; seen?: Set<number> }} [opts]
|
|
209
|
+
* @returns {string[] | null}
|
|
210
|
+
*/
|
|
211
|
+
export function extractFiniteStringUnion(checker, type, opts = {}) {
|
|
212
|
+
const max = opts.max ?? 32;
|
|
213
|
+
const seen = opts.seen ?? new Set();
|
|
214
|
+
|
|
215
|
+
const nn = checker.getNonNullableType(type);
|
|
216
|
+
if (seen.has(nn.id)) return null;
|
|
217
|
+
seen.add(nn.id);
|
|
218
|
+
|
|
219
|
+
if (nn.isUnion?.()) {
|
|
220
|
+
/** @type {string[]} */
|
|
221
|
+
const literals = [];
|
|
222
|
+
for (const member of nn.types) {
|
|
223
|
+
if (isOpenStringCatchAllType(checker, member)) continue;
|
|
224
|
+
const part = extractFiniteStringUnion(checker, member, { max, seen });
|
|
225
|
+
if (part === null) {
|
|
226
|
+
if (isPlainStringType(checker, member)) return null;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
literals.push(...part);
|
|
230
|
+
}
|
|
231
|
+
const unique = [...new Set(literals)];
|
|
232
|
+
if (unique.length < 2) return null;
|
|
233
|
+
unique.sort();
|
|
234
|
+
return unique.slice(0, max);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (nn.aliasSymbol) {
|
|
238
|
+
const aliasType = checker.getDeclaredTypeOfSymbol(nn.aliasSymbol);
|
|
239
|
+
if (aliasType.id !== nn.id) {
|
|
240
|
+
return extractFiniteStringUnion(checker, aliasType, { max, seen });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (nn.isStringLiteral()) {
|
|
245
|
+
return [nn.value];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (nn.flags & ts.TypeFlags.EnumLiteral) {
|
|
249
|
+
const value = nn.value;
|
|
250
|
+
if (typeof value === "string") return [value];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {string} projectRoot
|
|
258
|
+
* @param {string} relPath
|
|
259
|
+
* @returns {import("typescript").SourceFile | undefined}
|
|
260
|
+
*/
|
|
261
|
+
function sourceFileForRelPath(program, projectRoot, relPath) {
|
|
262
|
+
const abs = resolve(projectRoot, relPath);
|
|
263
|
+
const norm = (p) => p.replace(/\\/g, "/");
|
|
264
|
+
const want = norm(abs);
|
|
265
|
+
return program.getSourceFiles().find((f) => norm(f.fileName) === want);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {import("typescript").TypeChecker} checker
|
|
270
|
+
* @param {import("typescript").Program} program
|
|
271
|
+
* @param {string} projectRoot
|
|
272
|
+
* @param {string} relPath
|
|
273
|
+
* @param {string} exportName
|
|
274
|
+
* @param {string[]} declaredProps
|
|
275
|
+
* @param {{
|
|
276
|
+
* declared_prop_options?: Record<string, string[]>;
|
|
277
|
+
* declared_prop_defaults?: Record<string, string>;
|
|
278
|
+
* declared_prop_kinds?: Record<string, string>;
|
|
279
|
+
* }} [existing]
|
|
280
|
+
*/
|
|
281
|
+
export function inferPlaygroundPropMetadata(
|
|
282
|
+
checker,
|
|
283
|
+
program,
|
|
284
|
+
projectRoot,
|
|
285
|
+
relPath,
|
|
286
|
+
exportName,
|
|
287
|
+
declaredProps,
|
|
288
|
+
existing = {},
|
|
289
|
+
) {
|
|
290
|
+
const sf = sourceFileForRelPath(program, projectRoot, relPath);
|
|
291
|
+
if (!sf) {
|
|
292
|
+
return {
|
|
293
|
+
declared_prop_kinds: existing.declared_prop_kinds ?? {},
|
|
294
|
+
declared_prop_options: existing.declared_prop_options ?? {},
|
|
295
|
+
declared_prop_defaults: existing.declared_prop_defaults ?? {},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const paramType = findComponentParamType(checker, sf, exportName);
|
|
300
|
+
if (!paramType) {
|
|
301
|
+
return {
|
|
302
|
+
declared_prop_kinds: existing.declared_prop_kinds ?? {},
|
|
303
|
+
declared_prop_options: existing.declared_prop_options ?? {},
|
|
304
|
+
declared_prop_defaults: existing.declared_prop_defaults ?? {},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** @type {Record<string, DeclaredPropKind>} */
|
|
309
|
+
const kinds = { ...existing.declared_prop_kinds };
|
|
310
|
+
/** @type {Record<string, string[]>} */
|
|
311
|
+
const options = { ...existing.declared_prop_options };
|
|
312
|
+
/** @type {Record<string, string>} */
|
|
313
|
+
const defaults = { ...existing.declared_prop_defaults };
|
|
314
|
+
|
|
315
|
+
for (const key of declaredProps) {
|
|
316
|
+
if (key === "key" || key === "ref") continue;
|
|
317
|
+
|
|
318
|
+
const sym = checker.getPropertyOfType(paramType, key);
|
|
319
|
+
if (!sym) continue;
|
|
320
|
+
const propType = checker.getTypeOfSymbol(sym);
|
|
321
|
+
|
|
322
|
+
if (!kinds[key]) {
|
|
323
|
+
const kind = classifyPropType(checker, propType);
|
|
324
|
+
if (kind !== null) kinds[key] = kind;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!options[key]) {
|
|
328
|
+
const literals = extractFiniteStringUnion(checker, propType);
|
|
329
|
+
if (literals && literals.length >= 2) {
|
|
330
|
+
options[key] = literals;
|
|
331
|
+
if (key === "type" && literals.includes("text") && !defaults[key]) {
|
|
332
|
+
defaults[key] = "text";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
declared_prop_kinds: kinds,
|
|
340
|
+
declared_prop_options: options,
|
|
341
|
+
declared_prop_defaults: defaults,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function inferDeclaredPropsFromTsx(projectRoot, relPath, exportName) {
|
|
346
|
+
const abs = resolve(projectRoot, relPath);
|
|
347
|
+
let text;
|
|
348
|
+
try {
|
|
349
|
+
text = readFileSync(abs, "utf8");
|
|
350
|
+
} catch {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
355
|
+
/** @type {string[]} */
|
|
356
|
+
const names = [];
|
|
357
|
+
|
|
358
|
+
function visit(node) {
|
|
359
|
+
if (
|
|
360
|
+
ts.isFunctionDeclaration(node) &&
|
|
361
|
+
node.name?.text === exportName &&
|
|
362
|
+
(hasExportModifier(node) || collectNamedExportNames(sf).has(exportName))
|
|
363
|
+
) {
|
|
364
|
+
const p0 = node.parameters[0];
|
|
365
|
+
if (p0 && ts.isObjectBindingPattern(p0.name)) {
|
|
366
|
+
for (const el of p0.name.elements) {
|
|
367
|
+
if (!ts.isBindingElement(el)) continue;
|
|
368
|
+
if (el.propertyName && ts.isIdentifier(el.propertyName)) {
|
|
369
|
+
names.push(el.propertyName.text);
|
|
370
|
+
} else if (ts.isIdentifier(el.name)) {
|
|
371
|
+
names.push(el.name.text);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
ts.forEachChild(node, visit);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
visit(sf);
|
|
380
|
+
return [...new Set(names)].sort();
|
|
381
|
+
}
|