@kenlo/kdls-design-system 2.5.1 → 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.
- package/README.md +21 -1
- package/kdls-manifest.json +7098 -0
- package/package.json +9 -2
- package/scripts/kdls-guard.mjs +174 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenlo/kdls-design-system",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "KDLS Design System — componentes React + TypeScript",
|
|
@@ -16,10 +16,16 @@
|
|
|
16
16
|
"./tokens.css": "./foundations/tokens.css",
|
|
17
17
|
"./styles.css": "./foundations/styles.css",
|
|
18
18
|
"./tailwind-preset": "./foundations/tailwind-preset.cjs",
|
|
19
|
-
"./dist/index.css": "./dist/index.css"
|
|
19
|
+
"./dist/index.css": "./dist/index.css",
|
|
20
|
+
"./manifest.json": "./kdls-manifest.json"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"kdls-guard": "scripts/kdls-guard.mjs"
|
|
20
24
|
},
|
|
21
25
|
"files": [
|
|
22
26
|
"dist",
|
|
27
|
+
"kdls-manifest.json",
|
|
28
|
+
"scripts/kdls-guard.mjs",
|
|
23
29
|
"foundations/tokens.css",
|
|
24
30
|
"foundations/Scrollbar/Scrollbar.css",
|
|
25
31
|
"foundations/styles.css",
|
|
@@ -43,6 +49,7 @@
|
|
|
43
49
|
"typecheck": "tsc --noEmit",
|
|
44
50
|
"gen:styles": "node scripts/gen-styles.mjs",
|
|
45
51
|
"gen:tokens": "node scripts/gen-tokens.mjs",
|
|
52
|
+
"gen:manifest": "node scripts/gen-manifest.mjs",
|
|
46
53
|
"prepublishOnly": "npm run build:lib"
|
|
47
54
|
},
|
|
48
55
|
"peerDependencies": {
|
|
@@ -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)))
|