@@ -48,6 +48,7 @@ interface GenerateTypesInternalOptions {
4848 cacheDir : string
4949 stableNamespace ?: string
5050 autoStable ?: boolean
51+ hashed ?: boolean
5152 tsconfig ?: TsconfigResolutionContext
5253 resolver ?: CssResolver
5354}
@@ -65,6 +66,7 @@ export interface GenerateTypesOptions {
6566 outDir ?: string
6667 stableNamespace ?: string
6768 autoStable ?: boolean
69+ hashed ?: boolean
6870 resolver ?: CssResolver
6971}
7072
@@ -148,6 +150,7 @@ export async function generateTypes(
148150 cacheDir,
149151 stableNamespace : options . stableNamespace ,
150152 autoStable : options . autoStable ,
153+ hashed : options . hashed ,
151154 tsconfig,
152155 resolver : options . resolver ,
153156 }
@@ -224,12 +227,14 @@ async function generateDeclarations(
224227 : undefined ,
225228 resolver : options . resolver ,
226229 } )
227- selectorMap = buildStableSelectorsLiteral ( {
228- css,
229- namespace : resolvedNamespace ,
230- resourcePath : resolvedPath ,
231- emitWarning : message => warnings . push ( message ) ,
232- } ) . selectorMap
230+ selectorMap = options . hashed
231+ ? collectSelectorTokensFromCss ( css )
232+ : buildStableSelectorsLiteral ( {
233+ css,
234+ namespace : resolvedNamespace ,
235+ resourcePath : resolvedPath ,
236+ emitWarning : message => warnings . push ( message ) ,
237+ } ) . selectorMap
233238 } catch ( error ) {
234239 warnings . push (
235240 `Failed to extract CSS for ${ relativeToRoot ( resolvedPath , options . rootDir ) } : ${ formatErrorMessage ( error ) } ` ,
@@ -261,7 +266,9 @@ async function generateDeclarations(
261266 selectorMap ,
262267 previousSelectorManifest ,
263268 nextSelectorManifest ,
269+ selectorSource ,
264270 proxyInfo ?? undefined ,
271+ options . hashed ?? false ,
265272 )
266273 if ( moduleWrite ) {
267274 selectorModuleWrites += 1
@@ -501,9 +508,15 @@ function buildSelectorModulePath(resolvedPath: string): string {
501508function formatSelectorModuleSource (
502509 selectors : Map < string , string > ,
503510 proxyInfo ?: SelectorModuleProxyInfo ,
511+ options : {
512+ hashed ?: boolean
513+ selectorSource ?: string
514+ resolvedPath ?: string
515+ } = { } ,
504516) : string {
505517 const header = '// Generated by @knighted/css/generate-types\n// Do not edit.'
506518 const entries = Array . from ( selectors . entries ( ) ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
519+ const isHashed = options . hashed === true
507520 const lines = entries . map (
508521 ( [ token , selector ] ) => ` ${ JSON . stringify ( token ) } : ${ JSON . stringify ( selector ) } ,` ,
509522 )
@@ -513,22 +526,67 @@ function formatSelectorModuleSource(
513526${ lines . join ( '\n' ) }
514527} as const`
515528 : '{} as const'
529+ const typeLines = entries . map (
530+ ( [ token ] ) => ` readonly ${ JSON . stringify ( token ) } : string` ,
531+ )
532+ const typeLiteral =
533+ typeLines . length > 0
534+ ? `{
535+ ${ typeLines . join ( '\n' ) }
536+ }`
537+ : 'Record<string, string>'
516538 const proxyLines : string [ ] = [ ]
539+ const reexportLines : string [ ] = [ ]
540+ const hashedSpecifier =
541+ options . selectorSource && options . resolvedPath
542+ ? buildProxyModuleSpecifier ( options . resolvedPath , options . selectorSource )
543+ : undefined
544+
517545 if ( proxyInfo ) {
518- proxyLines . push ( `export * from '${ proxyInfo . moduleSpecifier } '` )
546+ reexportLines . push ( `export * from '${ proxyInfo . moduleSpecifier } '` )
519547 if ( proxyInfo . includeDefault ) {
520- proxyLines . push ( `export { default } from '${ proxyInfo . moduleSpecifier } '` )
548+ reexportLines . push ( `export { default } from '${ proxyInfo . moduleSpecifier } '` )
549+ }
550+ }
551+
552+ if ( isHashed ) {
553+ const sourceSpecifier = proxyInfo ?. moduleSpecifier ?? hashedSpecifier
554+ if ( sourceSpecifier ) {
555+ proxyLines . push (
556+ `import { knightedCss as __knightedCss, knightedCssModules as __knightedCssModules } from '${ sourceSpecifier } ?knighted-css'` ,
557+ )
558+ proxyLines . push ( 'export const knightedCss = __knightedCss' )
559+ proxyLines . push ( 'export const knightedCssModules = __knightedCssModules' )
521560 }
561+ } else if ( proxyInfo ) {
522562 proxyLines . push (
523563 `export { knightedCss } from '${ proxyInfo . moduleSpecifier } ?knighted-css'` ,
524564 )
525565 }
526- const defaultExport = proxyInfo ? '' : '\nexport default stableSelectors'
527- const stableBlock = `export const stableSelectors = ${ literal }
528566
529- export type KnightedCssStableSelectors = typeof stableSelectors
530- export type KnightedCssStableSelectorToken = keyof typeof stableSelectors${ defaultExport } `
531- const sections = [ header , proxyLines . join ( '\n' ) , stableBlock ] . filter ( Boolean )
567+ const exportName = isHashed ? 'selectors' : 'stableSelectors'
568+ const typeName = isHashed ? 'KnightedCssSelectors' : 'KnightedCssStableSelectors'
569+ const tokenTypeName = isHashed
570+ ? 'KnightedCssSelectorToken'
571+ : 'KnightedCssStableSelectorToken'
572+ const defaultExport = proxyInfo ? '' : `\nexport default ${ exportName } `
573+
574+ const selectorBlock = isHashed
575+ ? `export const ${ exportName } = __knightedCssModules as ${ typeLiteral }
576+
577+ export type ${ typeName } = typeof ${ exportName }
578+ export type ${ tokenTypeName } = keyof typeof ${ exportName } ${ defaultExport } `
579+ : `export const ${ exportName } = ${ literal }
580+
581+ export type ${ typeName } = typeof ${ exportName }
582+ export type ${ tokenTypeName } = keyof typeof ${ exportName } ${ defaultExport } `
583+
584+ const sections = [
585+ header ,
586+ proxyLines . join ( '\n' ) ,
587+ reexportLines . join ( '\n' ) ,
588+ selectorBlock ,
589+ ] . filter ( Boolean )
532590 return `${ sections . join ( '\n\n' ) }
533591`
534592}
@@ -591,11 +649,17 @@ async function ensureSelectorModule(
591649 selectors : Map < string , string > ,
592650 previousManifest : SelectorModuleManifest ,
593651 nextManifest : SelectorModuleManifest ,
652+ selectorSource : string ,
594653 proxyInfo ?: SelectorModuleProxyInfo ,
654+ hashed ?: boolean ,
595655) : Promise < boolean > {
596656 const manifestKey = buildSelectorModuleManifestKey ( resolvedPath )
597657 const targetPath = buildSelectorModulePath ( resolvedPath )
598- const source = formatSelectorModuleSource ( selectors , proxyInfo )
658+ const source = formatSelectorModuleSource ( selectors , proxyInfo , {
659+ hashed,
660+ selectorSource,
661+ resolvedPath,
662+ } )
599663 const hash = hashContent ( source )
600664 const previousEntry = previousManifest [ manifestKey ]
601665 const needsWrite = previousEntry ?. hash !== hash || ! ( await fileExists ( targetPath ) )
@@ -772,6 +836,23 @@ function isStyleResource(filePath: string): boolean {
772836 return STYLE_EXTENSIONS . some ( ext => normalized . endsWith ( ext ) )
773837}
774838
839+ function collectSelectorTokensFromCss ( css : string ) : Map < string , string > {
840+ const tokens = new Set < string > ( )
841+ const pattern = / \. ( [ A - Z a - z _ - ] [ A - Z a - z 0 - 9 _ - ] * ) \b / g
842+ let match : RegExpExecArray | null
843+ while ( ( match = pattern . exec ( css ) ) !== null ) {
844+ const token = match [ 1 ]
845+ if ( token ) {
846+ tokens . add ( token )
847+ }
848+ }
849+ const map = new Map < string , string > ( )
850+ for ( const token of tokens ) {
851+ map . set ( token , token )
852+ }
853+ return map
854+ }
855+
775856async function resolveProxyInfo (
776857 manifestKey : string ,
777858 selectorSource : string ,
@@ -890,6 +971,7 @@ export async function runGenerateTypesCli(argv = process.argv.slice(2)): Promise
890971 outDir : parsed . outDir ,
891972 stableNamespace : parsed . stableNamespace ,
892973 autoStable : parsed . autoStable ,
974+ hashed : parsed . hashed ,
893975 resolver,
894976 } )
895977 reportCliResult ( result )
@@ -906,6 +988,7 @@ export interface ParsedCliArgs {
906988 outDir ?: string
907989 stableNamespace ?: string
908990 autoStable ?: boolean
991+ hashed ?: boolean
909992 resolver ?: string
910993 help ?: boolean
911994}
@@ -916,6 +999,7 @@ function parseCliArgs(argv: string[]): ParsedCliArgs {
916999 let outDir : string | undefined
9171000 let stableNamespace : string | undefined
9181001 let autoStable = false
1002+ let hashed = false
9191003 let resolver : string | undefined
9201004
9211005 for ( let i = 0 ; i < argv . length ; i += 1 ) {
@@ -927,6 +1011,10 @@ function parseCliArgs(argv: string[]): ParsedCliArgs {
9271011 autoStable = true
9281012 continue
9291013 }
1014+ if ( arg === '--hashed' ) {
1015+ hashed = true
1016+ continue
1017+ }
9301018 if ( arg === '--root' || arg === '-r' ) {
9311019 const value = argv [ ++ i ]
9321020 if ( ! value ) {
@@ -973,7 +1061,11 @@ function parseCliArgs(argv: string[]): ParsedCliArgs {
9731061 include . push ( arg )
9741062 }
9751063
976- return { rootDir, include, outDir, stableNamespace, autoStable, resolver }
1064+ if ( autoStable && hashed ) {
1065+ throw new Error ( 'Cannot combine --auto-stable with --hashed' )
1066+ }
1067+
1068+ return { rootDir, include, outDir, stableNamespace, autoStable, hashed, resolver }
9771069}
9781070
9791071function printHelp ( ) : void {
@@ -985,6 +1077,7 @@ Options:
9851077 --out-dir <path> Directory to store selector module manifest cache
9861078 --stable-namespace <name> Stable namespace prefix for generated selector maps
9871079 --auto-stable Enable autoStable when extracting CSS for selectors
1080+ --hashed Emit selectors backed by loader-bridge hashed modules
9881081 --resolver <path> Path or package name exporting a CssResolver
9891082 -h, --help Show this help message
9901083` )
0 commit comments