Skip to content

Commit 1c0d075

Browse files
fix: vanilla extract import triggers declaration mode sidecar. (#74)
1 parent 91e0c91 commit 1c0d075

16 files changed

Lines changed: 257 additions & 9 deletions

docs/loader.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
`@knighted/css/loader` lets bundlers attach compiled CSS strings to any module by appending the `?knighted-css` query when importing. The loader mirrors the module graph, compiles every CSS dialect it discovers (CSS, Sass, Less, vanilla-extract, etc.), and exposes the concatenated result as `knightedCss`.
44

5+
## Loader vs bridge (quick comparison)
6+
7+
Use this table to decide which loader you need before wiring up rules:
8+
9+
| Capability | @knighted/css/loader | @knighted/css/loader-bridge |
10+
| ------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
11+
| Input | Original JS/TS module source + its style imports | Compiled CSS Modules output (post-hash) |
12+
| CSS extraction | Yes (walks the import graph) | No (wraps upstream output) |
13+
| Export behavior | Appends `knightedCss` (and optional selector exports) onto the original module | Exposes `knightedCss`/`knightedCssModules` only; does **not** re-export JS/TS module exports |
14+
| When to use | Default choice for `?knighted-css` in JS/TS modules | When you need hashed CSS Modules output for runtime `knightedCss` |
15+
| Combined wrapper needed? | Only for explicit `?knighted-css&combined` usage | **Yes** if you still need original JS/TS exports (use `&combined` via the resolver plugin) |
16+
517
## Loader example
618

719
```ts

docs/plugin.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ type KnightedCssResolverPluginOptions = {
5757
`.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.cts`, `.mjs`, `.cjs`.
5858
- `debug` (optional): Logs rewrite decisions and a summary of cache hits/misses.
5959
- `combinedPaths` (optional): List of strings or regexes. Any resolved path that matches will
60-
receive `&combined` alongside `?knighted-css`.
60+
receive `&combined` alongside `?knighted-css`. Use this when the request is handled by
61+
`@knighted/css/loader-bridge` (hashed CSS Modules) or whenever you need a wrapper module
62+
that re-exports the original JS/TS exports plus `knightedCss`.
6163
- `strictSidecar` (optional): When true, only modules present in the manifest are rewritten.
6264
Defaults to true when `manifestPath` is provided.
6365
- `manifestPath` (optional): Path to the sidecar manifest generated by
@@ -118,6 +120,21 @@ export default {
118120
If you have modules that are consumed with combined exports (`?knighted-css&combined`),
119121
set `combinedPaths` to ensure the resolver appends `&combined` during rewrites.
120122

123+
`&combined` is required when the underlying loader chain does **not** preserve the module’s
124+
original exports. The common case is declaration mode + `--hashed`, where requests flow through
125+
`@knighted/css/loader-bridge` and would otherwise only expose `knightedCss`/`knightedCssModules`.
126+
Appending `&combined` tells the loader to generate a small wrapper module that re-exports the
127+
original module and then appends the `knightedCss` exports.
128+
129+
You typically **do not** need `combinedPaths` when requests are handled by `@knighted/css/loader`
130+
(non-hashed declaration mode), because that loader appends exports directly onto the original
131+
module.
132+
133+
Outside of loader-bridge, `&combined` is still useful when you are **not** running
134+
`knighted-css-generate-types` but want a single runtime import that includes both the original
135+
JS/TS exports and `knightedCss` (for example, in runtime-only builds, tests, or tooling that
136+
cannot resolve the generated `.knighted-css` proxy modules).
137+
121138
Example of an import that relies on `&combined` at runtime:
122139

123140
```ts

docs/type-generation.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ The CLI only emits `.d.ts` sidecars for files that pass all of the following che
9494
- **Within the project root**: if a candidate file resolves outside `--root`, the CLI skips it
9595
and logs a warning.
9696
- **Imports styles**: the file must import/require a style resource directly (e.g. `.css`,
97-
`.scss`, `.sass`, `.less`) or resolve to one via tsconfig paths / resolver hooks.
97+
`.scss`, `.sass`, `.less`, or vanilla-extract `.css.ts` / `.css.js`) or resolve to one via
98+
tsconfig paths / resolver hooks.
9899
- **Produces selectors**: the extracted CSS must be non-empty and yield at least one selector
99100
token; otherwise the sidecar is skipped.
100101

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"engineStrict": true,
1414
"scripts": {
1515
"build": "npm run build -w @knighted/css",
16+
"prepare": "husky",
1617
"test": "npm run test -w @knighted/css",
1718
"pretest": "npm run build",
1819
"test:e2e": "npm run test -w @knighted/css-playwright-fixture",

packages/css/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/css",
3-
"version": "1.2.0-rc.0",
3+
"version": "1.2.0-rc.1",
44
"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.",
55
"type": "module",
66
"main": "./dist/css.js",

packages/css/src/generateTypes.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,18 @@ function isStyleResource(filePath: string): boolean {
10611061
return STYLE_EXTENSIONS.some(ext => normalized.endsWith(ext))
10621062
}
10631063

1064+
function isVanillaExtractResource(filePath: string): boolean {
1065+
const normalized = filePath.toLowerCase()
1066+
return (
1067+
normalized.endsWith('.css.ts') ||
1068+
normalized.endsWith('.css.js') ||
1069+
normalized.endsWith('.css.mts') ||
1070+
normalized.endsWith('.css.cts') ||
1071+
normalized.endsWith('.css.mjs') ||
1072+
normalized.endsWith('.css.cjs')
1073+
)
1074+
}
1075+
10641076
function isCssModuleResource(filePath: string): boolean {
10651077
return /\.module\.(css|scss|sass|less)$/i.test(filePath)
10661078
}
@@ -1116,7 +1128,7 @@ async function hasStyleImports(
11161128
if (!resource) {
11171129
continue
11181130
}
1119-
if (isStyleResource(resource)) {
1131+
if (isStyleResource(resource) || isVanillaExtractResource(resource)) {
11201132
return true
11211133
}
11221134
const resolved = await resolveImportPath(
@@ -1128,7 +1140,7 @@ async function hasStyleImports(
11281140
options.resolverFactory,
11291141
RESOLUTION_EXTENSIONS,
11301142
)
1131-
if (resolved && isStyleResource(resolved)) {
1143+
if (resolved && (isStyleResource(resolved) || isVanillaExtractResource(resolved))) {
11321144
return true
11331145
}
11341146
}

packages/css/test/generateTypes.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
resolveIndexFallback,
2828
readManifest,
2929
writeSidecarManifest,
30+
hasStyleImports,
3031
} = __generateTypesInternals
3132

3233
let cachedCliSnapshots: Record<string, string> | null = null
@@ -390,6 +391,46 @@ test('generateTypes declaration mode skips files without style imports', async (
390391
}
391392
})
392393

394+
test('hasStyleImports treats vanilla extract modules as style imports', async () => {
395+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'knighted-vanilla-'))
396+
try {
397+
const srcDir = path.join(root, 'src')
398+
await fs.mkdir(srcDir, { recursive: true })
399+
await fs.writeFile(
400+
path.join(srcDir, 'styles.css.ts'),
401+
"export const theme = { color: 'rebeccapurple' }\n",
402+
)
403+
const entryPath = path.join(srcDir, 'entry.ts')
404+
await fs.writeFile(entryPath, "import './styles.css.ts'\n")
405+
406+
const hasStyles = await hasStyleImports(entryPath, { rootDir: root })
407+
assert.equal(hasStyles, true)
408+
} finally {
409+
await fs.rm(root, { recursive: true, force: true })
410+
}
411+
})
412+
413+
test('hasStyleImports treats vanilla extract extension variants as style imports', async () => {
414+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'knighted-vanilla-variants-'))
415+
try {
416+
const srcDir = path.join(root, 'src')
417+
await fs.mkdir(srcDir, { recursive: true })
418+
const entryPath = path.join(srcDir, 'entry.ts')
419+
const variants = ['.css.js', '.css.mjs', '.css.cjs', '.css.mts', '.css.cts']
420+
421+
for (const ext of variants) {
422+
const stylePath = path.join(srcDir, `styles${ext}`)
423+
await fs.writeFile(stylePath, '')
424+
await fs.writeFile(entryPath, `import './styles${ext}'\n`)
425+
426+
const hasStyles = await hasStyleImports(entryPath, { rootDir: root })
427+
assert.equal(hasStyles, true)
428+
}
429+
} finally {
430+
await fs.rm(root, { recursive: true, force: true })
431+
}
432+
})
433+
393434
test('generateTypes hashed emits selector proxies for modules', async () => {
394435
const project = await setupFixtureProject()
395436
try {
@@ -616,7 +657,11 @@ test('generateTypes resolves hash-imports workspace package.json imports', async
616657
const sassModuleDir = path.join(appRoot, 'node_modules', 'sass')
617658
await fs.mkdir(path.dirname(sassModuleDir), { recursive: true })
618659
try {
619-
await fs.symlink(sassPackageDir, sassModuleDir)
660+
await fs.symlink(
661+
sassPackageDir,
662+
sassModuleDir,
663+
process.platform === 'win32' ? 'junction' : 'dir',
664+
)
620665
} catch {
621666
await fs.cp(sassPackageDir, sassModuleDir, { recursive: true })
622667
}

packages/playwright/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"pretest": "npm run types && npm run build"
3737
},
3838
"dependencies": {
39-
"@knighted/css": "1.2.0-rc.0",
39+
"@knighted/css": "1.2.0-rc.1",
4040
"@knighted/jsx": "^1.7.5",
4141
"lit": "^3.2.1",
4242
"react": "^19.0.0",

packages/playwright/rspack.mode.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ export default async () => ({
3333
},
3434
module: {
3535
rules: [
36+
{
37+
test: /\.css\.ts$/,
38+
type: 'javascript/auto',
39+
use: [
40+
{
41+
loader: '@knighted/css/loader',
42+
options: {
43+
vanilla: { transformToEsm: true },
44+
},
45+
},
46+
],
47+
},
3648
{
3749
test: /\.module\.css$/,
3850
include: declarationHashedDir,

0 commit comments

Comments
 (0)