forked from hplush/slowreader
-
Notifications
You must be signed in to change notification settings - Fork 0
/
check-names.ts
127 lines (110 loc) · 3.66 KB
/
check-names.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
// Check that all CSS classes in Svelte files follow our BEM name system:
// ui/foo/bar.svelte should have only classes like: .foo-bar, .foo-bar_element,
// .foo-bar_element.is-modifier
import { lstat, readdir, readFile } from 'node:fs/promises'
import { extname, join } from 'node:path'
import pico from 'picocolors'
import postcss from 'postcss'
import postcssHtml from 'postcss-html'
import nesting from 'postcss-nesting'
import selectorParser, {
type ClassName,
type Node
} from 'postcss-selector-parser'
let usedNames = new Set<string>()
let unwrapper = postcss([nesting])
function someParent(node: Node, cb: (node: Node) => boolean): false | Node {
if (cb(node)) return node
if (node.parent) return someParent(node.parent as Node, cb)
return false
}
function somePrevClass(node: Node, cb: (node: Node) => boolean): boolean {
if (cb(node)) return true
let prev = node.prev()
let notPseudo = someParent(node, i => i.value === ':not')
if (notPseudo) {
prev = notPseudo.prev()
}
if (prev && prev.type === 'class') return somePrevClass(prev, cb)
return false
}
const ALLOW_GLOBAL = /^(has-[a-z-]+|is-[a-z-]+|card)$/
function isGlobalModifier(node: ClassName): boolean {
return (
ALLOW_GLOBAL.test(node.value) &&
!!someParent(node, i => i.value === ':global')
)
}
function isBlock(node: Node, prefix: string): boolean {
return node.type === 'class' && node.value === prefix
}
function isElement(node: Node, prefix: string): boolean {
return (
node.type === 'class' &&
node.value.startsWith(prefix) &&
/^[a-z-]+_[a-z-]+$/.test(node.value)
)
}
function isModifier(node: Node, prefix: string): boolean {
return (
node.type === 'class' &&
/is-[a-z-]+/.test(node.value) &&
somePrevClass(node, i => isBlock(i, prefix) || isElement(i, prefix))
)
}
async function processComponents(dir: string, base: string): Promise<void> {
let files = await readdir(dir)
await Promise.all(
files.map(async file => {
let path = join(dir, file)
let name = path.slice(base.length + 1)
if (usedNames.has(name)) {
throw new Error(`Duplicate name: ${name}`)
}
usedNames.add(name)
let stat = await lstat(path)
if (stat.isDirectory()) {
await processComponents(path, base)
} else if (extname(file) === '.svelte') {
let content = await readFile(path, 'utf-8')
let unwrapped = unwrapper.process(content, {
from: path,
syntax: postcssHtml
}).root
let prefix = name
.replace(/^(ui|pages)\//, '')
.replace(/(\/index)?\.svelte$/, '')
.replaceAll('/', '-')
unwrapped.walkRules(rule => {
let classChecker = selectorParser(selector => {
selector.walkClasses(node => {
if (
!isGlobalModifier(node) &&
!isBlock(node, prefix) &&
!isElement(node, prefix) &&
!isModifier(node, prefix)
) {
let line = rule.source!.start!.line
process.stderr.write(pico.yellow(`${path}:${line}\n`))
process.stderr.write(
pico.red(
`Selector ${pico.yellow('.' + node.value)} does not ` +
`follow our BEM name system\n`
)
)
process.stderr.write(content.split('\n')[line - 1] + '\n')
process.exit(1)
}
})
})
classChecker.processSync(rule)
})
}
})
)
}
const ROOT = join(import.meta.dirname, '..')
await Promise.all([
processComponents(join(ROOT, 'ui'), ROOT),
processComponents(join(ROOT, 'pages'), ROOT)
])