Skip to content

Commit 5cb880b

Browse files
authored
Add @primer/css/classnames export with a Set of all classnames (#3080)
* Add @primer/css/classnames export with a Set of all classnames Generate dist/classnames.js during the dist build containing a Set of every unique bare class token across all bundles, and expose it via a new package.json exports map (with a wildcard fallback to preserve existing deep imports). * Updating package-lock * Make classnames export a dual ESM/CJS build with types Emit dist/classnames.cjs (module.exports = new Set) and dist/classnames.d.ts alongside the ESM build, and make the ./classnames subpath a conditional export (types/import/require) so CommonJS consumers can require() it.
1 parent d735d75 commit 5cb880b

5 files changed

Lines changed: 85 additions & 3 deletions

File tree

.changeset/eleven-dots-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/css': minor
3+
---
4+
5+
Adding a classnames export that has a list of all unique CSS classes in the library

__tests__/css.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
currentVersionDeprecations
77
} from './utils/css'
88
import semver from 'semver'
9+
import {createRequire} from 'module'
910

1011
let selectorsDiff, variablesDiff, version
1112

@@ -31,3 +32,35 @@ describe('deprecations', () => {
3132
})
3233
})
3334
})
35+
36+
describe('classnames', () => {
37+
let classNames
38+
39+
beforeAll(async () => {
40+
classNames = (await import('../dist/classnames.js')).default
41+
})
42+
43+
it('exports a non-empty Set', () => {
44+
expect(classNames).toBeInstanceOf(Set)
45+
expect(classNames.size).toBeGreaterThan(0)
46+
})
47+
48+
it('contains known classnames', () => {
49+
expect(classNames.has('btn')).toBe(true)
50+
expect(classNames.has('Box-body')).toBe(true)
51+
expect(classNames.has('d-flex')).toBe(true)
52+
})
53+
54+
it('contains bare tokens without a leading dot', () => {
55+
for (const className of classNames) {
56+
expect(className.startsWith('.')).toBe(false)
57+
}
58+
})
59+
60+
it('exposes the same Set from the CommonJS build', () => {
61+
const require = createRequire(import.meta.url)
62+
const cjsClassNames = require('../dist/classnames.cjs')
63+
expect(cjsClassNames).toBeInstanceOf(Set)
64+
expect([...cjsClassNames].sort()).toEqual([...classNames].sort())
65+
})
66+
})

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
"sass": "index.scss",
1313
"type": "module",
1414
"main": "dist/primer.js",
15+
"exports": {
16+
".": "./dist/primer.js",
17+
"./classnames": {
18+
"types": "./dist/classnames.d.ts",
19+
"import": "./dist/classnames.js",
20+
"require": "./dist/classnames.cjs"
21+
},
22+
"./*": "./*"
23+
},
1524
"repository": {
1625
"type": "git",
1726
"url": "git+https://github.com/primer/css.git"

script/dist.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const bundleNames = {
2626
async function dist() {
2727
try {
2828
const bundles = {}
29+
const classNames = new Set()
2930

3031
await remove(outDir)
3132
await mkdirp(statsDir)
@@ -61,9 +62,14 @@ async function dist() {
6162
throw new Error(`Warnings while compiling ${from}. See output above.`)
6263
}
6364

65+
const stats = cssstats(result.css)
66+
for (const className of getClassNames(stats.selectors.values)) {
67+
classNames.add(className)
68+
}
69+
6470
await Promise.all([
6571
writeFile(to, result.css, encoding),
66-
writeFile(meta.stats, JSON.stringify(cssstats(result.css)), encoding),
72+
writeFile(meta.stats, JSON.stringify(stats), encoding),
6773
writeFile(meta.js, `export {cssstats: require('./stats/${name}.json')}`, encoding),
6874
result.map ? writeFile(meta.map, result.map.toString(), encoding) : null
6975
])
@@ -74,6 +80,7 @@ async function dist() {
7480

7581
const meta = {bundles}
7682
await writeFile(join(outDir, 'meta.json'), JSON.stringify(meta, null, 2), encoding)
83+
await writeClassNames(classNames)
7784
await writeVariableData()
7885
await copy(join(inDir, 'deprecations.json'), join(outDir, 'deprecations.json'))
7986
} catch (error) {
@@ -97,6 +104,34 @@ function getPathName(path) {
97104
return path.replace(/\//g, '-')
98105
}
99106

107+
// Extract the bare class tokens (without the leading dot) from a list of
108+
// selector strings, e.g. ".Box-row:hover .btn" -> ["Box-row", "btn"].
109+
function getClassNames(selectors) {
110+
const names = new Set()
111+
const pattern = /\.((?:\\.|[\w-])+)/g
112+
for (const selector of selectors) {
113+
let match
114+
while ((match = pattern.exec(selector)) !== null) {
115+
names.add(match[1].replace(/\\(.)/g, '$1'))
116+
}
117+
}
118+
return names
119+
}
120+
121+
async function writeClassNames(classNames) {
122+
const sorted = [...classNames].sort()
123+
const list = JSON.stringify(sorted, null, 2)
124+
await Promise.all([
125+
writeFile(join(outDir, 'classnames.js'), `export default new Set(${list})\n`, encoding),
126+
writeFile(join(outDir, 'classnames.cjs'), `module.exports = new Set(${list})\n`, encoding),
127+
writeFile(
128+
join(outDir, 'classnames.d.ts'),
129+
`declare const classNames: Set<string>\nexport default classNames\n`,
130+
encoding
131+
)
132+
])
133+
}
134+
100135
dist()
101136

102137
async function writeVariableData() {

0 commit comments

Comments
 (0)