@kenlo/kdls-design-system 2.6.0 → 2.7.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": {
3
3
  "name": "@kenlo/kdls-design-system",
4
- "version": "2.6.0"
4
+ "version": "2.7.0"
5
5
  },
6
6
  "install": {
7
7
  "add": "npm install @kenlo/kdls-design-system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenlo/kdls-design-system",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "KDLS Design System — componentes React + TypeScript",
@@ -19,9 +19,13 @@
19
19
  "./dist/index.css": "./dist/index.css",
20
20
  "./manifest.json": "./kdls-manifest.json"
21
21
  },
22
+ "bin": {
23
+ "kdls-guard": "scripts/kdls-guard.mjs"
24
+ },
22
25
  "files": [
23
26
  "dist",
24
27
  "kdls-manifest.json",
28
+ "scripts/kdls-guard.mjs",
25
29
  "foundations/tokens.css",
26
30
  "foundations/Scrollbar/Scrollbar.css",
27
31
  "foundations/styles.css",
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ // kdls-guard — counts hand-built UI in an app that the KDLS design system already provides.
3
+ //
4
+ // npx -y -p @kenlo/kdls-design-system@<version> kdls-guard app # report only
5
+ // ... kdls-guard --write-baseline kdls-guard.baseline.json app # record today's counts
6
+ // ... kdls-guard --baseline kdls-guard.baseline.json app # CI: exit 1 above a ceiling
7
+ //
8
+ // The checks are the "Validação no CI/CD" table of ADR-PROD-004 plus two patterns the first
9
+ // real consumer showed that table misses (numeric fontSize/borderRadius in inline style
10
+ // objects, Tailwind pixel sizes). Consumers run it with allow_failure and a baseline equal to
11
+ // today's count, so it only complains when a change ADDS hand-built UI. A line containing
12
+ // `kdls-guard-ignore` is skipped: use it for a documented exception.
13
+ //
14
+ // ponytail: regexes over comment-stripped lines, not a CSS/TSX parser. A `//` or `/*` inside a
15
+ // string can be miscounted; the baseline absorbs that noise. Move to a parser only if a count
16
+ // is ever disputed.
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HEX = '#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\\b'
22
+ const STYLE_EXT = new Set(['.css', '.scss'])
23
+ const CODE_EXT = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])
24
+ const JSX_EXT = new Set(['.tsx', '.jsx'])
25
+ const SKIP_DIRS = new Set(['node_modules', '.next', 'dist', 'build', 'out', 'coverage', '.git'])
26
+
27
+ export const CHECKS = [
28
+ { id: 'css-hex', label: 'cor hex escrita no CSS', on: 'style', re: new RegExp(HEX, 'g'), declarationsOnly: true },
29
+ { id: 'css-font-size-px', label: 'font-size em px no CSS', on: 'style', re: /font-size\s*:\s*\d[\d.]*px/g },
30
+ { id: 'css-border-radius-px', label: 'border-radius em px no CSS', on: 'style', re: /border-radius\s*:\s*\d[\d.]*px/g },
31
+ { id: 'code-hex', label: 'cor hex escrita no codigo', on: 'code', re: new RegExp(`(?<=['"\`\\[(,])${HEX}`, 'g') },
32
+ { id: 'inline-style', label: 'style={{ }} inline', on: 'jsx', re: /style=\{\{/g },
33
+ {
34
+ id: 'inline-size-number',
35
+ label: 'fontSize/borderRadius numerico inline',
36
+ on: 'code',
37
+ re: /\b(?:fontSize|borderRadius)\s*:\s*(?:\d|['"`]\d[\d.]*(?:px)?['"`])/g,
38
+ },
39
+ {
40
+ id: 'tailwind-px',
41
+ label: 'tamanho em px do Tailwind (text-[Npx], rounded-[Npx])',
42
+ on: 'code',
43
+ re: /\b(?:text|rounded(?:-[a-z]{1,2})?)-\[\d[\d.]*px\]/g,
44
+ },
45
+ ]
46
+
47
+ const byName = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
48
+
49
+ function* walk(dir) {
50
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name))) {
51
+ const full = path.join(dir, entry.name)
52
+ if (entry.isDirectory()) {
53
+ if (!SKIP_DIRS.has(entry.name)) yield* walk(full)
54
+ } else if (entry.isFile()) {
55
+ yield full
56
+ }
57
+ }
58
+ }
59
+
60
+ // Blank out comments but keep every newline, so line N of the clean text is line N of the file.
61
+ function stripComments(text, isStyle) {
62
+ const noBlocks = text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ''))
63
+ return isStyle ? noBlocks : noBlocks.replace(/(^|\s)\/\/.*$/gm, '$1')
64
+ }
65
+
66
+ export function scan(dirs, root = process.cwd()) {
67
+ const counts = Object.fromEntries(CHECKS.map((c) => [c.id, 0]))
68
+ const files = Object.fromEntries(CHECKS.map((c) => [c.id, {}]))
69
+ for (const dir of dirs) {
70
+ for (const file of walk(path.resolve(root, dir))) {
71
+ const ext = path.extname(file)
72
+ const kind = { style: STYLE_EXT.has(ext), code: CODE_EXT.has(ext), jsx: JSX_EXT.has(ext) }
73
+ if (!kind.style && !kind.code) continue
74
+ const original = fs.readFileSync(file, 'utf8').split('\n')
75
+ const clean = stripComments(original.join('\n'), kind.style).split('\n')
76
+ const rel = path.relative(root, file).split(path.sep).join('/')
77
+ original.forEach((raw, i) => {
78
+ if (raw.includes('kdls-guard-ignore')) return
79
+ for (const check of CHECKS) {
80
+ if (!kind[check.on]) continue
81
+ let text = clean[i] ?? ''
82
+ if (check.declarationsOnly) {
83
+ // Only values count: `#faded {` is a selector, `color: #fade` is a colour.
84
+ if (text.includes('{')) text = text.slice(text.lastIndexOf('{') + 1)
85
+ if (!text.includes(':')) continue
86
+ }
87
+ const n = (text.match(check.re) || []).length
88
+ if (n) {
89
+ counts[check.id] += n
90
+ files[check.id][rel] = (files[check.id][rel] || 0) + n
91
+ }
92
+ }
93
+ })
94
+ }
95
+ }
96
+ return { counts, files }
97
+ }
98
+
99
+ function report({ counts, files }, ceilings) {
100
+ const lines = ['kdls-guard — interface feita a mao que o kit visual KDLS ja oferece (ADR-PROD-004)']
101
+ for (const check of CHECKS) {
102
+ const ceiling = ceilings ? ` teto ${ceilings[check.id] ?? 0}` : ''
103
+ lines.push(` ${check.id.padEnd(22)} ${String(counts[check.id]).padStart(6)}${ceiling} — ${check.label}`)
104
+ const top = Object.entries(files[check.id])
105
+ .sort((a, b) => b[1] - a[1] || byName(a[0], b[0]))
106
+ .slice(0, 5)
107
+ for (const [file, n] of top) lines.push(` ${String(n).padStart(6)} ${file}`)
108
+ }
109
+ return lines.join('\n')
110
+ }
111
+
112
+ export function main(args, root = process.cwd()) {
113
+ let baseline = null
114
+ let writeBaseline = null
115
+ const dirs = []
116
+ for (let i = 0; i < args.length; i++) {
117
+ if (args[i] === '--baseline') baseline = args[++i]
118
+ else if (args[i] === '--write-baseline') writeBaseline = args[++i]
119
+ else dirs.push(args[i])
120
+ }
121
+ if (dirs.length === 0) dirs.push(...['app', 'src'].filter((d) => fs.existsSync(path.resolve(root, d))))
122
+ const missing = dirs.filter((d) => !fs.existsSync(path.resolve(root, d)))
123
+ if (dirs.length === 0 || missing.length) {
124
+ console.error(`kdls-guard: nenhuma pasta para ler${missing.length ? ` (nao existe: ${missing.join(', ')})` : ''}`)
125
+ return 2
126
+ }
127
+
128
+ const result = scan(dirs, root)
129
+ if (writeBaseline) {
130
+ const body = {
131
+ _leia: 'Tetos do kdls-guard (ADR-PROD-004), gravados com a contagem do dia. So podem descer.',
132
+ ceilings: result.counts,
133
+ }
134
+ fs.writeFileSync(path.resolve(root, writeBaseline), `${JSON.stringify(body, null, 2)}\n`)
135
+ console.log(report(result, null))
136
+ console.log(`\nTetos gravados em ${writeBaseline}.`)
137
+ return 0
138
+ }
139
+
140
+ let ceilings = null
141
+ if (baseline) {
142
+ const file = path.resolve(root, baseline)
143
+ if (!fs.existsSync(file)) {
144
+ console.error(`kdls-guard: arquivo de tetos nao existe: ${baseline}`)
145
+ return 2
146
+ }
147
+ ceilings = JSON.parse(fs.readFileSync(file, 'utf8')).ceilings || {}
148
+ }
149
+ console.log(report(result, ceilings))
150
+ if (!ceilings) {
151
+ console.log('\nSem --baseline: so relatorio.')
152
+ return 0
153
+ }
154
+ const above = CHECKS.filter((c) => result.counts[c.id] > (ceilings[c.id] ?? 0)).map((c) => c.id)
155
+ const below = CHECKS.filter((c) => result.counts[c.id] < (ceilings[c.id] ?? 0)).map((c) => c.id)
156
+ if (above.length) {
157
+ console.log(
158
+ `\nACIMA DO TETO: ${above.join(', ')}. Use a peca ou o token do kit (o Kenlo Brain responde qual: ` +
159
+ 'kdls_componente), ou marque uma excecao documentada com kdls-guard-ignore.',
160
+ )
161
+ return 1
162
+ }
163
+ console.log(`\nNada acima do teto.${below.length ? ` Abaixo do teto em ${below.join(', ')}: baixe o teto para travar o ganho.` : ''}`)
164
+ return 0
165
+ }
166
+
167
+ const invokedDirectly = (() => {
168
+ try {
169
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url))
170
+ } catch {
171
+ return false
172
+ }
173
+ })()
174
+ if (invokedDirectly) process.exit(main(process.argv.slice(2)))