design-system-enforcer 1.0.0 → 1.0.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/dist/cli.cjs +383 -73
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +383 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +347 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +347 -56
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -60,6 +60,7 @@ interface RulesConfig {
|
|
|
60
60
|
"table-windowed-pagination"?: RuleSeverity;
|
|
61
61
|
"tabs-component-required"?: RuleSeverity;
|
|
62
62
|
"component-inventory-active"?: RuleSeverity;
|
|
63
|
+
"no-wrapping-compact-elements"?: RuleSeverity;
|
|
63
64
|
}
|
|
64
65
|
interface DseConfig {
|
|
65
66
|
preset?: "berserk" | "recommended" | "minimal" | "python";
|
package/dist/index.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ interface RulesConfig {
|
|
|
60
60
|
"table-windowed-pagination"?: RuleSeverity;
|
|
61
61
|
"tabs-component-required"?: RuleSeverity;
|
|
62
62
|
"component-inventory-active"?: RuleSeverity;
|
|
63
|
+
"no-wrapping-compact-elements"?: RuleSeverity;
|
|
63
64
|
}
|
|
64
65
|
interface DseConfig {
|
|
65
66
|
preset?: "berserk" | "recommended" | "minimal" | "python";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config/loader.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs3 from "fs";
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
import { pathToFileURL } from "url";
|
|
7
7
|
|
|
8
8
|
// src/config/presets/berserk.ts
|
|
@@ -91,6 +91,9 @@ var berserkPreset = {
|
|
|
91
91
|
disallowRawHtml: {
|
|
92
92
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
93
93
|
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
94
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
95
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
96
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true },
|
|
94
97
|
table: { replaceWith: "Table", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
95
98
|
thead: { replaceWith: "TableHeader", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
96
99
|
tbody: { replaceWith: "TableBody", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
@@ -142,7 +145,8 @@ var berserkPreset = {
|
|
|
142
145
|
"table-no-horizontal-stacking": "error",
|
|
143
146
|
"table-windowed-pagination": "error",
|
|
144
147
|
"tabs-component-required": "error",
|
|
145
|
-
"component-inventory-active": "error"
|
|
148
|
+
"component-inventory-active": "error",
|
|
149
|
+
"no-wrapping-compact-elements": "error"
|
|
146
150
|
}
|
|
147
151
|
};
|
|
148
152
|
|
|
@@ -179,7 +183,10 @@ var defaultPreset = {
|
|
|
179
183
|
components: {
|
|
180
184
|
disallowRawHtml: {
|
|
181
185
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
182
|
-
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true }
|
|
186
|
+
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
187
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
188
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
189
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true }
|
|
183
190
|
},
|
|
184
191
|
disallowPseudoChips: true
|
|
185
192
|
},
|
|
@@ -190,7 +197,8 @@ var defaultPreset = {
|
|
|
190
197
|
"no-pseudo-chips": "warn",
|
|
191
198
|
"no-chip-colored-buttons": "error",
|
|
192
199
|
"no-text-arrows": "error",
|
|
193
|
-
"no-backdrop-blur": "warn"
|
|
200
|
+
"no-backdrop-blur": "warn",
|
|
201
|
+
"no-wrapping-compact-elements": "warn"
|
|
194
202
|
}
|
|
195
203
|
};
|
|
196
204
|
|
|
@@ -254,6 +262,221 @@ var pythonPreset = {
|
|
|
254
262
|
}
|
|
255
263
|
};
|
|
256
264
|
|
|
265
|
+
// src/detector/index.ts
|
|
266
|
+
import fs2 from "fs";
|
|
267
|
+
import path2 from "path";
|
|
268
|
+
|
|
269
|
+
// src/detector/ignores.ts
|
|
270
|
+
import fs from "fs";
|
|
271
|
+
import path from "path";
|
|
272
|
+
var DEFAULT_IGNORES = [
|
|
273
|
+
"**/node_modules/**",
|
|
274
|
+
"**/.next/**",
|
|
275
|
+
"**/dist/**",
|
|
276
|
+
"**/build/**",
|
|
277
|
+
"**/out/**",
|
|
278
|
+
"**/.turbo/**",
|
|
279
|
+
"**/.git/**",
|
|
280
|
+
"**/coverage/**",
|
|
281
|
+
"**/.venv/**",
|
|
282
|
+
"**/venv/**",
|
|
283
|
+
"**/env/**",
|
|
284
|
+
"**/__pycache__/**",
|
|
285
|
+
"**/.pytest_cache/**",
|
|
286
|
+
"**/*.pyc",
|
|
287
|
+
// Non-UI code
|
|
288
|
+
"**/api/**",
|
|
289
|
+
"**/*.test.*",
|
|
290
|
+
"**/*.spec.*",
|
|
291
|
+
"**/__tests__/**",
|
|
292
|
+
"**/*.stories.*",
|
|
293
|
+
"**/generated/**"
|
|
294
|
+
];
|
|
295
|
+
function getSmartIgnores(cwd) {
|
|
296
|
+
const result = new Set(DEFAULT_IGNORES);
|
|
297
|
+
const gitignorePath = path.resolve(cwd, ".gitignore");
|
|
298
|
+
if (fs.existsSync(gitignorePath)) {
|
|
299
|
+
try {
|
|
300
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
301
|
+
const lines = content.split(/\r?\n/);
|
|
302
|
+
for (let line of lines) {
|
|
303
|
+
line = line.trim();
|
|
304
|
+
if (!line || line.startsWith("#")) continue;
|
|
305
|
+
if (line.startsWith("/")) {
|
|
306
|
+
line = line.substring(1);
|
|
307
|
+
}
|
|
308
|
+
if (line.endsWith("/")) {
|
|
309
|
+
line = line.slice(0, -1);
|
|
310
|
+
}
|
|
311
|
+
if (line) {
|
|
312
|
+
result.add(`**/${line}/**`);
|
|
313
|
+
result.add(`**/${line}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return Array.from(result);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/detector/index.ts
|
|
323
|
+
function safeReadJson(filePath) {
|
|
324
|
+
try {
|
|
325
|
+
if (fs2.existsSync(filePath)) {
|
|
326
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
327
|
+
return JSON.parse(raw);
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function detectProject(cwd = process.cwd()) {
|
|
334
|
+
const pkg = safeReadJson(path2.resolve(cwd, "package.json"));
|
|
335
|
+
const deps = { ...pkg?.dependencies || {}, ...pkg?.devDependencies || {} };
|
|
336
|
+
const hasRequirements = fs2.existsSync(path2.resolve(cwd, "requirements.txt"));
|
|
337
|
+
const hasPyproject = fs2.existsSync(path2.resolve(cwd, "pyproject.toml"));
|
|
338
|
+
const hasPython = hasRequirements || hasPyproject || fs2.existsSync(path2.resolve(cwd, "setup.py"));
|
|
339
|
+
let isStreamlit = false;
|
|
340
|
+
if (hasPython) {
|
|
341
|
+
if (hasRequirements) {
|
|
342
|
+
try {
|
|
343
|
+
const reqContent = fs2.readFileSync(path2.resolve(cwd, "requirements.txt"), "utf-8");
|
|
344
|
+
if (reqContent.toLowerCase().includes("streamlit")) {
|
|
345
|
+
isStreamlit = true;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const isPnpmMonorepo = fs2.existsSync(path2.resolve(cwd, "pnpm-workspace.yaml"));
|
|
352
|
+
const isTurbo = fs2.existsSync(path2.resolve(cwd, "turbo.json"));
|
|
353
|
+
let monorepoWorkspaces = [];
|
|
354
|
+
if (Array.isArray(pkg?.workspaces)) {
|
|
355
|
+
monorepoWorkspaces = pkg.workspaces;
|
|
356
|
+
} else if (pkg?.workspaces?.packages) {
|
|
357
|
+
monorepoWorkspaces = pkg.workspaces.packages;
|
|
358
|
+
}
|
|
359
|
+
const isMonorepo = isPnpmMonorepo || isTurbo || monorepoWorkspaces.length > 0;
|
|
360
|
+
let framework = "unknown";
|
|
361
|
+
if (deps["next"]) {
|
|
362
|
+
framework = "nextjs";
|
|
363
|
+
} else if (deps["vite"]) {
|
|
364
|
+
framework = "vite";
|
|
365
|
+
} else if (deps["@remix-run/react"]) {
|
|
366
|
+
framework = "remix";
|
|
367
|
+
} else if (deps["astro"]) {
|
|
368
|
+
framework = "astro";
|
|
369
|
+
} else if (deps["react-scripts"]) {
|
|
370
|
+
framework = "react-cra";
|
|
371
|
+
} else if (isStreamlit) {
|
|
372
|
+
framework = "streamlit";
|
|
373
|
+
} else if (hasPython) {
|
|
374
|
+
framework = "python-general";
|
|
375
|
+
}
|
|
376
|
+
const libraries = [];
|
|
377
|
+
const hasShadcnConfig = fs2.existsSync(path2.resolve(cwd, "components.json"));
|
|
378
|
+
if (hasShadcnConfig || deps["@radix-ui/react-slot"]) {
|
|
379
|
+
libraries.push("shadcn");
|
|
380
|
+
}
|
|
381
|
+
if (Object.keys(deps).some((d) => d.startsWith("@radix-ui/"))) {
|
|
382
|
+
if (!libraries.includes("shadcn")) libraries.push("radix");
|
|
383
|
+
}
|
|
384
|
+
if (deps["tailwindcss"]) {
|
|
385
|
+
libraries.push("tailwind");
|
|
386
|
+
}
|
|
387
|
+
if (deps["lucide-react"] || deps["lucide"]) {
|
|
388
|
+
libraries.push("lucide");
|
|
389
|
+
}
|
|
390
|
+
if (deps["@mui/material"] || deps["@material-ui/core"]) {
|
|
391
|
+
libraries.push("mui");
|
|
392
|
+
}
|
|
393
|
+
if (deps["@chakra-ui/react"]) {
|
|
394
|
+
libraries.push("chakra");
|
|
395
|
+
}
|
|
396
|
+
const hasTailwind = Boolean(deps["tailwindcss"] || fs2.existsSync(path2.resolve(cwd, "tailwind.config.js")) || fs2.existsSync(path2.resolve(cwd, "tailwind.config.ts")));
|
|
397
|
+
let tailwindVersion;
|
|
398
|
+
if (hasTailwind) {
|
|
399
|
+
const rawVersion = deps["tailwindcss"] || "";
|
|
400
|
+
if (rawVersion.includes("^4") || rawVersion.includes("~4") || rawVersion.startsWith("4")) {
|
|
401
|
+
tailwindVersion = "v4";
|
|
402
|
+
} else {
|
|
403
|
+
tailwindVersion = "v3";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const hasTypeScript = Boolean(deps["typescript"] || fs2.existsSync(path2.resolve(cwd, "tsconfig.json")));
|
|
407
|
+
const suggestedIncludes = [];
|
|
408
|
+
const extGroup = hasTypeScript ? "{tsx,jsx,ts,js}" : "{jsx,js}";
|
|
409
|
+
const candidateDirs = [
|
|
410
|
+
"frontend/src",
|
|
411
|
+
"frontend",
|
|
412
|
+
"apps/web/src",
|
|
413
|
+
"apps/web",
|
|
414
|
+
"client/src",
|
|
415
|
+
"client",
|
|
416
|
+
"src",
|
|
417
|
+
"app",
|
|
418
|
+
"pages",
|
|
419
|
+
"components"
|
|
420
|
+
];
|
|
421
|
+
let foundUiDir = false;
|
|
422
|
+
for (const dir of candidateDirs) {
|
|
423
|
+
if (fs2.existsSync(path2.resolve(cwd, dir))) {
|
|
424
|
+
if (dir === "frontend" && fs2.existsSync(path2.resolve(cwd, "frontend/src"))) continue;
|
|
425
|
+
if (dir === "client" && fs2.existsSync(path2.resolve(cwd, "client/src"))) continue;
|
|
426
|
+
if (dir === "apps/web" && fs2.existsSync(path2.resolve(cwd, "apps/web/src"))) continue;
|
|
427
|
+
suggestedIncludes.push(`${dir}/**/*.${extGroup}`);
|
|
428
|
+
foundUiDir = true;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (hasPython) {
|
|
433
|
+
suggestedIncludes.push("**/*.py");
|
|
434
|
+
} else if (!foundUiDir) {
|
|
435
|
+
suggestedIncludes.push(`**/*.${extGroup}`);
|
|
436
|
+
}
|
|
437
|
+
const suggestedIgnores = getSmartIgnores(cwd);
|
|
438
|
+
if (hasShadcnConfig || libraries.includes("shadcn")) {
|
|
439
|
+
suggestedIgnores.push("**/components/ui/**");
|
|
440
|
+
}
|
|
441
|
+
const tokenCandidates = [
|
|
442
|
+
"frontend/src/lib/tokens.ts",
|
|
443
|
+
"frontend/src/tokens.ts",
|
|
444
|
+
"src/lib/tokens.ts",
|
|
445
|
+
"src/tokens.ts",
|
|
446
|
+
"src/design/tokens.ts",
|
|
447
|
+
"styles/tokens.css",
|
|
448
|
+
"src/app/globals.css",
|
|
449
|
+
"frontend/src/index.css",
|
|
450
|
+
"src/index.css"
|
|
451
|
+
];
|
|
452
|
+
let tokensFile;
|
|
453
|
+
for (const candidate of tokenCandidates) {
|
|
454
|
+
if (fs2.existsSync(path2.resolve(cwd, candidate))) {
|
|
455
|
+
tokensFile = candidate;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
let preset = "berserk";
|
|
460
|
+
if (framework === "streamlit" || framework === "python-general") {
|
|
461
|
+
preset = "python";
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
root: cwd,
|
|
465
|
+
isMonorepo,
|
|
466
|
+
monorepoWorkspaces,
|
|
467
|
+
framework,
|
|
468
|
+
libraries,
|
|
469
|
+
hasTailwind,
|
|
470
|
+
tailwindVersion,
|
|
471
|
+
hasTypeScript,
|
|
472
|
+
hasPython,
|
|
473
|
+
suggestedIncludes,
|
|
474
|
+
suggestedIgnores,
|
|
475
|
+
tokensFile,
|
|
476
|
+
preset
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
257
480
|
// src/config/loader.ts
|
|
258
481
|
var CONFIG_FILES = [
|
|
259
482
|
"dse.config.js",
|
|
@@ -268,17 +491,18 @@ function defineConfig(config) {
|
|
|
268
491
|
return config;
|
|
269
492
|
}
|
|
270
493
|
async function loadConfig(cwd = process.cwd(), customPath) {
|
|
494
|
+
const detected = detectProject(cwd);
|
|
271
495
|
let userConfig = {};
|
|
272
496
|
let foundConfigPath = null;
|
|
273
497
|
if (customPath) {
|
|
274
|
-
const resolved =
|
|
275
|
-
if (
|
|
498
|
+
const resolved = path3.resolve(cwd, customPath);
|
|
499
|
+
if (fs3.existsSync(resolved)) {
|
|
276
500
|
foundConfigPath = resolved;
|
|
277
501
|
}
|
|
278
502
|
} else {
|
|
279
503
|
for (const filename of CONFIG_FILES) {
|
|
280
|
-
const candidate =
|
|
281
|
-
if (
|
|
504
|
+
const candidate = path3.resolve(cwd, filename);
|
|
505
|
+
if (fs3.existsSync(candidate)) {
|
|
282
506
|
foundConfigPath = candidate;
|
|
283
507
|
break;
|
|
284
508
|
}
|
|
@@ -286,7 +510,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
286
510
|
}
|
|
287
511
|
if (foundConfigPath) {
|
|
288
512
|
if (foundConfigPath.endsWith(".json") || foundConfigPath.endsWith(".dserc")) {
|
|
289
|
-
const content =
|
|
513
|
+
const content = fs3.readFileSync(foundConfigPath, "utf-8");
|
|
290
514
|
try {
|
|
291
515
|
userConfig = JSON.parse(content);
|
|
292
516
|
} catch (err) {
|
|
@@ -299,7 +523,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
299
523
|
userConfig = mod.default || mod;
|
|
300
524
|
} catch {
|
|
301
525
|
try {
|
|
302
|
-
const content =
|
|
526
|
+
const content = fs3.readFileSync(foundConfigPath, "utf-8");
|
|
303
527
|
const cjsReq = createCjsEvaluator(foundConfigPath, content);
|
|
304
528
|
userConfig = cjsReq;
|
|
305
529
|
} catch (innerErr) {
|
|
@@ -308,18 +532,19 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
308
532
|
}
|
|
309
533
|
}
|
|
310
534
|
}
|
|
311
|
-
let presetName = userConfig.preset;
|
|
312
|
-
if (!presetName) {
|
|
313
|
-
const isPython = fs.existsSync(path.resolve(cwd, "requirements.txt")) || fs.existsSync(path.resolve(cwd, "pyproject.toml")) || fs.existsSync(path.resolve(cwd, ".streamlit")) || fs.existsSync(path.resolve(cwd, "manage.py"));
|
|
314
|
-
presetName = isPython ? "python" : "berserk";
|
|
315
|
-
}
|
|
535
|
+
let presetName = userConfig.preset || detected.preset;
|
|
316
536
|
let basePreset = berserkPreset;
|
|
317
537
|
if (presetName === "python") {
|
|
318
538
|
basePreset = pythonPreset;
|
|
319
539
|
} else if (presetName === "recommended") {
|
|
320
540
|
basePreset = defaultPreset;
|
|
321
541
|
}
|
|
322
|
-
|
|
542
|
+
const autonomousDefaults = {
|
|
543
|
+
include: detected.suggestedIncludes,
|
|
544
|
+
exclude: detected.suggestedIgnores
|
|
545
|
+
};
|
|
546
|
+
const withAutonomous = mergeConfigs(basePreset, autonomousDefaults);
|
|
547
|
+
return mergeConfigs(withAutonomous, userConfig);
|
|
323
548
|
}
|
|
324
549
|
function createCjsEvaluator(filePath, content) {
|
|
325
550
|
if (content.includes("export default")) {
|
|
@@ -369,8 +594,8 @@ function mergeConfigs(base, overrides) {
|
|
|
369
594
|
}
|
|
370
595
|
|
|
371
596
|
// src/rules/runner.ts
|
|
372
|
-
import
|
|
373
|
-
import
|
|
597
|
+
import fs4 from "fs";
|
|
598
|
+
import path4 from "path";
|
|
374
599
|
import fg from "fast-glob";
|
|
375
600
|
|
|
376
601
|
// src/parser/ast.ts
|
|
@@ -783,16 +1008,52 @@ function checkRawHtmlElements(tagName, attributes, pos, filePath, addViolation,
|
|
|
783
1008
|
pos.line,
|
|
784
1009
|
pos.col,
|
|
785
1010
|
"no-raw-html-elements",
|
|
786
|
-
`Raw browser default '<input type="checkbox">' detected. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
1011
|
+
`Raw browser default '<input type="checkbox">' detected. Never use unstyled browser checkboxes. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
1012
|
+
lineSnippet
|
|
1013
|
+
);
|
|
1014
|
+
} else if (typeVal === "radio") {
|
|
1015
|
+
addViolation(
|
|
1016
|
+
filePath,
|
|
1017
|
+
pos.line,
|
|
1018
|
+
pos.col,
|
|
1019
|
+
"no-raw-html-elements",
|
|
1020
|
+
`Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
|
|
1021
|
+
lineSnippet
|
|
1022
|
+
);
|
|
1023
|
+
} else if (typeVal === "range") {
|
|
1024
|
+
addViolation(
|
|
1025
|
+
filePath,
|
|
1026
|
+
pos.line,
|
|
1027
|
+
pos.col,
|
|
1028
|
+
"no-raw-html-elements",
|
|
1029
|
+
`Raw browser default '<input type="range">' detected. Never use unstyled browser sliders. Use standard design system '<Slider>'.`,
|
|
1030
|
+
lineSnippet
|
|
1031
|
+
);
|
|
1032
|
+
} else if (typeVal === "file") {
|
|
1033
|
+
addViolation(
|
|
1034
|
+
filePath,
|
|
1035
|
+
pos.line,
|
|
1036
|
+
pos.col,
|
|
1037
|
+
"no-raw-html-elements",
|
|
1038
|
+
`Raw browser default '<input type="file">' detected. Never display native unstyled file picker inputs. Use a custom styled dropzone or trigger.`,
|
|
1039
|
+
lineSnippet
|
|
1040
|
+
);
|
|
1041
|
+
} else if (typeVal === "color") {
|
|
1042
|
+
addViolation(
|
|
1043
|
+
filePath,
|
|
1044
|
+
pos.line,
|
|
1045
|
+
pos.col,
|
|
1046
|
+
"no-raw-html-elements",
|
|
1047
|
+
`Raw browser default '<input type="color">' detected. Never use native browser color pickers. Use a custom color picker component.`,
|
|
787
1048
|
lineSnippet
|
|
788
1049
|
);
|
|
789
|
-
} else if (["text", "email", "search"].includes(typeVal)) {
|
|
1050
|
+
} else if (["text", "email", "search", "password", "number", "tel", "url"].includes(typeVal)) {
|
|
790
1051
|
addViolation(
|
|
791
1052
|
filePath,
|
|
792
1053
|
pos.line,
|
|
793
1054
|
pos.col,
|
|
794
1055
|
"no-raw-html-elements",
|
|
795
|
-
`Raw '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
|
|
1056
|
+
`Raw browser default '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
|
|
796
1057
|
lineSnippet
|
|
797
1058
|
);
|
|
798
1059
|
}
|
|
@@ -1050,6 +1311,9 @@ function checkPseudoChips(tagName, attributes, pos, filePath, addViolation, line
|
|
|
1050
1311
|
function checkElementClasses(node, tagName, attributes, pos, filePath, addViolation, sourceFile, config) {
|
|
1051
1312
|
if (!attributes.className) return;
|
|
1052
1313
|
const isButton = ["Button", "IconButton", "ButtonyIcon", "FloatingActionButton"].includes(tagName);
|
|
1314
|
+
const isChip = ["Chip", "Badge", "Tag", "StatusBadge"].includes(tagName);
|
|
1315
|
+
const isTab = ["Tab", "TabTrigger", "TabsTrigger"].includes(tagName);
|
|
1316
|
+
const isCompactElement = isButton || isChip || isTab;
|
|
1053
1317
|
const isCheckbox = tagName === "Checkbox" || attributes.type?.initializer?.getText(sourceFile)?.includes("checkbox") || attributes.role?.initializer?.getText(sourceFile)?.includes("checkbox") || Boolean(attributes["aria-checked"]) || attributes.className && /\b(?:size-5|w-5\s+h-5)\b/.test(attributes.className.rawText);
|
|
1054
1318
|
const lineSnippet = sourceFile.getFullText().split("\n")[pos.line - 1] || "";
|
|
1055
1319
|
const classStrings = extractClassNames(attributes.className.initializer, sourceFile);
|
|
@@ -1089,8 +1353,33 @@ function checkElementClasses(node, tagName, attributes, pos, filePath, addViolat
|
|
|
1089
1353
|
);
|
|
1090
1354
|
}
|
|
1091
1355
|
}
|
|
1356
|
+
if (isCompactElement && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1357
|
+
if (dec.core === "flex-wrap" || dec.core === "whitespace-normal" || dec.core === "whitespace-pre-wrap") {
|
|
1358
|
+
addViolation(
|
|
1359
|
+
filePath,
|
|
1360
|
+
nodePos.line,
|
|
1361
|
+
nodePos.col,
|
|
1362
|
+
"no-wrapping-compact-elements",
|
|
1363
|
+
`Disallowed text wrapping class '${dec.core}' on <${tagName}>. Compact interactive elements (buttons, chips, badges, tabs) must never wrap text across multiple lines.`,
|
|
1364
|
+
lineSnippet
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1092
1368
|
});
|
|
1093
1369
|
});
|
|
1370
|
+
if (tagName === "TableHead" && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1371
|
+
const rawClass = attributes.className?.rawText || "";
|
|
1372
|
+
if (/\b(?:flex-wrap|whitespace-normal|whitespace-pre-wrap)\b/.test(rawClass)) {
|
|
1373
|
+
addViolation(
|
|
1374
|
+
filePath,
|
|
1375
|
+
pos.line,
|
|
1376
|
+
pos.col,
|
|
1377
|
+
"no-wrapping-compact-elements",
|
|
1378
|
+
`Disallowed text wrapping on <TableHead>. Column headers must remain single-line ('whitespace-nowrap') to preserve table column alignment.`,
|
|
1379
|
+
lineSnippet
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1094
1383
|
}
|
|
1095
1384
|
|
|
1096
1385
|
// src/rules/regex-rules.ts
|
|
@@ -1311,8 +1600,8 @@ function runPythonRules(content, filePath, addViolation, config) {
|
|
|
1311
1600
|
|
|
1312
1601
|
// src/rules/runner.ts
|
|
1313
1602
|
async function runDseScan(config, cwd = process.cwd()) {
|
|
1314
|
-
let targetDir =
|
|
1315
|
-
if (!
|
|
1603
|
+
let targetDir = path4.resolve(cwd, config.targetDir || "src");
|
|
1604
|
+
if (!fs4.existsSync(targetDir)) {
|
|
1316
1605
|
targetDir = cwd;
|
|
1317
1606
|
}
|
|
1318
1607
|
const includePatterns = config.include || ["**/*.{tsx,jsx,ts,js}"];
|
|
@@ -1344,7 +1633,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1344
1633
|
const violations = [];
|
|
1345
1634
|
const seenViolationKeys = /* @__PURE__ */ new Set();
|
|
1346
1635
|
const addViolation = (file, line, col, rule, message, snippet) => {
|
|
1347
|
-
const normFile =
|
|
1636
|
+
const normFile = path4.relative(cwd, file).replace(/\\/g, "/");
|
|
1348
1637
|
const key = `${normFile}:${line}:${rule}`;
|
|
1349
1638
|
if (seenViolationKeys.has(key)) return;
|
|
1350
1639
|
seenViolationKeys.add(key);
|
|
@@ -1362,7 +1651,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1362
1651
|
};
|
|
1363
1652
|
let scannedJsxFiles = false;
|
|
1364
1653
|
for (const filePath of files) {
|
|
1365
|
-
const content =
|
|
1654
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1366
1655
|
const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || filePath.endsWith(".ts") || filePath.endsWith(".js");
|
|
1367
1656
|
const isPythonOrTemplate = filePath.endsWith(".py") || filePath.endsWith(".html") || filePath.endsWith(".jinja") || filePath.endsWith(".jinja2") || filePath.endsWith(".css");
|
|
1368
1657
|
if (isJsx) {
|
|
@@ -1483,8 +1772,8 @@ function formatJson(result) {
|
|
|
1483
1772
|
}
|
|
1484
1773
|
|
|
1485
1774
|
// src/scaffold/index.ts
|
|
1486
|
-
import
|
|
1487
|
-
import
|
|
1775
|
+
import fs5 from "fs";
|
|
1776
|
+
import path5 from "path";
|
|
1488
1777
|
|
|
1489
1778
|
// src/scaffold/templates/tokens.ts
|
|
1490
1779
|
var tokensTemplate = `/**
|
|
@@ -1627,7 +1916,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
1627
1916
|
({ className = "", variant = "primary", size = "default", children, ...props }, ref) => {
|
|
1628
1917
|
// Standard 36px height, 8px radius, font-medium, strictly neutral palette
|
|
1629
1918
|
const baseClasses =
|
|
1630
|
-
"inline-flex items-center justify-center font-medium text-sm rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none";
|
|
1919
|
+
"inline-flex items-center justify-center font-medium text-sm rounded-md whitespace-nowrap shrink-0 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 disabled:pointer-events-none disabled:opacity-50 select-none cursor-pointer";
|
|
1920
|
+
|
|
1631
1921
|
|
|
1632
1922
|
const variantClasses = {
|
|
1633
1923
|
primary: "bg-neutral-900 text-white hover:bg-neutral-800 shadow-2xs",
|
|
@@ -1668,7 +1958,8 @@ export const Chip: React.FC<ChipProps> = ({
|
|
|
1668
1958
|
}) => {
|
|
1669
1959
|
// Standard 36px height, 8px radius, 14px typography
|
|
1670
1960
|
const baseClasses =
|
|
1671
|
-
"inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium gap-1.5 select-none";
|
|
1961
|
+
"inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium whitespace-nowrap shrink-0 gap-1.5 select-none";
|
|
1962
|
+
|
|
1672
1963
|
|
|
1673
1964
|
const variantClasses = {
|
|
1674
1965
|
neutral: "bg-neutral-100 text-neutral-800 border border-neutral-200",
|
|
@@ -1838,12 +2129,12 @@ export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
|
|
1838
2129
|
({ className = "", icon, resizable = true, children, ...props }, ref) => (
|
|
1839
2130
|
<th
|
|
1840
2131
|
ref={ref}
|
|
1841
|
-
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative \${className}\`}
|
|
2132
|
+
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative whitespace-nowrap \${className}\`}
|
|
1842
2133
|
{...props}
|
|
1843
2134
|
>
|
|
1844
2135
|
<div className="flex items-center gap-2">
|
|
1845
2136
|
{icon && <span className="text-neutral-500 shrink-0">{icon}</span>}
|
|
1846
|
-
<span>{children}</span>
|
|
2137
|
+
<span className="whitespace-nowrap">{children}</span>
|
|
1847
2138
|
</div>
|
|
1848
2139
|
</th>
|
|
1849
2140
|
)
|
|
@@ -1889,7 +2180,7 @@ export const Tabs: React.FC<TabsProps> = ({ items, activeId, onChange, className
|
|
|
1889
2180
|
type="button"
|
|
1890
2181
|
disabled={tab.disabled}
|
|
1891
2182
|
onClick={() => onChange(tab.id)}
|
|
1892
|
-
className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-all select-none \${
|
|
2183
|
+
className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium whitespace-nowrap shrink-0 transition-all select-none \${
|
|
1893
2184
|
isActive
|
|
1894
2185
|
? "bg-white text-neutral-900 shadow-2xs"
|
|
1895
2186
|
: "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200/50"
|
|
@@ -1981,8 +2272,8 @@ var COMPONENT_TEMPLATES = {
|
|
|
1981
2272
|
};
|
|
1982
2273
|
function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
1983
2274
|
if (preset === "python") {
|
|
1984
|
-
const jsonPath =
|
|
1985
|
-
if (
|
|
2275
|
+
const jsonPath = path5.resolve(cwd, "dse.config.json");
|
|
2276
|
+
if (fs5.existsSync(jsonPath)) return jsonPath;
|
|
1986
2277
|
const content2 = JSON.stringify(
|
|
1987
2278
|
{
|
|
1988
2279
|
preset: "python",
|
|
@@ -1999,11 +2290,11 @@ function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
|
1999
2290
|
null,
|
|
2000
2291
|
2
|
|
2001
2292
|
) + "\n";
|
|
2002
|
-
|
|
2293
|
+
fs5.writeFileSync(jsonPath, content2, "utf-8");
|
|
2003
2294
|
return jsonPath;
|
|
2004
2295
|
}
|
|
2005
|
-
const configPath =
|
|
2006
|
-
if (
|
|
2296
|
+
const configPath = path5.resolve(cwd, "dse.config.js");
|
|
2297
|
+
if (fs5.existsSync(configPath)) {
|
|
2007
2298
|
return configPath;
|
|
2008
2299
|
}
|
|
2009
2300
|
const content = `import { defineConfig } from "design-system-enforcer";
|
|
@@ -2031,21 +2322,21 @@ export default defineConfig({
|
|
|
2031
2322
|
],
|
|
2032
2323
|
});
|
|
2033
2324
|
`;
|
|
2034
|
-
|
|
2325
|
+
fs5.writeFileSync(configPath, content, "utf-8");
|
|
2035
2326
|
return configPath;
|
|
2036
2327
|
}
|
|
2037
2328
|
function scaffoldTokens(cwd = process.cwd()) {
|
|
2038
|
-
const libDir =
|
|
2039
|
-
const appDir =
|
|
2040
|
-
if (!
|
|
2041
|
-
if (!
|
|
2042
|
-
const tokensPath =
|
|
2043
|
-
if (!
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
const cssPath =
|
|
2047
|
-
if (!
|
|
2048
|
-
|
|
2329
|
+
const libDir = path5.resolve(cwd, "src/lib");
|
|
2330
|
+
const appDir = path5.resolve(cwd, "src/app");
|
|
2331
|
+
if (!fs5.existsSync(libDir)) fs5.mkdirSync(libDir, { recursive: true });
|
|
2332
|
+
if (!fs5.existsSync(appDir)) fs5.mkdirSync(appDir, { recursive: true });
|
|
2333
|
+
const tokensPath = path5.join(libDir, "tokens.ts");
|
|
2334
|
+
if (!fs5.existsSync(tokensPath)) {
|
|
2335
|
+
fs5.writeFileSync(tokensPath, tokensTemplate, "utf-8");
|
|
2336
|
+
}
|
|
2337
|
+
const cssPath = path5.join(appDir, "globals.css");
|
|
2338
|
+
if (!fs5.existsSync(cssPath)) {
|
|
2339
|
+
fs5.writeFileSync(cssPath, cssTemplate, "utf-8");
|
|
2049
2340
|
}
|
|
2050
2341
|
return { tokensPath, cssPath };
|
|
2051
2342
|
}
|
|
@@ -2055,13 +2346,13 @@ function scaffoldComponent(componentName, cwd = process.cwd()) {
|
|
|
2055
2346
|
if (!template) {
|
|
2056
2347
|
return null;
|
|
2057
2348
|
}
|
|
2058
|
-
const uiDir =
|
|
2059
|
-
if (!
|
|
2060
|
-
|
|
2349
|
+
const uiDir = path5.resolve(cwd, "src/components/ui");
|
|
2350
|
+
if (!fs5.existsSync(uiDir)) {
|
|
2351
|
+
fs5.mkdirSync(uiDir, { recursive: true });
|
|
2061
2352
|
}
|
|
2062
|
-
const targetPath =
|
|
2063
|
-
if (!
|
|
2064
|
-
|
|
2353
|
+
const targetPath = path5.join(uiDir, template.filename);
|
|
2354
|
+
if (!fs5.existsSync(targetPath)) {
|
|
2355
|
+
fs5.writeFileSync(targetPath, template.content, "utf-8");
|
|
2065
2356
|
}
|
|
2066
2357
|
return targetPath;
|
|
2067
2358
|
}
|