Skip to content

Commit 33ca549

Browse files
feat: support package.json imports.
1 parent 3eeaa8d commit 33ca549

22 files changed

Lines changed: 275 additions & 9 deletions

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ I needed a single source of truth for UI components that could drop into both li
2424
## Features
2525

2626
- Traverses module graphs with a built-in walker to find transitive style imports (no bundler required).
27-
- Resolution parity via [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver): tsconfig `paths`, package `exports` conditions, and extension aliasing (e.g., `.css.js``.css.ts`) are honored without wiring up a bundler.
27+
- Resolution parity via [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver): tsconfig `paths`, package `exports` + `imports`, and extension aliasing (e.g., `.css.js``.css.ts`) are honored without wiring up a bundler.
2828
- Compiles `*.css`, `*.scss`, `*.sass`, `*.less`, and `*.css.ts` (vanilla-extract) files out of the box.
2929
- Optional post-processing via [`lightningcss`](https://github.com/parcel-bundler/lightningcss) for minification, prefixing, media query optimizations, or specificity boosts.
3030
- Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion.
@@ -172,6 +172,9 @@ export async function render(url: string) {
172172

173173
The built-in walker already leans on [`oxc-resolver`](https://github.com/oxc-project/oxc-resolver), so tsconfig `paths`, package `exports` conditions, and common extension aliases work out of the box. If you still need to mirror bespoke behavior (virtual modules, framework-specific loaders, etc.), plug in a custom resolver. Here’s how to use [`enhanced-resolve`](https://github.com/webpack/enhanced-resolve):
174174

175+
> [!TIP]
176+
> Hash-prefixed specifiers defined in `package.json#imports` resolve automaticallyno extra loader or `css()` options required. Reach for a custom resolver only when you need behavior beyond what `oxc-resolver` already mirrors.
177+
175178
```ts
176179
import { ResolverFactory } from 'enhanced-resolve'
177180
import { css } from '@knighted/css'

docs/loader.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export default {
3434
}
3535
```
3636

37+
> [!NOTE]
38+
> The loader shares the same auto-configured `oxc-resolver` as the standalone `css()` API, so hash-prefixed specifiers declared under `package.json#imports` (for example, `#ui/button`) resolve without additional options.
39+
3740
### Combined imports
3841

3942
Need the component exports **and** the compiled CSS from a single import? Use `?knighted-css&combined` and narrow the result with `KnightedCssCombinedModule` to keep TypeScript happy:

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.

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.0.0-rc.14",
3+
"version": "1.0.0-rc.15",
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/moduleGraph.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,10 @@ function normalizeSpecifier(raw: string): string {
266266
if (!trimmed || trimmed.startsWith('\0')) {
267267
return ''
268268
}
269-
const queryIndex = trimmed.search(/[?#]/)
269+
const querySearchOffset = trimmed.startsWith('#') ? 1 : 0
270+
const remainder = trimmed.slice(querySearchOffset)
271+
const queryMatchIndex = remainder.search(/[?#]/)
272+
const queryIndex = queryMatchIndex === -1 ? -1 : querySearchOffset + queryMatchIndex
270273
const withoutQuery = queryIndex === -1 ? trimmed : trimmed.slice(0, queryIndex)
271274
if (!withoutQuery) {
272275
return ''
@@ -394,9 +397,7 @@ function createResolverFactory(
394397
options.extensionAlias = extensionAlias
395398
}
396399
const tsconfigOption = resolveResolverTsconfig(graphOptions?.tsConfig, cwd)
397-
if (tsconfigOption) {
398-
options.tsconfig = tsconfigOption
399-
}
400+
options.tsconfig = tsconfigOption ?? 'auto'
400401
return new ResolverFactory(options)
401402
}
402403

packages/css/test/moduleGraph.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,49 @@ import '@blocks/panel'
205205
await project.cleanup()
206206
}
207207
})
208+
209+
test('collectStyleImports keeps hash-prefixed specifiers intact', async () => {
210+
const project = await createProject('knighted-module-graph-imports-hash-')
211+
try {
212+
await project.writeFile(
213+
'package.json',
214+
JSON.stringify(
215+
{
216+
name: 'hash-imports',
217+
type: 'module',
218+
imports: {
219+
'#ui/*': './src/ui/*',
220+
},
221+
},
222+
null,
223+
2,
224+
),
225+
)
226+
await project.writeFile('src/ui/button.scss', '.button { color: hotpink; }')
227+
await project.writeFile(
228+
'src/ui/button.js',
229+
`import './button.scss'
230+
export const Button = () => null
231+
`,
232+
)
233+
await project.writeFile(
234+
'src/entry.ts',
235+
`import { Button } from '#ui/button.js'
236+
void Button
237+
`,
238+
)
239+
240+
const styles = await collectStyleImports(project.file('src/entry.ts'), {
241+
cwd: project.root,
242+
styleExtensions: ['.scss'],
243+
filter: () => true,
244+
})
245+
246+
assert.deepEqual(
247+
await realpathAll(styles),
248+
await realpathAll([project.file('src/ui/button.scss')]),
249+
)
250+
} finally {
251+
await project.cleanup()
252+
}
253+
})

packages/playwright/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# @knighted/css Playwright fixtures
2+
3+
This package builds the demo surface that Playwright pokes during CI. It now renders two scenarios side by side:
4+
5+
- **Lit + React wrapper**: the existing showcase that exercises vanilla CSS, Sass/Less, vanilla-extract, and the combined loader queries.
6+
- **Hash-imports workspace demo**: a minimal npm workspace under `src/hash-imports-workspace/` where `apps/hash-import-demo` uses `package.json#imports` (hash-prefixed specifiers) to resolve UI modules provided by a sibling workspace package. The fixture proves that `@knighted/css/loader` and the standalone `css()` API honor `#workspace/*` specifiers with zero extra configuration.
7+
8+
Run `npm run test -- --project=chromium hash-imports.spec.ts` from this directory to rebuild the preview bundle and execute only the hash-imports checks. The default `npm test` target still runs the full matrix (chromium on CI plus the webpack + SSR builds).

packages/playwright/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"pretest": "npm run build"
1616
},
1717
"dependencies": {
18-
"@knighted/css": "1.0.0-rc.14",
18+
"@knighted/css": "1.0.0-rc.15",
1919
"@knighted/jsx": "^1.4.1",
2020
"lit": "^3.2.1",
2121
"react": "^19.0.0",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@hash-imports/demo",
3+
"private": true,
4+
"type": "module",
5+
"imports": {
6+
"#workspace/ui/*": "./src/workspace-bridge/*"
7+
}
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const HASH_IMPORTS_SECTION_ID = 'hash-imports-workspace'
2+
export const HASH_IMPORTS_CARD_TEST_ID = 'hash-imports-card'
3+
export const HASH_IMPORTS_BADGE_TEST_ID = 'hash-imports-badge'

0 commit comments

Comments
 (0)