Skip to content

Commit f22dbb3

Browse files
test: different import specifiers e2e. (#33)
1 parent bb4a10f commit f22dbb3

25 files changed

Lines changed: 553 additions & 32 deletions

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,25 @@ Need the module exports, `knightedCss`, and a runtime `stableSelectors` map from
113113

114114
```ts
115115
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
116-
import combined, { stableSelectors } from './button.js?knighted-css&combined&types'
117-
118-
const { default: Button, knightedCss } = combined as KnightedCssCombinedModule<
119-
typeof import('./button.js')
120-
>
116+
import { asKnightedCssCombinedModule } from '@knighted/css/loader-helpers'
117+
import type { ButtonStableSelectors } from './button.css.knighted-css.js'
118+
import * as buttonModule from './button.js?knighted-css&combined&types'
119+
120+
const {
121+
default: Button,
122+
knightedCss,
123+
stableSelectors,
124+
} = asKnightedCssCombinedModule<
125+
typeof import('./button.js'),
126+
{ stableSelectors: Readonly<Record<keyof ButtonStableSelectors, string>> }
127+
>(buttonModule)
128+
129+
stableSelectors.shell
121130
```
122131

123132
> [!NOTE]
124133
> `stableSelectors` here is for runtime use; TypeScript still reads literal tokens from the generated `.knighted-css.*` modules. For a full decision matrix, see [docs/combined-queries.md](./docs/combined-queries.md).
134+
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` instead of grabbing it from `@knighted/css/loader`the helper lives in a Node-free chunk so both browser and server bundles stay happy.
125135

126136
## Examples
127137

docs/combined-queries.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ This document summarizes how `?knighted-css&combined` behaves for different modu
55
> [!NOTE]
66
> TypeScript now reads literal selector tokens from the generated `.knighted-css.ts` modules (emitted by `knighted-css-generate-types`). Append `&types` to combined imports only when you also need `stableSelectors` at runtime—the loader still exports the map, while the double-extension modules keep your editors in sync.
77
8+
> [!TIP]
9+
> `KnightedCssCombinedModule` accepts an optional second generic parameter so you can describe loader-injected exports (for example, `{ stableSelectors: Record<string, string> }` when you append `&types`). That keeps the helper self-contained without intersecting additional types in your own code.
10+
11+
> [!TIP]
12+
> Prefer importing `asKnightedCssCombinedModule` from `@knighted/css/loader-helpers` when you want a runtime helper—the file has zero Node dependencies, so both browser and Node builds stay green.
13+
814
## Decision Matrix
915

1016
| Source module exports | Recommended query | TypeScript import pattern | Notes |
@@ -37,7 +43,8 @@ import type { KnightedCssCombinedModule } from '@knighted/css/loader'
3743
import combined from './module.js?knighted-css&combined'
3844

3945
const { default: Component, knightedCss } = combined as KnightedCssCombinedModule<
40-
typeof import('./module.js')
46+
typeof import('./module.js'),
47+
{ stableSelectors: typeof stableSelectors }
4148
>
4249
```
4350

@@ -51,7 +58,10 @@ const {
5158
default: Component,
5259
helper,
5360
knightedCss,
54-
} = combined as KnightedCssCombinedModule<typeof import('./module.js')>
61+
} = combined as KnightedCssCombinedModule<
62+
typeof import('./module.js'),
63+
{ stableSelectors: typeof stableSelectors }
64+
>
5565
```
5666

5767
Prefer `?knighted-css&combined&named-only` plus the [named exports only](#named-exports-only) snippet when you intentionally avoid default exports but still need the named members and `knightedCss`.

docs/loader.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,29 @@ Need the component exports **and** the compiled CSS from a single import? Use `?
4040

4141
```ts
4242
import type { KnightedCssCombinedModule } from '@knighted/css/loader'
43-
import buttonModule from './button.js?knighted-css&combined'
44-
45-
const { default: Button, knightedCss } = buttonModule as KnightedCssCombinedModule<
46-
typeof import('./button.js')
47-
>
43+
import { asKnightedCssCombinedModule } from '@knighted/css/loader-helpers'
44+
import * as buttonModule from './button.js?knighted-css&combined'
45+
46+
const { default: Button, knightedCss } =
47+
asKnightedCssCombinedModule<typeof import('./button.js')>(buttonModule)
48+
49+
// Need to describe additional loader-injected exports (for example, `stableSelectors` when
50+
// using `?knighted-css&combined&types`)? Pass a second generic:
51+
const {
52+
default: ButtonWithSelectors,
53+
knightedCss: buttonCss,
54+
stableSelectors,
55+
} = asKnightedCssCombinedModule<
56+
typeof import('./button.js'),
57+
{ stableSelectors: Record<string, string> }
58+
>(buttonModule)
4859
```
4960

5061
Append `&named-only` (alias: `&no-default`) if you never consume the default export. Refer to [docs/combined-queries.md](./combined-queries.md) for the full matrix of query flags and destructuring patterns.
5162

63+
> [!TIP]
64+
> `@knighted/css/loader-helpers` ships the `asKnightedCssCombinedModule` helper in isolation so you can safely import it from browser bundles. This keeps the heavy loader implementation (and its Node dependencies) out of client builds.
65+
5266
### Runtime selectors (`&types`)
5367

5468
When you need the runtime `stableSelectors` map alongside `knightedCss`, append `&types` to either the plain or combined import:

packages/css/loader-queries.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ declare module '*?knighted-css&types' {
1919
* TypeScript cannot infer the underlying module automatically, so consumers can
2020
* import the default export and narrow it with `KnightedCssCombinedModule<typeof import('./file')>`.
2121
*/
22-
type KnightedCssCombinedModule<TModule> = TModule & { knightedCss: string }
22+
type KnightedCssCombinedExtras = Readonly<Record<string, unknown>>
23+
24+
type KnightedCssCombinedModule<
25+
TModule,
26+
TExtras extends KnightedCssCombinedExtras = Record<never, never>,
27+
> = TModule & TExtras & { knightedCss: string }
2328

2429
declare module '*?knighted-css&combined' {
2530
const combined: KnightedCssCombinedModule<Record<string, unknown>>

packages/css/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
"loader": [
1111
"./dist/loader.d.ts"
1212
],
13+
"loader-helpers": [
14+
"./dist/loader-helpers.d.ts"
15+
],
1316
"loader-queries": [
1417
"./loader-queries.d.ts"
1518
],
@@ -32,6 +35,11 @@
3235
"import": "./dist/loader.js",
3336
"require": "./dist/cjs/loader.cjs"
3437
},
38+
"./loader-helpers": {
39+
"types": "./dist/loader-helpers.d.ts",
40+
"import": "./dist/loader-helpers.js",
41+
"require": "./dist/cjs/loader-helpers.cjs"
42+
},
3543
"./loader-queries": {
3644
"types": "./loader-queries.d.ts",
3745
"default": "./loader-queries.d.ts"

packages/css/src/loader-helpers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { KnightedCssCombinedModule } from './loader.js'
2+
3+
// Keep helper side-effect free so bundlers can safely tree-shake it into web targets.
4+
type KnightedCssCombinedExtras = Readonly<Record<string, unknown>>
5+
6+
export function asKnightedCssCombinedModule<
7+
TModule,
8+
TExtras extends KnightedCssCombinedExtras = Record<never, never>,
9+
>(module: unknown): KnightedCssCombinedModule<TModule, TExtras> {
10+
return module as KnightedCssCombinedModule<TModule, TExtras>
11+
}

packages/css/src/loader.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,15 @@ import {
1818
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js'
1919
import { resolveStableNamespace } from './stableNamespace.js'
2020

21-
export type KnightedCssCombinedModule<TModule> = TModule & {
22-
knightedCss: string
23-
}
21+
type KnightedCssCombinedExtras = Readonly<Record<string, unknown>>
22+
23+
export type KnightedCssCombinedModule<
24+
TModule,
25+
TExtras extends KnightedCssCombinedExtras = Record<never, never>,
26+
> = TModule &
27+
TExtras & {
28+
knightedCss: string
29+
}
2430

2531
export interface KnightedCssVanillaOptions {
2632
transformToEsm?: boolean
@@ -50,6 +56,7 @@ const loader: LoaderDefinitionFunction<KnightedCssLoaderOptions> = async functio
5056
namespace: resolvedNamespace,
5157
resourcePath: this.resourcePath,
5258
emitWarning: message => emitKnightedWarning(this, message),
59+
target: 'js',
5360
})
5461
: undefined
5562
const injection = buildInjection(css, {
@@ -131,6 +138,7 @@ export const pitch: PitchLoaderDefinitionFunction<KnightedCssLoaderOptions> =
131138
namespace: resolvedNamespace,
132139
resourcePath: this.resourcePath,
133140
emitWarning: message => emitKnightedWarning(this, message),
141+
target: 'js',
134142
})
135143
: undefined
136144
return createCombinedModule(request, css, {

packages/css/src/stableSelectorsLiteral.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@ export interface StableSelectorsLiteralResult {
77
selectorMap: Map<string, string>
88
}
99

10+
type StableSelectorsLiteralTarget = 'ts' | 'js'
11+
1012
export function buildStableSelectorsLiteral(options: {
1113
css: string
1214
namespace: string
1315
resourcePath: string
1416
emitWarning: (message: string) => void
17+
target?: StableSelectorsLiteralTarget
1518
}): StableSelectorsLiteralResult {
19+
const target: StableSelectorsLiteralTarget = options.target ?? 'ts'
1620
const trimmedNamespace = options.namespace.trim()
1721
if (!trimmedNamespace) {
1822
options.emitWarning(
1923
`stableSelectors requested for ${options.resourcePath} but "stableNamespace" resolved to an empty value.`,
2024
)
21-
return {
22-
literal: 'export const stableSelectors = {} as const;\n',
23-
selectorMap: new Map<string, string>(),
24-
}
25+
return finalizeLiteral(new Map<string, string>(), target)
2526
}
2627

2728
const selectorMap = collectStableSelectors(
@@ -34,9 +35,17 @@ export function buildStableSelectorsLiteral(options: {
3435
`stableSelectors requested for ${options.resourcePath} but no selectors matched namespace "${trimmedNamespace}".`,
3536
)
3637
}
38+
return finalizeLiteral(selectorMap, target)
39+
}
3740

41+
function finalizeLiteral(
42+
selectorMap: Map<string, string>,
43+
target: StableSelectorsLiteralTarget,
44+
): StableSelectorsLiteralResult {
45+
const formatted = formatStableSelectorMap(selectorMap)
46+
const suffix = target === 'ts' ? ' as const' : ''
3847
return {
39-
literal: `export const stableSelectors = ${formatStableSelectorMap(selectorMap)} as const;\n`,
48+
literal: `export const stableSelectors = ${formatted}${suffix};\n`,
4049
selectorMap,
4150
}
4251
}

packages/css/test/loader_unit.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ test('loader emits stableSelectors export when ?types flag is present', async ()
174174
assert.match(output, /export const stableSelectors = /)
175175
assert.match(
176176
output,
177-
/export const stableSelectors = Object\.freeze\(\{\s*"demo": "knighted-demo",\s*"icon": "knighted-icon"\s*\}\) as const;/,
177+
/export const stableSelectors = Object\.freeze\(\{\s*"demo": "knighted-demo",\s*"icon": "knighted-icon"\s*\}\);/,
178178
'should emit map of detected selectors using default namespace',
179179
)
180180
})
@@ -195,7 +195,7 @@ test('loader respects stableNamespace loader option', async () => {
195195

196196
assert.match(
197197
output,
198-
/export const stableSelectors = Object\.freeze\(\{\s*"card": "acme-card"\s*\}\) as const;/,
198+
/export const stableSelectors = Object\.freeze\(\{\s*"card": "acme-card"\s*\}\);/,
199199
'should scope selector discovery to provided namespace',
200200
)
201201
})
@@ -218,7 +218,7 @@ test('loader warns when stableNamespace option resolves to empty value', async (
218218
),
219219
)
220220

221-
assert.match(output, /export const stableSelectors = \{\} as const;/)
221+
assert.match(output, /export const stableSelectors = Object\.freeze\(\{\}\);/)
222222
assert.equal(warnings.length, 1)
223223
assert.match(
224224
warnings[0] ?? '',
@@ -273,7 +273,7 @@ test('pitch injects stableSelectors export when combined types query is used', a
273273
const combinedOutput = String(result ?? '')
274274
assert.match(
275275
combinedOutput,
276-
/export const stableSelectors = Object\.freeze\(\{\s*"demo": "knighted-demo",\s*"icon": "knighted-icon"\s*\}\) as const;/,
276+
/export const stableSelectors = Object\.freeze\(\{\s*"demo": "knighted-demo",\s*"icon": "knighted-icon"\s*\}\);/,
277277
'combined proxy should forward stable selector map',
278278
)
279279
})

packages/css/test/stableSelectorsLiteral.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,26 @@ test('buildStableSelectorsLiteral warns when namespace is empty', () => {
1414
resourcePath: 'demo.css',
1515
emitWarning: message => warnings.push(message),
1616
})
17-
assert.equal(result.literal.trim(), 'export const stableSelectors = {} as const;')
17+
assert.equal(
18+
result.literal.trim(),
19+
'export const stableSelectors = Object.freeze({}) as const;',
20+
)
1821
assert.equal(result.selectorMap.size, 0)
1922
assert.equal(warnings.length, 1)
2023
})
2124

25+
test('buildStableSelectorsLiteral emits JS-friendly literal when requested', () => {
26+
const result = buildStableSelectorsLiteral({
27+
css: '.knighted-card {}',
28+
namespace: 'knighted',
29+
resourcePath: 'demo.css',
30+
emitWarning: () => {},
31+
target: 'js',
32+
})
33+
assert.match(result.literal, /export const stableSelectors = Object\.freeze/)
34+
assert.ok(!result.literal.includes('as const'))
35+
})
36+
2237
test('collectStableSelectors captures selectors and formats map output', () => {
2338
const { collectStableSelectors, formatStableSelectorMap } =
2439
__stableSelectorsLiteralInternals

0 commit comments

Comments
 (0)