Skip to content

Commit d60ba8a

Browse files
committed
Add classnames.js Set export to the CSS build
Extend script/export-css-selectors to emit static/classnames.js, a default-exported Set of every unique component CSS class name (excluding the utilities.css and layout.css utility classes). Publish the file via package.json files.
1 parent e187c55 commit d60ba8a

4 files changed

Lines changed: 491 additions & 5 deletions

File tree

.changeset/classnames-export.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/view-components": minor
3+
---
4+
5+
Add `static/classnames.js`, which default-exports a `Set` of every unique CSS class name in the library for consumers and tooling

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"files": [
2323
"static/*.json",
24+
"static/classnames.js",
2425
"app/assets/**/*",
2526
"app/components/primer/**/*.js",
2627
"app/components/primer/**/*.css",

script/export-css-selectors

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const snakeToCamelCase = (s) => s.split('_').map(capitalize).join('')
1212
const componentNameToRubyClass = (componentName) =>
1313
'Primer::' + componentName.split('/').map(snakeToCamelCase).join('::')
1414

15-
const exportSelectors = (folder) => {
15+
// Collects every unique bare class name (without the leading ".") across all
16+
// processed folders so we can emit a flat set in static/classnames.js.
17+
const allClassNames = new Set()
18+
19+
const exportSelectors = (folder, {classNameExcludedFiles = []} = {}) => {
1620
const folderGlob = `${folder}/**/*.css`
1721
const componentNameRegex = new RegExp(`${folder.replace('/','\\/')}\\/(.*).css`)
1822

@@ -23,6 +27,7 @@ const exportSelectors = (folder) => {
2327
const css = await readFile(file, 'utf8')
2428
const root = postcss.parse(css)
2529
const componentName = componentNameRegex.exec(file)[1]
30+
const collectClassNames = !classNameExcludedFiles.includes(file)
2631

2732
const selectors = []
2833
const classFiles = []
@@ -38,11 +43,24 @@ const exportSelectors = (folder) => {
3843

3944
rule.selectors.forEach(ruleSelector => {
4045
selectors.push(ruleSelector)
41-
const ruleObj = CSSwhat.parse(ruleSelector)[0][0]
46+
const parsed = CSSwhat.parse(ruleSelector)
47+
const ruleObj = parsed[0][0]
4248
if (ruleObj.type === 'attribute' && ruleObj.name === 'class') {
4349
const baseHtmlClass = ruleObj.value
4450
classFiles.push([baseHtmlClass, componentNameToRubyClass(componentName)])
4551
}
52+
53+
// Collect every class token from every compound of the selector
54+
// (not just the first) for the complete classnames.js set.
55+
if (collectClassNames) {
56+
parsed.forEach(compound => {
57+
compound.forEach(token => {
58+
if (token.type === 'attribute' && token.name === 'class') {
59+
allClassNames.add(token.value)
60+
}
61+
})
62+
})
63+
}
4664
})
4765
})
4866

@@ -66,7 +84,9 @@ const exportSelectors = (folder) => {
6684
const classShouldBeReserved = className =>
6785
(className[0].toUpperCase() === className[0])
6886

69-
exportSelectors('app/components/primer')
87+
const componentsBuild = exportSelectors('app/components/primer')
88+
89+
componentsBuild
7090
.then(classLists => {
7191
const htmlClassToRubyClasses = {}
7292

@@ -103,5 +123,26 @@ exportSelectors('app/components/primer')
103123
.catch(error => console.error("failed to write static/classes.json", { error }))
104124

105125
// stylesheets under app/lib/primer/css need their individual
106-
// json files generated
107-
exportSelectors('app/lib/primer/css')
126+
// json files generated. The utility and layout classes in these files are
127+
// intentionally excluded from the flat classnames.js set.
128+
const libBuild = exportSelectors('app/lib/primer/css', {
129+
classNameExcludedFiles: [
130+
'app/lib/primer/css/utilities.css',
131+
'app/lib/primer/css/layout.css'
132+
]
133+
})
134+
135+
// Once both folders have been processed, write the complete, flat set of every
136+
// unique class name in the library to static/classnames.js.
137+
Promise.all([componentsBuild, libBuild])
138+
.then(() => {
139+
const sortedClassNames = [...allClassNames].sort()
140+
const body = sortedClassNames.map(className => ` '${className}',`).join('\n')
141+
142+
console.log('Writing static/classnames.js')
143+
return writeFile(
144+
'static/classnames.js',
145+
`const classNames = new Set([\n${body}\n])\n\nexport default classNames\n`
146+
)
147+
})
148+
.catch(error => console.error("failed to write static/classnames.js", { error }))

0 commit comments

Comments
 (0)