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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ CSS Modules hash class names after the loader extracts selectors, so the stylesh

Run `npx knighted-css-generate-types --root .` to scan your project for `?knighted-css&types` imports. The CLI:

- extracts selectors via the loader, then writes literal module declarations into `node_modules/@knighted/css/node_modules/.knighted-css`
- extracts selectors via the loader, then writes literal module declarations whose specifiers resolve to the real stylesheet paths (TypeScript picks up `import './foo.scss?knighted-css&types'` directly—no registries or casting helpers required). The default output still lands in `node_modules/@knighted/css/node_modules/.knighted-css`, but every module declaration points back to your source tree.
- updates the packaged stub at `node_modules/@knighted/css/types-stub/index.d.ts`
- exposes the declarations automatically because `types.d.ts` references the stub, so no `tsconfig` wiring is required

Expand Down Expand Up @@ -316,7 +316,7 @@ or wire it into `package.json` for local workflows:
}
```

The CLI scans every file you include (by default the project root, skipping `node_modules`, `dist`, etc.), finds imports containing `?knighted-css&types`, reuses the loader to extract CSS, and writes deterministic `.d.ts` files into `node_modules/.knighted-css/knt-*.d.ts`. It also maintains `node_modules/@knighted/css/types-stub/index.d.ts`, so TypeScript picks up the generated declarations automatically—no extra `typeRoots` configuration is required.
The CLI scans every file you include (by default the project root, skipping `node_modules`, `dist`, etc.), finds imports containing `?knighted-css&types`, reuses the loader to extract CSS, and writes deterministic `.d.ts` files into `node_modules/.knighted-css/knt-*.d.ts`. Each declaration uses a specifier that resolves back to the original stylesheet path, so TypeScript sees the literal `import './foo.scss?knighted-css&types'` as soon as the CLI runs. It also maintains `node_modules/@knighted/css/types-stub/index.d.ts`, so TypeScript picks up the generated declarations automatically—no extra `typeRoots` or registry scripts are required.

Key flags:

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.

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.0.0-rc.10",
"version": "1.0.0-rc.11",
"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
83 changes: 74 additions & 9 deletions packages/css/src/generateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
determineSelectorVariant,
hasQueryFlag,
TYPES_QUERY_FLAG,
buildSanitizedQuery,
COMBINED_QUERY_FLAG,
NAMED_ONLY_QUERY_FLAGS,
type SelectorTypeVariant,
} from './loaderInternals.js'
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js'
Expand Down Expand Up @@ -174,9 +177,6 @@ async function generateDeclarations(
if (!query || !hasQueryFlag(query, TYPES_QUERY_FLAG)) {
continue
}
if (processedSpecifiers.has(cleaned)) {
continue
}
const resolvedNamespace = resolveStableNamespace(options.stableNamespace)
const resolvedPath = await resolveImportPath(
resource,
Expand Down Expand Up @@ -214,23 +214,35 @@ async function generateDeclarations(
selectorCache.set(cacheKey, selectorMap)
}

const canonicalSpecifier = buildDeclarationModuleSpecifier(
resolvedPath,
options.outDir,
query,
)
if (processedSpecifiers.has(canonicalSpecifier)) {
continue
}
const variant = determineSelectorVariant(query)
const declaration = formatModuleDeclaration(cleaned, variant, selectorMap)
const declaration = formatModuleDeclaration(
canonicalSpecifier,
variant,
selectorMap,
)
const declarationHash = hashContent(declaration)
const fileName = buildDeclarationFileName(cleaned)
const fileName = buildDeclarationFileName(canonicalSpecifier)
const targetPath = path.join(options.outDir, fileName)
const previousEntry = previousManifest[cleaned]
const previousEntry = previousManifest[canonicalSpecifier]
const needsWrite =
previousEntry?.hash !== declarationHash || !(await fileExists(targetPath))
if (needsWrite) {
await fs.writeFile(targetPath, declaration, 'utf8')
writes += 1
}
nextManifest[cleaned] = { file: fileName, hash: declarationHash }
nextManifest[canonicalSpecifier] = { file: fileName, hash: declarationHash }
if (needsWrite) {
declarations.push({ specifier: cleaned, filePath: targetPath })
declarations.push({ specifier: canonicalSpecifier, filePath: targetPath })
}
processedSpecifiers.add(cleaned)
processedSpecifiers.add(canonicalSpecifier)
}
}

Expand Down Expand Up @@ -436,6 +448,57 @@ ${lines.join('\n')}
}>`
}

function buildDeclarationModuleSpecifier(
resolvedPath: string,
declarationDir: string,
query: string,
): string {
const relativePath = path.relative(declarationDir, resolvedPath)
const normalizedPath = normalizeRelativePath(relativePath)
const canonicalQuery = buildCanonicalQuery(query)
return `${normalizedPath}${canonicalQuery}`
}

function normalizeRelativePath(relativePath: string): string {
let normalized = relativePath.split(path.sep).join('/')
if (!normalized || normalized === '') {
normalized = '.'
}
if (normalized === '.') {
return './'
}
if (normalized.startsWith('./') || normalized.startsWith('../')) {
return normalized
}
if (normalized.startsWith('.')) {
return normalized
}
return `./${normalized}`
}

function buildCanonicalQuery(query: string): string {
if (!query) {
return ''
}
const sanitized = buildSanitizedQuery(query)
const extraParts = sanitized ? sanitized.slice(1).split('&').filter(Boolean) : []
const parts: string[] = []
parts.push('knighted-css')
if (hasQueryFlag(query, COMBINED_QUERY_FLAG)) {
parts.push(COMBINED_QUERY_FLAG)
}
for (const flag of NAMED_ONLY_QUERY_FLAGS) {
if (hasQueryFlag(query, flag)) {
parts.push(flag)
}
}
if (hasQueryFlag(query, TYPES_QUERY_FLAG)) {
parts.push(TYPES_QUERY_FLAG)
}
const merged = [...parts, ...extraParts]
return merged.length > 0 ? `?${merged.join('&')}` : ''
}

function hashContent(content: string): string {
return crypto.createHash('sha1').update(content).digest('hex')
}
Expand Down Expand Up @@ -778,6 +841,8 @@ export const __generateTypesInternals = {
buildDeclarationFileName,
formatModuleDeclaration,
formatSelectorType,
buildDeclarationModuleSpecifier,
buildCanonicalQuery,
relativeToRoot,
collectCandidateFiles,
normalizeIncludeOptions,
Expand Down
16 changes: 15 additions & 1 deletion packages/css/test/generateTypes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ test('generateTypes resolves tsconfig baseUrl specifiers', async () => {
string,
unknown
>
assert.ok(manifest['styles/demo.css?knighted-css&types'])
assert.ok(manifest['../src/styles/demo.css?knighted-css&types'])
} finally {
await project.cleanup()
}
Expand Down Expand Up @@ -354,6 +354,8 @@ test('generateTypes internals format selector-aware declarations', async () => {
buildDeclarationFileName,
formatSelectorType,
formatModuleDeclaration,
buildDeclarationModuleSpecifier,
buildCanonicalQuery,
writeTypesIndex,
normalizeIncludeOptions,
collectCandidateFiles,
Expand Down Expand Up @@ -411,6 +413,18 @@ test('generateTypes internals format selector-aware declarations', async () => {
)
assert.doesNotMatch(withoutDefault, /export default/)

const canonicalSpecifier = buildDeclarationModuleSpecifier(
path.join('/tmp/project', 'src', 'styles', 'demo.css'),
path.join('/tmp/project', '.knighted-css'),
'?types&knighted-css&foo=1',
)
assert.equal(canonicalSpecifier, '../src/styles/demo.css?knighted-css&types&foo=1')

assert.equal(
buildCanonicalQuery('?knighted-css&combined&no-default&types&foo=1'),
'?knighted-css&combined&no-default&types&foo=1',
)

const normalized = normalizeIncludeOptions(undefined, '/tmp/demo')
assert.deepEqual(normalized, ['/tmp/demo'])
assert.deepEqual(normalizeIncludeOptions(['./src'], '/tmp/demo'), [
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"pretest": "npm run build"
},
"dependencies": {
"@knighted/css": "1.0.0-rc.10",
"@knighted/css": "1.0.0-rc.11",
"@knighted/jsx": "^1.4.1",
"lit": "^3.2.1",
"react": "^19.0.0",
Expand Down