From 12bd51c2a119b34e13acd00c57d3eada2f77214f Mon Sep 17 00:00:00 2001 From: KCM Date: Sun, 21 Dec 2025 21:31:20 -0600 Subject: [PATCH] feat: merge stable utils. --- package-lock.json | 4 +- packages/css/package.json | 2 +- packages/css/src/stableSelectors.ts | 59 +++++++++++++++++++++++ packages/css/test/stableSelectors.test.ts | 25 ++++++++++ packages/playwright/package.json | 2 +- 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58c5b79..10977af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11922,7 +11922,7 @@ }, "packages/css": { "name": "@knighted/css", - "version": "1.0.0-rc.12", + "version": "1.0.0-rc.13", "license": "MIT", "dependencies": { "es-module-lexer": "^2.0.0", @@ -12222,7 +12222,7 @@ "name": "@knighted/css-playwright-fixture", "version": "0.0.0", "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", diff --git a/packages/css/package.json b/packages/css/package.json index a1c505a..773c204 100644 --- a/packages/css/package.json +++ b/packages/css/package.json @@ -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", diff --git a/packages/css/src/stableSelectors.ts b/packages/css/src/stableSelectors.ts index eceb2be..acda6e9 100644 --- a/packages/css/src/stableSelectors.ts +++ b/packages/css/src/stableSelectors.ts @@ -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, + Selectors extends Record | undefined = Record, +> 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() @@ -55,3 +74,43 @@ export function stableClassName>( } export const stableClassFromModule = stableClassName + +export function mergeStableClass(input: MergeStableClassSingleInput): string +export function mergeStableClass>( + input: MergeStableClassBatchInput, +): { [Key in keyof Hashed]: string } +export function mergeStableClass( + input: + | MergeStableClassSingleInput + | MergeStableClassBatchInput>, +): string | Record { + 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>( + input: MergeStableClassBatchInput, +): Record { + const join = input.join ?? defaultJoin + const output: Partial> = {} + for (const key of Object.keys(input.hashed) as Array) { + 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 +} diff --git a/packages/css/test/stableSelectors.test.ts b/packages/css/test/stableSelectors.test.ts index bb430c9..4c36a6c 100644 --- a/packages/css/test/stableSelectors.test.ts +++ b/packages/css/test/stableSelectors.test.ts @@ -3,6 +3,7 @@ import test from 'node:test' import { createStableClassFactory, + mergeStableClass, stableClass, stableClassFromModule, stableClassName, @@ -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', + }) +}) diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 87d4793..2ac3466 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -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",