Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knighted/css",
"version": "1.0.0-rc.12",
"version": "1.0.0-rc.13",
"description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.",
"type": "module",
"main": "./dist/css.js",
Expand Down
59 changes: 59 additions & 0 deletions packages/css/src/stableSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,27 @@ export interface StableClassNameOptions extends StableSelectorOptions {
join?: (values: string[]) => string
}

export interface MergeStableClassSingleInput extends StableSelectorOptions {
hashed: string | string[]
selector?: string
token: string
join?: (values: string[]) => string
}

export interface MergeStableClassBatchInput<
Hashed extends Record<string, string | string[]>,
Selectors extends Record<string, string> | undefined = Record<string, string>,
> extends StableSelectorOptions {
hashed: Hashed
selectors?: Selectors
join?: (values: string[]) => string
}

const defaultJoin = (values: string[]) => values.filter(Boolean).join(' ')

const toArray = (value: string | string[]): string[] =>
Array.isArray(value) ? value : [value]

const normalizeToken = (token: string): string => {
const sanitized = token
.trim()
Expand Down Expand Up @@ -55,3 +74,43 @@ export function stableClassName<T extends Record<string, string>>(
}

export const stableClassFromModule = stableClassName

export function mergeStableClass(input: MergeStableClassSingleInput): string
export function mergeStableClass<Hashed extends Record<string, string | string[]>>(
input: MergeStableClassBatchInput<Hashed>,
): { [Key in keyof Hashed]: string }
export function mergeStableClass(
input:
| MergeStableClassSingleInput
| MergeStableClassBatchInput<Record<string, string | string[]>>,
): string | Record<string, string> {
if ('token' in input) {
return mergeSingle(input)
}
return mergeBatch(input)
}

function mergeSingle(input: MergeStableClassSingleInput): string {
const join = input.join ?? defaultJoin
const hashed = toArray(input.hashed)
const stable = input.selector?.trim().length
? input.selector
: stableClass(input.token, { namespace: input.namespace })
return join([...hashed, stable])
}

function mergeBatch<Hashed extends Record<string, string | string[]>>(
input: MergeStableClassBatchInput<Hashed>,
): Record<keyof Hashed, string> {
const join = input.join ?? defaultJoin
const output: Partial<Record<keyof Hashed, string>> = {}
for (const key of Object.keys(input.hashed) as Array<keyof Hashed>) {
const hashedValue = input.hashed[key]
const selector = input.selectors?.[String(key)]
const stable = selector?.trim().length
? selector
: stableClass(String(key), { namespace: input.namespace })
output[key] = join([...toArray(hashedValue), stable])
}
return output as Record<keyof Hashed, string>
}
25 changes: 25 additions & 0 deletions packages/css/test/stableSelectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import test from 'node:test'

import {
createStableClassFactory,
mergeStableClass,
stableClass,
stableClassFromModule,
stableClassName,
Expand Down Expand Up @@ -55,3 +56,27 @@ test('stableClassFromModule is an alias', () => {
test('stableSelector returns a CSS selector string', () => {
assert.equal(stableSelector('badge'), '.knighted-badge')
})

test('mergeStableClass combines hashed and selector strings', () => {
const combined = mergeStableClass({
hashed: 'badge__hash',
selector: 'storybook-badge',
token: 'badge',
})
assert.equal(combined, 'badge__hash storybook-badge')
})

test('mergeStableClass batch mode merges entire objects', () => {
const hashed = {
badge: 'badge__hash',
cta: ['cta__hash', 'cta__variant'],
}
const selectors = {
badge: 'storybook-badge',
}
const combined = mergeStableClass({ hashed, selectors, namespace: 'storybook' })
assert.deepEqual(combined, {
badge: 'badge__hash storybook-badge',
cta: 'cta__hash cta__variant storybook-cta',
})
})
2 changes: 1 addition & 1 deletion packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"pretest": "npm run build"
},
"dependencies": {
"@knighted/css": "1.0.0-rc.12",
"@knighted/css": "1.0.0-rc.13",
"@knighted/jsx": "^1.4.1",
"lit": "^3.2.1",
"react": "^19.0.0",
Expand Down