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
5 changes: 5 additions & 0 deletions docs/type-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ Use `--hashed` when you want `.knighted-css` proxy modules to export `selectors`
CSS Modules hashing instead of stable selector strings. This keeps the module and selector
types while preserving hashed class names at runtime.

> [!NOTE]
> `--hashed` derives the selector list from the compiled CSS, so the generated sidecar can
> include class names that are not exported by the module (for example, sprinkles output from
> vanilla-extract). At runtime, `selectors` reflects only exported locals from the loader bridge, so the runtime map can be a subset of the generated sidecar.

> [!IMPORTANT]
> `--hashed` requires the bundler to route `?knighted-css` imports through
> `@knighted/css/loader-bridge`, so the proxy can read `knightedCss` and
Expand Down
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.

5 changes: 5 additions & 0 deletions packages/css/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ selectors.card // hashed CSS Modules class name
> `--hashed` requires wiring `@knighted/css/loader-bridge` to handle `?knighted-css` queries so
> the generated proxies can read `knightedCss` and `knightedCssModules` at build time.

> [!NOTE]
> `--hashed` builds the selector list from compiled CSS. The generated sidecar can therefore
> include class names that are not exported by the module (e.g. sprinkles output), while the
> runtime `selectors` map only includes exported locals from the loader bridge.

Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.

### Combined + runtime selectors
Expand Down
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.1.0",
"version": "1.1.1",
"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
16 changes: 10 additions & 6 deletions packages/css/src/loaderBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,18 @@ function resolveCssModules(
if (!locals || typeof locals !== 'object') continue
return locals as Record<string, string>
}
const isStringMapLocal = (value: object): value is Record<string, string> => {
const entries = Object.entries(value)
if (entries.length === 0) return false
return entries.every(([, entry]) => typeof entry === 'string')
const normalizeStringMapLocal = (value: object): Record<string, string> | undefined => {
const entries = Object.entries(value).filter(
([key]) => key !== 'default' && key !== '__esModule',
)
if (entries.length === 0) return undefined
if (!entries.every(([, entry]) => typeof entry === 'string')) return undefined
return Object.fromEntries(entries) as Record<string, string>
}
for (const candidate of candidates) {
if (!candidate || typeof candidate !== 'object') continue
if (isStringMapLocal(candidate)) return candidate
const normalized = normalizeStringMapLocal(candidate)
if (normalized) return normalized
}
const collectNamedExportsLocal = (
value: unknown,
Expand Down Expand Up @@ -443,7 +447,7 @@ function createBridgeModule(options: BridgeModuleOptions): string {
`const __knightedResolveCss = ${resolveCssText.toString()};`,
`const __knightedResolveCssModules = ${resolveCssModules.toString()};`,
`const __knightedUpstreamLocals =\n __knightedResolveCssModules(__knightedUpstream, __knightedUpstream);`,
`const __knightedLocalsExport =\n __knightedUpstreamLocals ??\n __knightedResolveCssModules(__knightedLocals, __knightedLocals) ??\n __knightedLocals;`,
`const __knightedLocalsExport =\n __knightedUpstreamLocals ??\n __knightedResolveCssModules(__knightedLocals, __knightedLocals);`,
`const __knightedBaseCss = __knightedResolveCss(__knightedDefault, __knightedUpstream);`,
`const __knightedCss = [__knightedBaseCss, ${cssValues.join(', ')}].filter(Boolean).join('\\n');`,
`export const ${DEFAULT_EXPORT_NAME} = __knightedCss;`,
Expand Down
17 changes: 17 additions & 0 deletions packages/css/test/loaderBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ test('resolveCssModules finds locals on module export', () => {
})
})

test('resolveCssModules ignores default string export', () => {
const module = { default: '.card{color:red}' }
assert.equal(__loaderBridgeInternals.resolveCssModules(module, module), undefined)
})

test('resolveCssModules omits default from named exports', () => {
const module = {
default: '.card{color:red}',
card: 'card_hash',
title: 'title_hash',
}
assert.deepEqual(__loaderBridgeInternals.resolveCssModules(module, module), {
card: 'card_hash',
title: 'title_hash',
})
})

test('pitch returns combined module wrapper when combined flag is present', async () => {
const ctx = createMockContext({
resourceQuery: '?knighted-css&combined',
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"pretest": "npm run types && npm run build"
},
"dependencies": {
"@knighted/css": "1.1.0",
"@knighted/css": "1.1.1",
"@knighted/jsx": "^1.7.5",
"lit": "^3.2.1",
"react": "^19.0.0",
Expand Down
Loading