@knighted/css 1.1.0-rc.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -121,6 +121,22 @@ When the `.knighted-css` import targets a JavaScript/TypeScript module, the gene
121
121
  import Button, { knightedCss, stableSelectors } from './button.knighted-css.js'
122
122
  ```
123
123
 
124
+ Need hashed class names instead of stable selectors? Run the CLI with `--hashed` to emit proxy modules that export `selectors` backed by `knightedCssModules` from the loader-bridge:
125
+
126
+ ```sh
127
+ knighted-css-generate-types --root . --include src --hashed
128
+ ```
129
+
130
+ ```ts
131
+ import Button, { knightedCss, selectors } from './button.knighted-css.js'
132
+
133
+ selectors.card // hashed CSS Modules class name
134
+ ```
135
+
136
+ > [!IMPORTANT]
137
+ > `--hashed` requires wiring `@knighted/css/loader-bridge` to handle `?knighted-css` queries so
138
+ > the generated proxies can read `knightedCss` and `knightedCssModules` at build time.
139
+
124
140
  Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.
125
141
 
126
142
  ### Combined + runtime selectors
@@ -87,6 +87,7 @@ async function generateTypes(options = {}) {
87
87
  cacheDir,
88
88
  stableNamespace: options.stableNamespace,
89
89
  autoStable: options.autoStable,
90
+ hashed: options.hashed,
90
91
  tsconfig,
91
92
  resolver: options.resolver,
92
93
  };
@@ -142,12 +143,14 @@ async function generateDeclarations(options) {
142
143
  : undefined,
143
144
  resolver: options.resolver,
144
145
  });
145
- selectorMap = (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
146
- css,
147
- namespace: resolvedNamespace,
148
- resourcePath: resolvedPath,
149
- emitWarning: message => warnings.push(message),
150
- }).selectorMap;
146
+ selectorMap = options.hashed
147
+ ? collectSelectorTokensFromCss(css)
148
+ : (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
149
+ css,
150
+ namespace: resolvedNamespace,
151
+ resourcePath: resolvedPath,
152
+ emitWarning: message => warnings.push(message),
153
+ }).selectorMap;
151
154
  }
152
155
  catch (error) {
153
156
  warnings.push(`Failed to extract CSS for ${relativeToRoot(resolvedPath, options.rootDir)}: ${formatErrorMessage(error)}`);
@@ -164,7 +167,7 @@ async function generateDeclarations(options) {
164
167
  continue;
165
168
  }
166
169
  const proxyInfo = await resolveProxyInfo(manifestKey, selectorSource, resolvedPath, proxyInfoCache);
167
- const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, proxyInfo ?? undefined);
170
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, selectorSource, proxyInfo ?? undefined, options.hashed ?? false);
168
171
  if (moduleWrite) {
169
172
  selectorModuleWrites += 1;
170
173
  }
@@ -356,29 +359,65 @@ function buildSelectorModulePath(resolvedPath) {
356
359
  const base = ext ? resolvedPath.slice(0, -ext.length) : resolvedPath;
357
360
  return `${base}${SELECTOR_MODULE_SUFFIX}`;
358
361
  }
359
- function formatSelectorModuleSource(selectors, proxyInfo) {
362
+ function formatSelectorModuleSource(selectors, proxyInfo, options = {}) {
360
363
  const header = '// Generated by @knighted/css/generate-types\n// Do not edit.';
361
364
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
365
+ const isHashed = options.hashed === true;
362
366
  const lines = entries.map(([token, selector]) => ` ${JSON.stringify(token)}: ${JSON.stringify(selector)},`);
363
367
  const literal = lines.length > 0
364
368
  ? `{
365
369
  ${lines.join('\n')}
366
370
  } as const`
367
371
  : '{} as const';
372
+ const typeLines = entries.map(([token]) => ` readonly ${JSON.stringify(token)}: string`);
373
+ const typeLiteral = typeLines.length > 0
374
+ ? `{
375
+ ${typeLines.join('\n')}
376
+ }`
377
+ : 'Record<string, string>';
368
378
  const proxyLines = [];
379
+ const reexportLines = [];
380
+ const hashedSpecifier = options.selectorSource && options.resolvedPath
381
+ ? buildProxyModuleSpecifier(options.resolvedPath, options.selectorSource)
382
+ : undefined;
369
383
  if (proxyInfo) {
370
- proxyLines.push(`export * from '${proxyInfo.moduleSpecifier}'`);
384
+ reexportLines.push(`export * from '${proxyInfo.moduleSpecifier}'`);
371
385
  if (proxyInfo.includeDefault) {
372
- proxyLines.push(`export { default } from '${proxyInfo.moduleSpecifier}'`);
386
+ reexportLines.push(`export { default } from '${proxyInfo.moduleSpecifier}'`);
387
+ }
388
+ }
389
+ if (isHashed) {
390
+ const sourceSpecifier = proxyInfo?.moduleSpecifier ?? hashedSpecifier;
391
+ if (sourceSpecifier) {
392
+ proxyLines.push(`import { knightedCss as __knightedCss, knightedCssModules as __knightedCssModules } from '${sourceSpecifier}?knighted-css'`);
393
+ proxyLines.push('export const knightedCss = __knightedCss');
394
+ proxyLines.push('export const knightedCssModules = __knightedCssModules');
373
395
  }
396
+ }
397
+ else if (proxyInfo) {
374
398
  proxyLines.push(`export { knightedCss } from '${proxyInfo.moduleSpecifier}?knighted-css'`);
375
399
  }
376
- const defaultExport = proxyInfo ? '' : '\nexport default stableSelectors';
377
- const stableBlock = `export const stableSelectors = ${literal}
400
+ const exportName = isHashed ? 'selectors' : 'stableSelectors';
401
+ const typeName = isHashed ? 'KnightedCssSelectors' : 'KnightedCssStableSelectors';
402
+ const tokenTypeName = isHashed
403
+ ? 'KnightedCssSelectorToken'
404
+ : 'KnightedCssStableSelectorToken';
405
+ const defaultExport = proxyInfo ? '' : `\nexport default ${exportName}`;
406
+ const selectorBlock = isHashed
407
+ ? `export const ${exportName} = __knightedCssModules as ${typeLiteral}
408
+
409
+ export type ${typeName} = typeof ${exportName}
410
+ export type ${tokenTypeName} = keyof typeof ${exportName}${defaultExport}`
411
+ : `export const ${exportName} = ${literal}
378
412
 
379
- export type KnightedCssStableSelectors = typeof stableSelectors
380
- export type KnightedCssStableSelectorToken = keyof typeof stableSelectors${defaultExport}`;
381
- const sections = [header, proxyLines.join('\n'), stableBlock].filter(Boolean);
413
+ export type ${typeName} = typeof ${exportName}
414
+ export type ${tokenTypeName} = keyof typeof ${exportName}${defaultExport}`;
415
+ const sections = [
416
+ header,
417
+ proxyLines.join('\n'),
418
+ reexportLines.join('\n'),
419
+ selectorBlock,
420
+ ].filter(Boolean);
382
421
  return `${sections.join('\n\n')}
383
422
  `;
384
423
  }
@@ -424,10 +463,14 @@ function isWithinRoot(filePath, rootDir) {
424
463
  const relative = node_path_1.default.relative(rootDir, filePath);
425
464
  return relative === '' || (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative));
426
465
  }
427
- async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest, proxyInfo) {
466
+ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest, selectorSource, proxyInfo, hashed) {
428
467
  const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
429
468
  const targetPath = buildSelectorModulePath(resolvedPath);
430
- const source = formatSelectorModuleSource(selectors, proxyInfo);
469
+ const source = formatSelectorModuleSource(selectors, proxyInfo, {
470
+ hashed,
471
+ selectorSource,
472
+ resolvedPath,
473
+ });
431
474
  const hash = hashContent(source);
432
475
  const previousEntry = previousManifest[manifestKey];
433
476
  const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
@@ -578,6 +621,22 @@ function isStyleResource(filePath) {
578
621
  const normalized = filePath.toLowerCase();
579
622
  return STYLE_EXTENSIONS.some(ext => normalized.endsWith(ext));
580
623
  }
624
+ function collectSelectorTokensFromCss(css) {
625
+ const tokens = new Set();
626
+ const pattern = /\.([A-Za-z_-][A-Za-z0-9_-]*)\b/g;
627
+ let match;
628
+ while ((match = pattern.exec(css)) !== null) {
629
+ const token = match[1];
630
+ if (token) {
631
+ tokens.add(token);
632
+ }
633
+ }
634
+ const map = new Map();
635
+ for (const token of tokens) {
636
+ map.set(token, token);
637
+ }
638
+ return map;
639
+ }
581
640
  async function resolveProxyInfo(manifestKey, selectorSource, resolvedPath, cache) {
582
641
  if (isStyleResource(resolvedPath)) {
583
642
  return null;
@@ -681,6 +740,7 @@ async function runGenerateTypesCli(argv = process.argv.slice(2)) {
681
740
  outDir: parsed.outDir,
682
741
  stableNamespace: parsed.stableNamespace,
683
742
  autoStable: parsed.autoStable,
743
+ hashed: parsed.hashed,
684
744
  resolver,
685
745
  });
686
746
  reportCliResult(result);
@@ -697,6 +757,7 @@ function parseCliArgs(argv) {
697
757
  let outDir;
698
758
  let stableNamespace;
699
759
  let autoStable = false;
760
+ let hashed = false;
700
761
  let resolver;
701
762
  for (let i = 0; i < argv.length; i += 1) {
702
763
  const arg = argv[i];
@@ -707,6 +768,10 @@ function parseCliArgs(argv) {
707
768
  autoStable = true;
708
769
  continue;
709
770
  }
771
+ if (arg === '--hashed') {
772
+ hashed = true;
773
+ continue;
774
+ }
710
775
  if (arg === '--root' || arg === '-r') {
711
776
  const value = argv[++i];
712
777
  if (!value) {
@@ -752,7 +817,10 @@ function parseCliArgs(argv) {
752
817
  }
753
818
  include.push(arg);
754
819
  }
755
- return { rootDir, include, outDir, stableNamespace, autoStable, resolver };
820
+ if (autoStable && hashed) {
821
+ throw new Error('Cannot combine --auto-stable with --hashed');
822
+ }
823
+ return { rootDir, include, outDir, stableNamespace, autoStable, hashed, resolver };
756
824
  }
757
825
  function printHelp() {
758
826
  console.log(`Usage: knighted-css-generate-types [options]
@@ -763,6 +831,7 @@ Options:
763
831
  --out-dir <path> Directory to store selector module manifest cache
764
832
  --stable-namespace <name> Stable namespace prefix for generated selector maps
765
833
  --auto-stable Enable autoStable when extracting CSS for selectors
834
+ --hashed Emit selectors backed by loader-bridge hashed modules
766
835
  --resolver <path> Path or package name exporting a CssResolver
767
836
  -h, --help Show this help message
768
837
  `);