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
package/bin/lib/run-scanner.mjs
CHANGED
|
@@ -3,14 +3,38 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readEnv, envIs } from "./env.mjs";
|
|
6
7
|
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const packageRoot = join(__dirname, "../..");
|
|
9
10
|
const binScript = join(__dirname, "../dslinter.mjs");
|
|
11
|
+
const enrichScript = join(__dirname, "enrich-report-cli.mjs");
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
13
|
|
|
12
14
|
const SCANNER_VERSION_MARKER = "design system linting";
|
|
13
15
|
|
|
16
|
+
/** Env vars for the Rust watch post-write TS enrich hook. */
|
|
17
|
+
export function scannerEnrichEnv(projectRoot) {
|
|
18
|
+
if (process.env.DSLINTER_SKIP_TS_ENRICH === "1") {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
/** @type {Record<string, string>} */
|
|
22
|
+
const env = {
|
|
23
|
+
DSLINTER_ENRICH_SCRIPT: enrichScript,
|
|
24
|
+
DSLINTER_NODE: process.execPath,
|
|
25
|
+
};
|
|
26
|
+
if (projectRoot) {
|
|
27
|
+
env.DSLINTER_PROJECT_ROOT = projectRoot;
|
|
28
|
+
}
|
|
29
|
+
return env;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyEnrichEnv(projectRoot) {
|
|
33
|
+
for (const [key, value] of Object.entries(scannerEnrichEnv(projectRoot))) {
|
|
34
|
+
process.env[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
14
38
|
function isOurScanner(binary) {
|
|
15
39
|
const help = spawnSync(binary, ["--help"], { encoding: "utf8" });
|
|
16
40
|
const out = `${help.stdout ?? ""}${help.stderr ?? ""}`;
|
|
@@ -21,17 +45,24 @@ function isOurScanner(binary) {
|
|
|
21
45
|
* @returns {Promise<import("node:child_process").ChildProcess>}
|
|
22
46
|
*/
|
|
23
47
|
export async function spawnScanner(scannerArgs, options = {}) {
|
|
24
|
-
const
|
|
48
|
+
const projectRoot = options.projectRoot ?? process.env.DSLINTER_PROJECT_ROOT;
|
|
49
|
+
const enrichEnv = scannerEnrichEnv(projectRoot);
|
|
50
|
+
const fromEnv = readEnv("BIN");
|
|
25
51
|
if (fromEnv) {
|
|
26
52
|
if (!existsSync(fromEnv)) {
|
|
27
|
-
throw new Error(`dslinter:
|
|
53
|
+
throw new Error(`dslinter: DSLINTER_BIN not found: ${fromEnv}`);
|
|
28
54
|
}
|
|
29
55
|
if (!isOurScanner(fromEnv)) {
|
|
30
|
-
throw new Error("dslinter:
|
|
56
|
+
throw new Error("dslinter: DSLINTER_BIN does not look like the DSLinter scanner");
|
|
31
57
|
}
|
|
32
58
|
return spawn(fromEnv, scannerArgs, {
|
|
33
59
|
stdio: "inherit",
|
|
34
60
|
...options,
|
|
61
|
+
env: {
|
|
62
|
+
...process.env,
|
|
63
|
+
...enrichEnv,
|
|
64
|
+
...options.env,
|
|
65
|
+
},
|
|
35
66
|
});
|
|
36
67
|
}
|
|
37
68
|
|
|
@@ -41,6 +72,7 @@ export async function spawnScanner(scannerArgs, options = {}) {
|
|
|
41
72
|
env: {
|
|
42
73
|
...process.env,
|
|
43
74
|
DSLINTER_INTERNAL: "1",
|
|
75
|
+
...enrichEnv,
|
|
44
76
|
...options.env,
|
|
45
77
|
},
|
|
46
78
|
});
|
|
@@ -53,7 +85,8 @@ export async function spawnScanner(scannerArgs, options = {}) {
|
|
|
53
85
|
* @returns {number}
|
|
54
86
|
*/
|
|
55
87
|
export function runScannerSync(scannerArgs, opts = {}) {
|
|
56
|
-
|
|
88
|
+
applyEnrichEnv(opts.projectRoot);
|
|
89
|
+
const fromEnv = readEnv("BIN");
|
|
57
90
|
if (fromEnv) {
|
|
58
91
|
const child = spawnSync(fromEnv, scannerArgs, {
|
|
59
92
|
stdio: opts.captureStdout ? ["ignore", "pipe", "inherit"] : "inherit",
|
|
@@ -105,13 +138,14 @@ export function runScannerSync(scannerArgs, opts = {}) {
|
|
|
105
138
|
* @param {string[]} args
|
|
106
139
|
*/
|
|
107
140
|
export function runScannerInternal(args) {
|
|
108
|
-
|
|
141
|
+
applyEnrichEnv(process.env.DSLINTER_PROJECT_ROOT);
|
|
142
|
+
const fromEnv = readEnv("BIN");
|
|
109
143
|
if (fromEnv) {
|
|
110
144
|
const child = spawnSync(fromEnv, args, { stdio: "inherit" });
|
|
111
145
|
process.exit(child.status === null ? 1 : child.status);
|
|
112
146
|
}
|
|
113
147
|
|
|
114
|
-
if (
|
|
148
|
+
if (envIs("ALLOW_PATH")) {
|
|
115
149
|
const onPath = spawnSync("dslinter", ["--help"], { encoding: "utf8" });
|
|
116
150
|
const out = `${onPath.stdout ?? ""}${onPath.stderr ?? ""}`;
|
|
117
151
|
if (onPath.status === 0 && out.includes(SCANNER_VERSION_MARKER)) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_FILE_NAMES,
|
|
5
|
+
DEFAULT_CONFIG_FILE_NAME,
|
|
6
|
+
} from "./paths.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} targetDir
|
|
10
|
+
* @returns {"laravel" | "default"}
|
|
11
|
+
*/
|
|
12
|
+
export function detectInitLayout(targetDir) {
|
|
13
|
+
if (existsSync(join(targetDir, "resources", "js"))) return "laravel";
|
|
14
|
+
return "default";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} targetDir
|
|
19
|
+
* @returns {string | null}
|
|
20
|
+
*/
|
|
21
|
+
export function findDslintConfigPath(targetDir) {
|
|
22
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
23
|
+
const candidate = join(targetDir, name);
|
|
24
|
+
if (existsSync(candidate)) return candidate;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} targetDir
|
|
31
|
+
* @param {string[]} candidates
|
|
32
|
+
* @returns {string[]}
|
|
33
|
+
*/
|
|
34
|
+
function existingPaths(targetDir, candidates) {
|
|
35
|
+
return candidates.filter((rel) => existsSync(join(targetDir, rel)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Prefer lowercase `resources/js/components` for playground_groups when both casings exist.
|
|
40
|
+
* @param {string} targetDir
|
|
41
|
+
* @param {"laravel" | "default"} layout
|
|
42
|
+
* @param {string[]} includeDirs
|
|
43
|
+
* @returns {string | undefined}
|
|
44
|
+
*/
|
|
45
|
+
function pickPlaygroundGroupPrefix(targetDir, layout, includeDirs) {
|
|
46
|
+
if (layout === "laravel") {
|
|
47
|
+
const lower = "resources/js/components";
|
|
48
|
+
const upper = "resources/js/Components";
|
|
49
|
+
if (existsSync(join(targetDir, lower))) return lower;
|
|
50
|
+
if (existsSync(join(targetDir, upper))) return upper;
|
|
51
|
+
}
|
|
52
|
+
return includeDirs[0];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} targetDir
|
|
57
|
+
* @param {"laravel" | "default"} layout
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Pick the narrowest existing directory from ordered candidates (first match wins).
|
|
61
|
+
* @param {string} targetDir
|
|
62
|
+
* @param {string[]} candidates ordered narrow → broad
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
function narrowestIncludeDir(targetDir, candidates) {
|
|
66
|
+
for (const rel of candidates) {
|
|
67
|
+
if (existsSync(join(targetDir, rel))) return [rel];
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} targetDir
|
|
74
|
+
* @param {"laravel" | "default"} layout
|
|
75
|
+
* @returns {string | undefined}
|
|
76
|
+
*/
|
|
77
|
+
export function detectDefaultIncludeDir(targetDir, layout) {
|
|
78
|
+
const candidates =
|
|
79
|
+
layout === "laravel"
|
|
80
|
+
? ["resources/js/components", "resources/js/Components", "resources/js"]
|
|
81
|
+
: ["src/components", "src/ui", "src"];
|
|
82
|
+
return narrowestIncludeDir(targetDir, candidates)[0];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildStarterConfig(targetDir, layout) {
|
|
86
|
+
const includeCandidates =
|
|
87
|
+
layout === "laravel"
|
|
88
|
+
? ["resources/js/components", "resources/js/Components", "resources/js"]
|
|
89
|
+
: ["src/components", "src/ui", "src"];
|
|
90
|
+
const cssCandidates =
|
|
91
|
+
layout === "laravel"
|
|
92
|
+
? ["resources/css/app.css", "src/index.css"]
|
|
93
|
+
: ["src/index.css", "src/styles.css", "src/app.css", "app/globals.css"];
|
|
94
|
+
|
|
95
|
+
const includeDirs = narrowestIncludeDir(targetDir, includeCandidates);
|
|
96
|
+
const cssEntrypoints = existingPaths(targetDir, cssCandidates);
|
|
97
|
+
const groupPrefix = pickPlaygroundGroupPrefix(targetDir, layout, includeDirs);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
include_dirs: includeDirs,
|
|
101
|
+
ignore_globs: [],
|
|
102
|
+
hidden_components: [],
|
|
103
|
+
hidden_paths: [],
|
|
104
|
+
css_entrypoints: cssEntrypoints,
|
|
105
|
+
...(groupPrefix
|
|
106
|
+
? {
|
|
107
|
+
playground_groups: {
|
|
108
|
+
components: [groupPrefix],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} targetDir
|
|
117
|
+
* @returns {{ exists: boolean; path: string | null }}
|
|
118
|
+
*/
|
|
119
|
+
export function assessDslintConfig(targetDir) {
|
|
120
|
+
const existing = findDslintConfigPath(resolve(targetDir));
|
|
121
|
+
return { exists: Boolean(existing), path: existing };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {{
|
|
126
|
+
* targetDir: string;
|
|
127
|
+
* layout: "laravel" | "default";
|
|
128
|
+
* includeDir?: string;
|
|
129
|
+
* }} opts
|
|
130
|
+
* @returns {{ created: boolean; path: string; existed: boolean }}
|
|
131
|
+
*/
|
|
132
|
+
export function writeDslintConfig(opts) {
|
|
133
|
+
const targetDir = resolve(opts.targetDir);
|
|
134
|
+
const existing = findDslintConfigPath(targetDir);
|
|
135
|
+
if (existing) {
|
|
136
|
+
return { created: false, path: existing, existed: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const configPath = join(targetDir, DEFAULT_CONFIG_FILE_NAME);
|
|
140
|
+
mkdirSync(targetDir, { recursive: true });
|
|
141
|
+
const payload = buildStarterConfig(targetDir, opts.layout);
|
|
142
|
+
if (opts.includeDir) {
|
|
143
|
+
payload.include_dirs = [opts.includeDir];
|
|
144
|
+
const groupPrefix = pickPlaygroundGroupPrefix(
|
|
145
|
+
targetDir,
|
|
146
|
+
opts.layout,
|
|
147
|
+
payload.include_dirs,
|
|
148
|
+
);
|
|
149
|
+
if (groupPrefix) {
|
|
150
|
+
payload.playground_groups = { components: [groupPrefix] };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
writeFileSync(configPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
154
|
+
return { created: true, path: configPath, existed: false };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {{ targetDir: string; layout: "laravel" | "default" }} opts
|
|
159
|
+
* @returns {{ created: boolean; path: string; existed: boolean }}
|
|
160
|
+
*/
|
|
161
|
+
export function ensureDslintConfig(opts) {
|
|
162
|
+
return writeDslintConfig(opts);
|
|
163
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ensureDslintConfig } from "./scaffold-config.mjs";
|
|
6
|
+
|
|
7
|
+
describe("ensureDslintConfig", () => {
|
|
8
|
+
it("writes starter config with detected include/css paths", () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scaffold-"));
|
|
10
|
+
mkdirSync(join(root, "src", "components"), { recursive: true });
|
|
11
|
+
writeFileSync(join(root, "src", "index.css"), "@theme { --color-primary: #000; }");
|
|
12
|
+
|
|
13
|
+
const result = ensureDslintConfig({ targetDir: root, layout: "default" });
|
|
14
|
+
expect(result.created).toBe(true);
|
|
15
|
+
|
|
16
|
+
const raw = readFileSync(result.path, "utf8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
expect(parsed.include_dirs).toEqual(["src/components"]);
|
|
19
|
+
expect(parsed.css_entrypoints).toContain("src/index.css");
|
|
20
|
+
expect(parsed.ignore_globs).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("prefers narrowest laravel components dir", () => {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scaffold-laravel-"));
|
|
25
|
+
mkdirSync(join(root, "resources", "js", "components"), { recursive: true });
|
|
26
|
+
mkdirSync(join(root, "resources", "js", "layouts"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
const result = ensureDslintConfig({ targetDir: root, layout: "laravel" });
|
|
29
|
+
const parsed = JSON.parse(readFileSync(result.path, "utf8"));
|
|
30
|
+
expect(parsed.include_dirs).toEqual(["resources/js/components"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("does not overwrite an existing config", () => {
|
|
34
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-scaffold-existing-"));
|
|
35
|
+
const existing = join(root, ".dslinter.json");
|
|
36
|
+
writeFileSync(existing, "{\n \"ignore_globs\": [\"custom/**\"]\n}\n");
|
|
37
|
+
|
|
38
|
+
const result = ensureDslintConfig({ targetDir: root, layout: "default" });
|
|
39
|
+
expect(result.created).toBe(false);
|
|
40
|
+
expect(result.path).toBe(existing);
|
|
41
|
+
expect(readFileSync(existing, "utf8")).toContain("custom/**");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { detectInitLayout } from "./scaffold-config.mjs";
|
|
4
|
+
import { envIs } from "./env.mjs";
|
|
5
|
+
|
|
6
|
+
const HOST_APP_CANDIDATES = [
|
|
7
|
+
"src/App.tsx",
|
|
8
|
+
"src/App.jsx",
|
|
9
|
+
"src/app.tsx",
|
|
10
|
+
"resources/js/app.tsx",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when the scan root already embeds DashboardLayout (e.g. demo app).
|
|
15
|
+
* @param {string} scanRoot
|
|
16
|
+
*/
|
|
17
|
+
export function scanProjectHostsDashboard(scanRoot) {
|
|
18
|
+
const root = resolve(scanRoot);
|
|
19
|
+
for (const rel of HOST_APP_CANDIDATES) {
|
|
20
|
+
const p = join(root, rel);
|
|
21
|
+
if (!existsSync(p)) continue;
|
|
22
|
+
try {
|
|
23
|
+
const text = readFileSync(p, "utf8");
|
|
24
|
+
if (/DashboardLayout/.test(text) && /dslinter/.test(text)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prefer consumer Vite dev (host app) vs embed dashboard SPA.
|
|
36
|
+
* @param {string} scanRoot
|
|
37
|
+
*/
|
|
38
|
+
export function shouldUseConsumerViteDev(scanRoot) {
|
|
39
|
+
if (envIs("USE_CONSUMER_VITE")) return true;
|
|
40
|
+
if (envIs("NO_CONSUMER_VITE")) return false;
|
|
41
|
+
if (scanProjectHostsDashboard(scanRoot)) return true;
|
|
42
|
+
if (detectInitLayout(scanRoot) === "laravel") return false;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
scanProjectHostsDashboard,
|
|
7
|
+
shouldUseConsumerViteDev,
|
|
8
|
+
} from "./scan-host.mjs";
|
|
9
|
+
|
|
10
|
+
describe("scanProjectHostsDashboard", () => {
|
|
11
|
+
it("detects DashboardLayout in src/App.tsx", () => {
|
|
12
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-host-"));
|
|
13
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
14
|
+
writeFileSync(
|
|
15
|
+
join(root, "src", "App.tsx"),
|
|
16
|
+
`import { DashboardLayout } from "dslinter";\nexport default function App() { return <DashboardLayout />; }\n`,
|
|
17
|
+
);
|
|
18
|
+
expect(scanProjectHostsDashboard(root)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns false for laravel app entry", () => {
|
|
22
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-laravel-app-"));
|
|
23
|
+
mkdirSync(join(root, "resources", "js"), { recursive: true });
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(root, "resources", "js", "app.tsx"),
|
|
26
|
+
`import { createInertiaApp } from "@inertiajs/react";\n`,
|
|
27
|
+
);
|
|
28
|
+
expect(scanProjectHostsDashboard(root)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("shouldUseConsumerViteDev", () => {
|
|
33
|
+
it("prefers embed for laravel layout", () => {
|
|
34
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-laravel-vite-"));
|
|
35
|
+
mkdirSync(join(root, "resources", "js"), { recursive: true });
|
|
36
|
+
const prev = process.env.DSLINTER_USE_CONSUMER_VITE;
|
|
37
|
+
delete process.env.DSLINTER_USE_CONSUMER_VITE;
|
|
38
|
+
expect(shouldUseConsumerViteDev(root)).toBe(false);
|
|
39
|
+
process.env.DSLINTER_USE_CONSUMER_VITE = prev;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
detectDefaultIncludeDir,
|
|
5
|
+
detectInitLayout,
|
|
6
|
+
findDslintConfigPath,
|
|
7
|
+
writeDslintConfig,
|
|
8
|
+
} from "./scaffold-config.mjs";
|
|
9
|
+
import { resolveProjectRoot } from "./resolve-project.mjs";
|
|
10
|
+
import { readEnv } from "./env.mjs";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
13
|
+
import { confirmYesNo, isInteractiveTTY } from "./prompt.mjs";
|
|
14
|
+
import { defaultReportPath } from "./project-root.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {"missing_config" | "missing_public"} SetupIssueKind
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{ kind: SetupIssueKind; label: string }} SetupIssue
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} targetDir project / vite root
|
|
26
|
+
* @param {string} reportPath absolute report file path
|
|
27
|
+
* @returns {SetupIssue[]}
|
|
28
|
+
*/
|
|
29
|
+
export function assessSetupReadiness(targetDir, reportPath) {
|
|
30
|
+
const root = resolve(targetDir);
|
|
31
|
+
const issues = [];
|
|
32
|
+
|
|
33
|
+
if (!findDslintConfigPath(root)) {
|
|
34
|
+
issues.push({ kind: "missing_config", label: ".dslinter.json" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const publicDir = dirname(resolve(reportPath));
|
|
38
|
+
if (!existsSync(publicDir)) {
|
|
39
|
+
issues.push({ kind: "missing_public", label: "public/" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} reportPath
|
|
47
|
+
*/
|
|
48
|
+
export function ensurePublicDir(reportPath) {
|
|
49
|
+
const publicDir = dirname(resolve(reportPath));
|
|
50
|
+
if (!existsSync(publicDir)) {
|
|
51
|
+
mkdirSync(publicDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {{
|
|
57
|
+
* targetDir: string;
|
|
58
|
+
* reportPath: string;
|
|
59
|
+
* yes?: boolean;
|
|
60
|
+
* interactive?: boolean;
|
|
61
|
+
* }} opts
|
|
62
|
+
* @returns {Promise<{ applied: string[]; skipped: boolean }>}
|
|
63
|
+
*/
|
|
64
|
+
export async function ensureMinimalSetup(opts) {
|
|
65
|
+
const targetDir = resolve(opts.targetDir);
|
|
66
|
+
const reportPath = resolve(opts.reportPath);
|
|
67
|
+
const noScaffold = readEnv("NO_SCAFFOLD") === "1";
|
|
68
|
+
|
|
69
|
+
const issues = assessSetupReadiness(targetDir, reportPath);
|
|
70
|
+
if (!issues.length || noScaffold) {
|
|
71
|
+
if (issues.length && noScaffold) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`dslinter: setup incomplete (${issues.map((i) => i.label).join(", ")}). Set DSLINTER_NO_SCAFFOLD=0 or run with --yes to create.\n`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return { applied: [], skipped: noScaffold && issues.length > 0 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const ci = process.env.CI === "true" || process.env.CI === "1";
|
|
80
|
+
const autoYes = opts.yes === true || ci;
|
|
81
|
+
const interactive = opts.interactive ?? isInteractiveTTY();
|
|
82
|
+
|
|
83
|
+
let shouldApply = autoYes;
|
|
84
|
+
if (!shouldApply && interactive) {
|
|
85
|
+
process.stderr.write("dslinter: setup incomplete for live previews.\n");
|
|
86
|
+
for (const issue of issues) {
|
|
87
|
+
process.stderr.write(` Missing: ${issue.label}\n`);
|
|
88
|
+
}
|
|
89
|
+
shouldApply = await confirmYesNo("Create these files now?");
|
|
90
|
+
} else if (!shouldApply && !interactive) {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
`dslinter: setup incomplete (${issues.map((i) => i.label).join(", ")}). Run with --yes or use an interactive terminal.\n`,
|
|
93
|
+
);
|
|
94
|
+
return { applied: [], skipped: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!shouldApply) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
"dslinter: continuing without scaffold — previews and governance may be limited.\n",
|
|
100
|
+
);
|
|
101
|
+
return { applied: [], skipped: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @type {string[]} */
|
|
105
|
+
const applied = [];
|
|
106
|
+
|
|
107
|
+
if (issues.some((i) => i.kind === "missing_config")) {
|
|
108
|
+
const layout = detectInitLayout(targetDir);
|
|
109
|
+
let includeDir = detectDefaultIncludeDir(targetDir, layout);
|
|
110
|
+
if (interactive && includeDir) {
|
|
111
|
+
const rl = createInterface({ input, output });
|
|
112
|
+
try {
|
|
113
|
+
const answer = (
|
|
114
|
+
await rl.question(`Components directory [${includeDir}]: `)
|
|
115
|
+
).trim();
|
|
116
|
+
if (answer) includeDir = answer;
|
|
117
|
+
} finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const result = writeDslintConfig({
|
|
122
|
+
targetDir,
|
|
123
|
+
layout,
|
|
124
|
+
...(includeDir ? { includeDir } : {}),
|
|
125
|
+
});
|
|
126
|
+
applied.push(result.path);
|
|
127
|
+
process.stderr.write(`dslinter: created ${result.path}\n`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (issues.some((i) => i.kind === "missing_public")) {
|
|
131
|
+
ensurePublicDir(reportPath);
|
|
132
|
+
applied.push(dirname(reportPath));
|
|
133
|
+
process.stderr.write(`dslinter: created ${dirname(reportPath)}/\n`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { applied, skipped: false };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Convenience: assess using default report path for a scan root.
|
|
141
|
+
* @param {string} scanPath
|
|
142
|
+
* @param {{ yes?: boolean }} [opts]
|
|
143
|
+
*/
|
|
144
|
+
export async function ensureMinimalSetupForScan(scanPath, opts = {}) {
|
|
145
|
+
const scanAbs = resolve(scanPath);
|
|
146
|
+
const reportPath = defaultReportPath(scanAbs, null);
|
|
147
|
+
const targetDir = resolveProjectRoot(scanAbs);
|
|
148
|
+
return ensureMinimalSetup({
|
|
149
|
+
targetDir,
|
|
150
|
+
reportPath,
|
|
151
|
+
yes: opts.yes,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, 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 { assessSetupReadiness, ensurePublicDir } from "./setup-readiness.mjs";
|
|
6
|
+
|
|
7
|
+
describe("assessSetupReadiness", () => {
|
|
8
|
+
it("reports missing config and public", () => {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ready-"));
|
|
10
|
+
const reportPath = join(root, "public", "dslinter-report.json");
|
|
11
|
+
const issues = assessSetupReadiness(root, reportPath);
|
|
12
|
+
expect(issues.map((i) => i.kind)).toContain("missing_config");
|
|
13
|
+
expect(issues.map((i) => i.kind)).toContain("missing_public");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("passes when config and public exist", () => {
|
|
17
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-ready-ok-"));
|
|
18
|
+
mkdirSync(join(root, "public"), { recursive: true });
|
|
19
|
+
const reportPath = join(root, "public", "dslinter-report.json");
|
|
20
|
+
writeFileSync(join(root, ".dslinter.json"), "{}\n");
|
|
21
|
+
expect(assessSetupReadiness(root, reportPath)).toHaveLength(0);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("ensurePublicDir", () => {
|
|
26
|
+
it("creates public directory", () => {
|
|
27
|
+
const root = mkdtempSync(join(tmpdir(), "dslinter-public-"));
|
|
28
|
+
const reportPath = join(root, "public", "dslinter-report.json");
|
|
29
|
+
ensurePublicDir(reportPath);
|
|
30
|
+
expect(existsSync(join(root, "public"))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/bin/modes/build.mjs
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
defaultReportPath,
|
|
5
|
+
findViteRoot,
|
|
6
|
+
resolveViteBin,
|
|
7
|
+
} from "../lib/project-root.mjs";
|
|
8
|
+
import { enrichPlaygroundsFromTs } from "../lib/enrich-playgrounds-from-ts.mjs";
|
|
9
|
+
import { ensureMinimalSetup } from "../lib/setup-readiness.mjs";
|
|
4
10
|
import { runScannerSync } from "../lib/run-scanner.mjs";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* @param {{
|
|
8
14
|
* scanPath: string;
|
|
15
|
+
* projectRoot: string;
|
|
9
16
|
* outputPath: string | null;
|
|
10
17
|
* scannerArgs: string[];
|
|
18
|
+
* yes?: boolean;
|
|
11
19
|
* }}
|
|
12
20
|
*/
|
|
13
|
-
export function runBuildMode({
|
|
14
|
-
|
|
21
|
+
export async function runBuildMode({
|
|
22
|
+
scanPath,
|
|
23
|
+
projectRoot,
|
|
24
|
+
outputPath,
|
|
25
|
+
scannerArgs,
|
|
26
|
+
yes = false,
|
|
27
|
+
}) {
|
|
28
|
+
const scanAbs = resolve(scanPath);
|
|
29
|
+
const projectAbs = resolve(projectRoot);
|
|
30
|
+
const reportPath = defaultReportPath(scanAbs, outputPath);
|
|
31
|
+
await ensureMinimalSetup({
|
|
32
|
+
targetDir: projectAbs,
|
|
33
|
+
reportPath,
|
|
34
|
+
yes,
|
|
35
|
+
});
|
|
15
36
|
const args = ["--report", ...scannerArgs];
|
|
16
37
|
if (!args.some((a) => a === "--output" || a.startsWith("--output="))) {
|
|
17
38
|
args.push("--output", reportPath);
|
|
18
39
|
}
|
|
19
40
|
|
|
20
|
-
const code = runScannerSync(args);
|
|
41
|
+
const code = runScannerSync(args, { projectRoot: projectAbs });
|
|
21
42
|
if (code !== 0) process.exit(code);
|
|
22
43
|
|
|
44
|
+
await enrichPlaygroundsFromTs({
|
|
45
|
+
projectRoot: projectAbs,
|
|
46
|
+
reportPath,
|
|
47
|
+
});
|
|
48
|
+
|
|
23
49
|
const viteRoot = findViteRoot(process.cwd());
|
|
24
50
|
if (!viteRoot) {
|
|
25
51
|
process.stderr.write(
|
|
@@ -33,13 +59,12 @@ export function runBuildMode({ scanPath, outputPath, scannerArgs }) {
|
|
|
33
59
|
process.stderr.write(`dslinter: vite not installed in ${viteRoot}. Run npm install.\n`);
|
|
34
60
|
process.exit(1);
|
|
35
61
|
}
|
|
36
|
-
const scanAbs = resolve(scanPath);
|
|
37
62
|
const child = spawnSync(process.execPath, [viteBin, "build"], {
|
|
38
63
|
cwd: viteRoot,
|
|
39
64
|
stdio: "inherit",
|
|
40
65
|
env: {
|
|
41
66
|
...process.env,
|
|
42
|
-
|
|
67
|
+
DSLINTER_SCAN_ROOT: scanAbs,
|
|
43
68
|
},
|
|
44
69
|
});
|
|
45
70
|
process.exit(child.status === null ? 1 : child.status);
|