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.cjs
CHANGED
|
@@ -48,8 +48,8 @@ __export(index_exports, {
|
|
|
48
48
|
module.exports = __toCommonJS(index_exports);
|
|
49
49
|
|
|
50
50
|
// src/config/loader.ts
|
|
51
|
-
var
|
|
52
|
-
var
|
|
51
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
52
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
53
53
|
var import_node_url = require("url");
|
|
54
54
|
|
|
55
55
|
// src/config/presets/berserk.ts
|
|
@@ -138,6 +138,9 @@ var berserkPreset = {
|
|
|
138
138
|
disallowRawHtml: {
|
|
139
139
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
140
140
|
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
141
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
142
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
143
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true },
|
|
141
144
|
table: { replaceWith: "Table", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
142
145
|
thead: { replaceWith: "TableHeader", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
143
146
|
tbody: { replaceWith: "TableBody", importFrom: "@/components/ui/table", checkRawElement: true },
|
|
@@ -189,7 +192,8 @@ var berserkPreset = {
|
|
|
189
192
|
"table-no-horizontal-stacking": "error",
|
|
190
193
|
"table-windowed-pagination": "error",
|
|
191
194
|
"tabs-component-required": "error",
|
|
192
|
-
"component-inventory-active": "error"
|
|
195
|
+
"component-inventory-active": "error",
|
|
196
|
+
"no-wrapping-compact-elements": "error"
|
|
193
197
|
}
|
|
194
198
|
};
|
|
195
199
|
|
|
@@ -226,7 +230,10 @@ var defaultPreset = {
|
|
|
226
230
|
components: {
|
|
227
231
|
disallowRawHtml: {
|
|
228
232
|
button: { replaceWith: "Button", importFrom: "@/components/ui/button", checkRawElement: true },
|
|
229
|
-
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true }
|
|
233
|
+
select: { replaceWith: "Dropdown", importFrom: "@/components/ui/dropdown", checkRawElement: true },
|
|
234
|
+
textarea: { replaceWith: "Textarea", importFrom: "@/components/ui/textarea", checkRawElement: true },
|
|
235
|
+
progress: { replaceWith: "ProgressBar", importFrom: "@/components/ui/progress", checkRawElement: true },
|
|
236
|
+
dialog: { replaceWith: "Modal or Dialog", importFrom: "@/components/ui/modal", checkRawElement: true }
|
|
230
237
|
},
|
|
231
238
|
disallowPseudoChips: true
|
|
232
239
|
},
|
|
@@ -237,7 +244,8 @@ var defaultPreset = {
|
|
|
237
244
|
"no-pseudo-chips": "warn",
|
|
238
245
|
"no-chip-colored-buttons": "error",
|
|
239
246
|
"no-text-arrows": "error",
|
|
240
|
-
"no-backdrop-blur": "warn"
|
|
247
|
+
"no-backdrop-blur": "warn",
|
|
248
|
+
"no-wrapping-compact-elements": "warn"
|
|
241
249
|
}
|
|
242
250
|
};
|
|
243
251
|
|
|
@@ -301,6 +309,221 @@ var pythonPreset = {
|
|
|
301
309
|
}
|
|
302
310
|
};
|
|
303
311
|
|
|
312
|
+
// src/detector/index.ts
|
|
313
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
314
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
315
|
+
|
|
316
|
+
// src/detector/ignores.ts
|
|
317
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
318
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
319
|
+
var DEFAULT_IGNORES = [
|
|
320
|
+
"**/node_modules/**",
|
|
321
|
+
"**/.next/**",
|
|
322
|
+
"**/dist/**",
|
|
323
|
+
"**/build/**",
|
|
324
|
+
"**/out/**",
|
|
325
|
+
"**/.turbo/**",
|
|
326
|
+
"**/.git/**",
|
|
327
|
+
"**/coverage/**",
|
|
328
|
+
"**/.venv/**",
|
|
329
|
+
"**/venv/**",
|
|
330
|
+
"**/env/**",
|
|
331
|
+
"**/__pycache__/**",
|
|
332
|
+
"**/.pytest_cache/**",
|
|
333
|
+
"**/*.pyc",
|
|
334
|
+
// Non-UI code
|
|
335
|
+
"**/api/**",
|
|
336
|
+
"**/*.test.*",
|
|
337
|
+
"**/*.spec.*",
|
|
338
|
+
"**/__tests__/**",
|
|
339
|
+
"**/*.stories.*",
|
|
340
|
+
"**/generated/**"
|
|
341
|
+
];
|
|
342
|
+
function getSmartIgnores(cwd) {
|
|
343
|
+
const result = new Set(DEFAULT_IGNORES);
|
|
344
|
+
const gitignorePath = import_node_path.default.resolve(cwd, ".gitignore");
|
|
345
|
+
if (import_node_fs.default.existsSync(gitignorePath)) {
|
|
346
|
+
try {
|
|
347
|
+
const content = import_node_fs.default.readFileSync(gitignorePath, "utf-8");
|
|
348
|
+
const lines = content.split(/\r?\n/);
|
|
349
|
+
for (let line of lines) {
|
|
350
|
+
line = line.trim();
|
|
351
|
+
if (!line || line.startsWith("#")) continue;
|
|
352
|
+
if (line.startsWith("/")) {
|
|
353
|
+
line = line.substring(1);
|
|
354
|
+
}
|
|
355
|
+
if (line.endsWith("/")) {
|
|
356
|
+
line = line.slice(0, -1);
|
|
357
|
+
}
|
|
358
|
+
if (line) {
|
|
359
|
+
result.add(`**/${line}/**`);
|
|
360
|
+
result.add(`**/${line}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return Array.from(result);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/detector/index.ts
|
|
370
|
+
function safeReadJson(filePath) {
|
|
371
|
+
try {
|
|
372
|
+
if (import_node_fs2.default.existsSync(filePath)) {
|
|
373
|
+
const raw = import_node_fs2.default.readFileSync(filePath, "utf-8");
|
|
374
|
+
return JSON.parse(raw);
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
function detectProject(cwd = process.cwd()) {
|
|
381
|
+
const pkg = safeReadJson(import_node_path2.default.resolve(cwd, "package.json"));
|
|
382
|
+
const deps = { ...pkg?.dependencies || {}, ...pkg?.devDependencies || {} };
|
|
383
|
+
const hasRequirements = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "requirements.txt"));
|
|
384
|
+
const hasPyproject = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "pyproject.toml"));
|
|
385
|
+
const hasPython = hasRequirements || hasPyproject || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "setup.py"));
|
|
386
|
+
let isStreamlit = false;
|
|
387
|
+
if (hasPython) {
|
|
388
|
+
if (hasRequirements) {
|
|
389
|
+
try {
|
|
390
|
+
const reqContent = import_node_fs2.default.readFileSync(import_node_path2.default.resolve(cwd, "requirements.txt"), "utf-8");
|
|
391
|
+
if (reqContent.toLowerCase().includes("streamlit")) {
|
|
392
|
+
isStreamlit = true;
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const isPnpmMonorepo = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "pnpm-workspace.yaml"));
|
|
399
|
+
const isTurbo = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "turbo.json"));
|
|
400
|
+
let monorepoWorkspaces = [];
|
|
401
|
+
if (Array.isArray(pkg?.workspaces)) {
|
|
402
|
+
monorepoWorkspaces = pkg.workspaces;
|
|
403
|
+
} else if (pkg?.workspaces?.packages) {
|
|
404
|
+
monorepoWorkspaces = pkg.workspaces.packages;
|
|
405
|
+
}
|
|
406
|
+
const isMonorepo = isPnpmMonorepo || isTurbo || monorepoWorkspaces.length > 0;
|
|
407
|
+
let framework = "unknown";
|
|
408
|
+
if (deps["next"]) {
|
|
409
|
+
framework = "nextjs";
|
|
410
|
+
} else if (deps["vite"]) {
|
|
411
|
+
framework = "vite";
|
|
412
|
+
} else if (deps["@remix-run/react"]) {
|
|
413
|
+
framework = "remix";
|
|
414
|
+
} else if (deps["astro"]) {
|
|
415
|
+
framework = "astro";
|
|
416
|
+
} else if (deps["react-scripts"]) {
|
|
417
|
+
framework = "react-cra";
|
|
418
|
+
} else if (isStreamlit) {
|
|
419
|
+
framework = "streamlit";
|
|
420
|
+
} else if (hasPython) {
|
|
421
|
+
framework = "python-general";
|
|
422
|
+
}
|
|
423
|
+
const libraries = [];
|
|
424
|
+
const hasShadcnConfig = import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "components.json"));
|
|
425
|
+
if (hasShadcnConfig || deps["@radix-ui/react-slot"]) {
|
|
426
|
+
libraries.push("shadcn");
|
|
427
|
+
}
|
|
428
|
+
if (Object.keys(deps).some((d) => d.startsWith("@radix-ui/"))) {
|
|
429
|
+
if (!libraries.includes("shadcn")) libraries.push("radix");
|
|
430
|
+
}
|
|
431
|
+
if (deps["tailwindcss"]) {
|
|
432
|
+
libraries.push("tailwind");
|
|
433
|
+
}
|
|
434
|
+
if (deps["lucide-react"] || deps["lucide"]) {
|
|
435
|
+
libraries.push("lucide");
|
|
436
|
+
}
|
|
437
|
+
if (deps["@mui/material"] || deps["@material-ui/core"]) {
|
|
438
|
+
libraries.push("mui");
|
|
439
|
+
}
|
|
440
|
+
if (deps["@chakra-ui/react"]) {
|
|
441
|
+
libraries.push("chakra");
|
|
442
|
+
}
|
|
443
|
+
const hasTailwind = Boolean(deps["tailwindcss"] || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tailwind.config.js")) || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tailwind.config.ts")));
|
|
444
|
+
let tailwindVersion;
|
|
445
|
+
if (hasTailwind) {
|
|
446
|
+
const rawVersion = deps["tailwindcss"] || "";
|
|
447
|
+
if (rawVersion.includes("^4") || rawVersion.includes("~4") || rawVersion.startsWith("4")) {
|
|
448
|
+
tailwindVersion = "v4";
|
|
449
|
+
} else {
|
|
450
|
+
tailwindVersion = "v3";
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const hasTypeScript = Boolean(deps["typescript"] || import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "tsconfig.json")));
|
|
454
|
+
const suggestedIncludes = [];
|
|
455
|
+
const extGroup = hasTypeScript ? "{tsx,jsx,ts,js}" : "{jsx,js}";
|
|
456
|
+
const candidateDirs = [
|
|
457
|
+
"frontend/src",
|
|
458
|
+
"frontend",
|
|
459
|
+
"apps/web/src",
|
|
460
|
+
"apps/web",
|
|
461
|
+
"client/src",
|
|
462
|
+
"client",
|
|
463
|
+
"src",
|
|
464
|
+
"app",
|
|
465
|
+
"pages",
|
|
466
|
+
"components"
|
|
467
|
+
];
|
|
468
|
+
let foundUiDir = false;
|
|
469
|
+
for (const dir of candidateDirs) {
|
|
470
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, dir))) {
|
|
471
|
+
if (dir === "frontend" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "frontend/src"))) continue;
|
|
472
|
+
if (dir === "client" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "client/src"))) continue;
|
|
473
|
+
if (dir === "apps/web" && import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, "apps/web/src"))) continue;
|
|
474
|
+
suggestedIncludes.push(`${dir}/**/*.${extGroup}`);
|
|
475
|
+
foundUiDir = true;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (hasPython) {
|
|
480
|
+
suggestedIncludes.push("**/*.py");
|
|
481
|
+
} else if (!foundUiDir) {
|
|
482
|
+
suggestedIncludes.push(`**/*.${extGroup}`);
|
|
483
|
+
}
|
|
484
|
+
const suggestedIgnores = getSmartIgnores(cwd);
|
|
485
|
+
if (hasShadcnConfig || libraries.includes("shadcn")) {
|
|
486
|
+
suggestedIgnores.push("**/components/ui/**");
|
|
487
|
+
}
|
|
488
|
+
const tokenCandidates = [
|
|
489
|
+
"frontend/src/lib/tokens.ts",
|
|
490
|
+
"frontend/src/tokens.ts",
|
|
491
|
+
"src/lib/tokens.ts",
|
|
492
|
+
"src/tokens.ts",
|
|
493
|
+
"src/design/tokens.ts",
|
|
494
|
+
"styles/tokens.css",
|
|
495
|
+
"src/app/globals.css",
|
|
496
|
+
"frontend/src/index.css",
|
|
497
|
+
"src/index.css"
|
|
498
|
+
];
|
|
499
|
+
let tokensFile;
|
|
500
|
+
for (const candidate of tokenCandidates) {
|
|
501
|
+
if (import_node_fs2.default.existsSync(import_node_path2.default.resolve(cwd, candidate))) {
|
|
502
|
+
tokensFile = candidate;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
let preset = "berserk";
|
|
507
|
+
if (framework === "streamlit" || framework === "python-general") {
|
|
508
|
+
preset = "python";
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
root: cwd,
|
|
512
|
+
isMonorepo,
|
|
513
|
+
monorepoWorkspaces,
|
|
514
|
+
framework,
|
|
515
|
+
libraries,
|
|
516
|
+
hasTailwind,
|
|
517
|
+
tailwindVersion,
|
|
518
|
+
hasTypeScript,
|
|
519
|
+
hasPython,
|
|
520
|
+
suggestedIncludes,
|
|
521
|
+
suggestedIgnores,
|
|
522
|
+
tokensFile,
|
|
523
|
+
preset
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
304
527
|
// src/config/loader.ts
|
|
305
528
|
var CONFIG_FILES = [
|
|
306
529
|
"dse.config.js",
|
|
@@ -315,17 +538,18 @@ function defineConfig(config) {
|
|
|
315
538
|
return config;
|
|
316
539
|
}
|
|
317
540
|
async function loadConfig(cwd = process.cwd(), customPath) {
|
|
541
|
+
const detected = detectProject(cwd);
|
|
318
542
|
let userConfig = {};
|
|
319
543
|
let foundConfigPath = null;
|
|
320
544
|
if (customPath) {
|
|
321
|
-
const resolved =
|
|
322
|
-
if (
|
|
545
|
+
const resolved = import_node_path3.default.resolve(cwd, customPath);
|
|
546
|
+
if (import_node_fs3.default.existsSync(resolved)) {
|
|
323
547
|
foundConfigPath = resolved;
|
|
324
548
|
}
|
|
325
549
|
} else {
|
|
326
550
|
for (const filename of CONFIG_FILES) {
|
|
327
|
-
const candidate =
|
|
328
|
-
if (
|
|
551
|
+
const candidate = import_node_path3.default.resolve(cwd, filename);
|
|
552
|
+
if (import_node_fs3.default.existsSync(candidate)) {
|
|
329
553
|
foundConfigPath = candidate;
|
|
330
554
|
break;
|
|
331
555
|
}
|
|
@@ -333,7 +557,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
333
557
|
}
|
|
334
558
|
if (foundConfigPath) {
|
|
335
559
|
if (foundConfigPath.endsWith(".json") || foundConfigPath.endsWith(".dserc")) {
|
|
336
|
-
const content =
|
|
560
|
+
const content = import_node_fs3.default.readFileSync(foundConfigPath, "utf-8");
|
|
337
561
|
try {
|
|
338
562
|
userConfig = JSON.parse(content);
|
|
339
563
|
} catch (err) {
|
|
@@ -346,7 +570,7 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
346
570
|
userConfig = mod.default || mod;
|
|
347
571
|
} catch {
|
|
348
572
|
try {
|
|
349
|
-
const content =
|
|
573
|
+
const content = import_node_fs3.default.readFileSync(foundConfigPath, "utf-8");
|
|
350
574
|
const cjsReq = createCjsEvaluator(foundConfigPath, content);
|
|
351
575
|
userConfig = cjsReq;
|
|
352
576
|
} catch (innerErr) {
|
|
@@ -355,18 +579,19 @@ async function loadConfig(cwd = process.cwd(), customPath) {
|
|
|
355
579
|
}
|
|
356
580
|
}
|
|
357
581
|
}
|
|
358
|
-
let presetName = userConfig.preset;
|
|
359
|
-
if (!presetName) {
|
|
360
|
-
const isPython = import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "requirements.txt")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "pyproject.toml")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, ".streamlit")) || import_node_fs.default.existsSync(import_node_path.default.resolve(cwd, "manage.py"));
|
|
361
|
-
presetName = isPython ? "python" : "berserk";
|
|
362
|
-
}
|
|
582
|
+
let presetName = userConfig.preset || detected.preset;
|
|
363
583
|
let basePreset = berserkPreset;
|
|
364
584
|
if (presetName === "python") {
|
|
365
585
|
basePreset = pythonPreset;
|
|
366
586
|
} else if (presetName === "recommended") {
|
|
367
587
|
basePreset = defaultPreset;
|
|
368
588
|
}
|
|
369
|
-
|
|
589
|
+
const autonomousDefaults = {
|
|
590
|
+
include: detected.suggestedIncludes,
|
|
591
|
+
exclude: detected.suggestedIgnores
|
|
592
|
+
};
|
|
593
|
+
const withAutonomous = mergeConfigs(basePreset, autonomousDefaults);
|
|
594
|
+
return mergeConfigs(withAutonomous, userConfig);
|
|
370
595
|
}
|
|
371
596
|
function createCjsEvaluator(filePath, content) {
|
|
372
597
|
if (content.includes("export default")) {
|
|
@@ -416,8 +641,8 @@ function mergeConfigs(base, overrides) {
|
|
|
416
641
|
}
|
|
417
642
|
|
|
418
643
|
// src/rules/runner.ts
|
|
419
|
-
var
|
|
420
|
-
var
|
|
644
|
+
var import_node_fs4 = __toESM(require("fs"), 1);
|
|
645
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
421
646
|
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
422
647
|
|
|
423
648
|
// src/parser/ast.ts
|
|
@@ -830,16 +1055,52 @@ function checkRawHtmlElements(tagName, attributes, pos, filePath, addViolation,
|
|
|
830
1055
|
pos.line,
|
|
831
1056
|
pos.col,
|
|
832
1057
|
"no-raw-html-elements",
|
|
833
|
-
`Raw browser default '<input type="checkbox">' detected. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
1058
|
+
`Raw browser default '<input type="checkbox">' detected. Never use unstyled browser checkboxes. Use standard design system '<Checkbox>' or '<Toggle>'.`,
|
|
1059
|
+
lineSnippet
|
|
1060
|
+
);
|
|
1061
|
+
} else if (typeVal === "radio") {
|
|
1062
|
+
addViolation(
|
|
1063
|
+
filePath,
|
|
1064
|
+
pos.line,
|
|
1065
|
+
pos.col,
|
|
1066
|
+
"no-raw-html-elements",
|
|
1067
|
+
`Raw browser default '<input type="radio">' detected. Never use unstyled browser radio buttons. Use standard design system '<RadioGroup>' or styled toggle.`,
|
|
1068
|
+
lineSnippet
|
|
1069
|
+
);
|
|
1070
|
+
} else if (typeVal === "range") {
|
|
1071
|
+
addViolation(
|
|
1072
|
+
filePath,
|
|
1073
|
+
pos.line,
|
|
1074
|
+
pos.col,
|
|
1075
|
+
"no-raw-html-elements",
|
|
1076
|
+
`Raw browser default '<input type="range">' detected. Never use unstyled browser sliders. Use standard design system '<Slider>'.`,
|
|
1077
|
+
lineSnippet
|
|
1078
|
+
);
|
|
1079
|
+
} else if (typeVal === "file") {
|
|
1080
|
+
addViolation(
|
|
1081
|
+
filePath,
|
|
1082
|
+
pos.line,
|
|
1083
|
+
pos.col,
|
|
1084
|
+
"no-raw-html-elements",
|
|
1085
|
+
`Raw browser default '<input type="file">' detected. Never display native unstyled file picker inputs. Use a custom styled dropzone or trigger.`,
|
|
1086
|
+
lineSnippet
|
|
1087
|
+
);
|
|
1088
|
+
} else if (typeVal === "color") {
|
|
1089
|
+
addViolation(
|
|
1090
|
+
filePath,
|
|
1091
|
+
pos.line,
|
|
1092
|
+
pos.col,
|
|
1093
|
+
"no-raw-html-elements",
|
|
1094
|
+
`Raw browser default '<input type="color">' detected. Never use native browser color pickers. Use a custom color picker component.`,
|
|
834
1095
|
lineSnippet
|
|
835
1096
|
);
|
|
836
|
-
} else if (["text", "email", "search"].includes(typeVal)) {
|
|
1097
|
+
} else if (["text", "email", "search", "password", "number", "tel", "url"].includes(typeVal)) {
|
|
837
1098
|
addViolation(
|
|
838
1099
|
filePath,
|
|
839
1100
|
pos.line,
|
|
840
1101
|
pos.col,
|
|
841
1102
|
"no-raw-html-elements",
|
|
842
|
-
`Raw '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
|
|
1103
|
+
`Raw browser default '<input type="${typeVal}">' element detected. Use standard design system '<Input>' component.`,
|
|
843
1104
|
lineSnippet
|
|
844
1105
|
);
|
|
845
1106
|
}
|
|
@@ -1097,6 +1358,9 @@ function checkPseudoChips(tagName, attributes, pos, filePath, addViolation, line
|
|
|
1097
1358
|
function checkElementClasses(node, tagName, attributes, pos, filePath, addViolation, sourceFile, config) {
|
|
1098
1359
|
if (!attributes.className) return;
|
|
1099
1360
|
const isButton = ["Button", "IconButton", "ButtonyIcon", "FloatingActionButton"].includes(tagName);
|
|
1361
|
+
const isChip = ["Chip", "Badge", "Tag", "StatusBadge"].includes(tagName);
|
|
1362
|
+
const isTab = ["Tab", "TabTrigger", "TabsTrigger"].includes(tagName);
|
|
1363
|
+
const isCompactElement = isButton || isChip || isTab;
|
|
1100
1364
|
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);
|
|
1101
1365
|
const lineSnippet = sourceFile.getFullText().split("\n")[pos.line - 1] || "";
|
|
1102
1366
|
const classStrings = extractClassNames(attributes.className.initializer, sourceFile);
|
|
@@ -1136,8 +1400,33 @@ function checkElementClasses(node, tagName, attributes, pos, filePath, addViolat
|
|
|
1136
1400
|
);
|
|
1137
1401
|
}
|
|
1138
1402
|
}
|
|
1403
|
+
if (isCompactElement && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1404
|
+
if (dec.core === "flex-wrap" || dec.core === "whitespace-normal" || dec.core === "whitespace-pre-wrap") {
|
|
1405
|
+
addViolation(
|
|
1406
|
+
filePath,
|
|
1407
|
+
nodePos.line,
|
|
1408
|
+
nodePos.col,
|
|
1409
|
+
"no-wrapping-compact-elements",
|
|
1410
|
+
`Disallowed text wrapping class '${dec.core}' on <${tagName}>. Compact interactive elements (buttons, chips, badges, tabs) must never wrap text across multiple lines.`,
|
|
1411
|
+
lineSnippet
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1139
1415
|
});
|
|
1140
1416
|
});
|
|
1417
|
+
if (tagName === "TableHead" && config.rules?.["no-wrapping-compact-elements"] !== "off") {
|
|
1418
|
+
const rawClass = attributes.className?.rawText || "";
|
|
1419
|
+
if (/\b(?:flex-wrap|whitespace-normal|whitespace-pre-wrap)\b/.test(rawClass)) {
|
|
1420
|
+
addViolation(
|
|
1421
|
+
filePath,
|
|
1422
|
+
pos.line,
|
|
1423
|
+
pos.col,
|
|
1424
|
+
"no-wrapping-compact-elements",
|
|
1425
|
+
`Disallowed text wrapping on <TableHead>. Column headers must remain single-line ('whitespace-nowrap') to preserve table column alignment.`,
|
|
1426
|
+
lineSnippet
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1141
1430
|
}
|
|
1142
1431
|
|
|
1143
1432
|
// src/rules/regex-rules.ts
|
|
@@ -1358,8 +1647,8 @@ function runPythonRules(content, filePath, addViolation, config) {
|
|
|
1358
1647
|
|
|
1359
1648
|
// src/rules/runner.ts
|
|
1360
1649
|
async function runDseScan(config, cwd = process.cwd()) {
|
|
1361
|
-
let targetDir =
|
|
1362
|
-
if (!
|
|
1650
|
+
let targetDir = import_node_path4.default.resolve(cwd, config.targetDir || "src");
|
|
1651
|
+
if (!import_node_fs4.default.existsSync(targetDir)) {
|
|
1363
1652
|
targetDir = cwd;
|
|
1364
1653
|
}
|
|
1365
1654
|
const includePatterns = config.include || ["**/*.{tsx,jsx,ts,js}"];
|
|
@@ -1391,7 +1680,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1391
1680
|
const violations = [];
|
|
1392
1681
|
const seenViolationKeys = /* @__PURE__ */ new Set();
|
|
1393
1682
|
const addViolation = (file, line, col, rule, message, snippet) => {
|
|
1394
|
-
const normFile =
|
|
1683
|
+
const normFile = import_node_path4.default.relative(cwd, file).replace(/\\/g, "/");
|
|
1395
1684
|
const key = `${normFile}:${line}:${rule}`;
|
|
1396
1685
|
if (seenViolationKeys.has(key)) return;
|
|
1397
1686
|
seenViolationKeys.add(key);
|
|
@@ -1409,7 +1698,7 @@ async function runDseScan(config, cwd = process.cwd()) {
|
|
|
1409
1698
|
};
|
|
1410
1699
|
let scannedJsxFiles = false;
|
|
1411
1700
|
for (const filePath of files) {
|
|
1412
|
-
const content =
|
|
1701
|
+
const content = import_node_fs4.default.readFileSync(filePath, "utf-8");
|
|
1413
1702
|
const isJsx = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") || filePath.endsWith(".ts") || filePath.endsWith(".js");
|
|
1414
1703
|
const isPythonOrTemplate = filePath.endsWith(".py") || filePath.endsWith(".html") || filePath.endsWith(".jinja") || filePath.endsWith(".jinja2") || filePath.endsWith(".css");
|
|
1415
1704
|
if (isJsx) {
|
|
@@ -1530,8 +1819,8 @@ function formatJson(result) {
|
|
|
1530
1819
|
}
|
|
1531
1820
|
|
|
1532
1821
|
// src/scaffold/index.ts
|
|
1533
|
-
var
|
|
1534
|
-
var
|
|
1822
|
+
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
1823
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1535
1824
|
|
|
1536
1825
|
// src/scaffold/templates/tokens.ts
|
|
1537
1826
|
var tokensTemplate = `/**
|
|
@@ -1674,7 +1963,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
1674
1963
|
({ className = "", variant = "primary", size = "default", children, ...props }, ref) => {
|
|
1675
1964
|
// Standard 36px height, 8px radius, font-medium, strictly neutral palette
|
|
1676
1965
|
const baseClasses =
|
|
1677
|
-
"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";
|
|
1966
|
+
"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";
|
|
1967
|
+
|
|
1678
1968
|
|
|
1679
1969
|
const variantClasses = {
|
|
1680
1970
|
primary: "bg-neutral-900 text-white hover:bg-neutral-800 shadow-2xs",
|
|
@@ -1715,7 +2005,8 @@ export const Chip: React.FC<ChipProps> = ({
|
|
|
1715
2005
|
}) => {
|
|
1716
2006
|
// Standard 36px height, 8px radius, 14px typography
|
|
1717
2007
|
const baseClasses =
|
|
1718
|
-
"inline-flex items-center h-[36px] min-h-[36px] px-3 rounded-md text-sm font-medium gap-1.5 select-none";
|
|
2008
|
+
"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";
|
|
2009
|
+
|
|
1719
2010
|
|
|
1720
2011
|
const variantClasses = {
|
|
1721
2012
|
neutral: "bg-neutral-100 text-neutral-800 border border-neutral-200",
|
|
@@ -1885,12 +2176,12 @@ export const TableHead = React.forwardRef<HTMLTableCellElement, TableHeadProps>(
|
|
|
1885
2176
|
({ className = "", icon, resizable = true, children, ...props }, ref) => (
|
|
1886
2177
|
<th
|
|
1887
2178
|
ref={ref}
|
|
1888
|
-
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative \${className}\`}
|
|
2179
|
+
className={\`h-[36px] px-4 text-left font-medium text-sm text-neutral-900 select-none align-middle relative whitespace-nowrap \${className}\`}
|
|
1889
2180
|
{...props}
|
|
1890
2181
|
>
|
|
1891
2182
|
<div className="flex items-center gap-2">
|
|
1892
2183
|
{icon && <span className="text-neutral-500 shrink-0">{icon}</span>}
|
|
1893
|
-
<span>{children}</span>
|
|
2184
|
+
<span className="whitespace-nowrap">{children}</span>
|
|
1894
2185
|
</div>
|
|
1895
2186
|
</th>
|
|
1896
2187
|
)
|
|
@@ -1936,7 +2227,7 @@ export const Tabs: React.FC<TabsProps> = ({ items, activeId, onChange, className
|
|
|
1936
2227
|
type="button"
|
|
1937
2228
|
disabled={tab.disabled}
|
|
1938
2229
|
onClick={() => onChange(tab.id)}
|
|
1939
|
-
className={\`inline-flex h-[28px] items-center justify-center gap-1.5 rounded-sm px-3 text-sm font-medium transition-all select-none \${
|
|
2230
|
+
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 \${
|
|
1940
2231
|
isActive
|
|
1941
2232
|
? "bg-white text-neutral-900 shadow-2xs"
|
|
1942
2233
|
: "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200/50"
|
|
@@ -2028,8 +2319,8 @@ var COMPONENT_TEMPLATES = {
|
|
|
2028
2319
|
};
|
|
2029
2320
|
function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
2030
2321
|
if (preset === "python") {
|
|
2031
|
-
const jsonPath =
|
|
2032
|
-
if (
|
|
2322
|
+
const jsonPath = import_node_path5.default.resolve(cwd, "dse.config.json");
|
|
2323
|
+
if (import_node_fs5.default.existsSync(jsonPath)) return jsonPath;
|
|
2033
2324
|
const content2 = JSON.stringify(
|
|
2034
2325
|
{
|
|
2035
2326
|
preset: "python",
|
|
@@ -2046,11 +2337,11 @@ function scaffoldConfig(cwd = process.cwd(), preset = "berserk") {
|
|
|
2046
2337
|
null,
|
|
2047
2338
|
2
|
|
2048
2339
|
) + "\n";
|
|
2049
|
-
|
|
2340
|
+
import_node_fs5.default.writeFileSync(jsonPath, content2, "utf-8");
|
|
2050
2341
|
return jsonPath;
|
|
2051
2342
|
}
|
|
2052
|
-
const configPath =
|
|
2053
|
-
if (
|
|
2343
|
+
const configPath = import_node_path5.default.resolve(cwd, "dse.config.js");
|
|
2344
|
+
if (import_node_fs5.default.existsSync(configPath)) {
|
|
2054
2345
|
return configPath;
|
|
2055
2346
|
}
|
|
2056
2347
|
const content = `import { defineConfig } from "design-system-enforcer";
|
|
@@ -2078,21 +2369,21 @@ export default defineConfig({
|
|
|
2078
2369
|
],
|
|
2079
2370
|
});
|
|
2080
2371
|
`;
|
|
2081
|
-
|
|
2372
|
+
import_node_fs5.default.writeFileSync(configPath, content, "utf-8");
|
|
2082
2373
|
return configPath;
|
|
2083
2374
|
}
|
|
2084
2375
|
function scaffoldTokens(cwd = process.cwd()) {
|
|
2085
|
-
const libDir =
|
|
2086
|
-
const appDir =
|
|
2087
|
-
if (!
|
|
2088
|
-
if (!
|
|
2089
|
-
const tokensPath =
|
|
2090
|
-
if (!
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
const cssPath =
|
|
2094
|
-
if (!
|
|
2095
|
-
|
|
2376
|
+
const libDir = import_node_path5.default.resolve(cwd, "src/lib");
|
|
2377
|
+
const appDir = import_node_path5.default.resolve(cwd, "src/app");
|
|
2378
|
+
if (!import_node_fs5.default.existsSync(libDir)) import_node_fs5.default.mkdirSync(libDir, { recursive: true });
|
|
2379
|
+
if (!import_node_fs5.default.existsSync(appDir)) import_node_fs5.default.mkdirSync(appDir, { recursive: true });
|
|
2380
|
+
const tokensPath = import_node_path5.default.join(libDir, "tokens.ts");
|
|
2381
|
+
if (!import_node_fs5.default.existsSync(tokensPath)) {
|
|
2382
|
+
import_node_fs5.default.writeFileSync(tokensPath, tokensTemplate, "utf-8");
|
|
2383
|
+
}
|
|
2384
|
+
const cssPath = import_node_path5.default.join(appDir, "globals.css");
|
|
2385
|
+
if (!import_node_fs5.default.existsSync(cssPath)) {
|
|
2386
|
+
import_node_fs5.default.writeFileSync(cssPath, cssTemplate, "utf-8");
|
|
2096
2387
|
}
|
|
2097
2388
|
return { tokensPath, cssPath };
|
|
2098
2389
|
}
|
|
@@ -2102,13 +2393,13 @@ function scaffoldComponent(componentName, cwd = process.cwd()) {
|
|
|
2102
2393
|
if (!template) {
|
|
2103
2394
|
return null;
|
|
2104
2395
|
}
|
|
2105
|
-
const uiDir =
|
|
2106
|
-
if (!
|
|
2107
|
-
|
|
2396
|
+
const uiDir = import_node_path5.default.resolve(cwd, "src/components/ui");
|
|
2397
|
+
if (!import_node_fs5.default.existsSync(uiDir)) {
|
|
2398
|
+
import_node_fs5.default.mkdirSync(uiDir, { recursive: true });
|
|
2108
2399
|
}
|
|
2109
|
-
const targetPath =
|
|
2110
|
-
if (!
|
|
2111
|
-
|
|
2400
|
+
const targetPath = import_node_path5.default.join(uiDir, template.filename);
|
|
2401
|
+
if (!import_node_fs5.default.existsSync(targetPath)) {
|
|
2402
|
+
import_node_fs5.default.writeFileSync(targetPath, template.content, "utf-8");
|
|
2112
2403
|
}
|
|
2113
2404
|
return targetPath;
|
|
2114
2405
|
}
|