@knighted/css 1.1.0 → 1.2.0-rc.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
@@ -30,7 +30,7 @@ I needed a single source of truth for UI components that could drop into both li
30
30
  - Deterministic selector duplication via `autoStable`: duplicate matching class selectors with a stable namespace (default `knighted-`) in both plain CSS and CSS Modules exports.
31
31
  - Pluggable resolver/filter hooks for custom module resolution (e.g., Rspack/Vite/webpack aliases) or selective inclusion.
32
32
  - First-class loader (`@knighted/css/loader`) so bundlers can import compiled CSS alongside their modules via `?knighted-css`.
33
- - Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests so TypeScript gets literal tokens in lockstep with the loader exports.
33
+ - Built-in type generation CLI (`knighted-css-generate-types`) that emits `.knighted-css.*` selector manifests (module mode) or declaration augmentations (declaration mode) so TypeScript stays in lockstep with loader exports.
34
34
 
35
35
  ## Requirements
36
36
 
@@ -107,7 +107,7 @@ See [docs/loader.md](../../docs/loader.md) for the full configuration, combined
107
107
 
108
108
  ### Type generation hook (`*.knighted-css*`)
109
109
 
110
- Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens:
110
+ Run `knighted-css-generate-types` so every specifier that ends with `.knighted-css` produces a sibling manifest containing literal selector tokens (module mode, the default):
111
111
 
112
112
  ```ts
113
113
  import stableSelectors from './button.module.scss.knighted-css.js'
@@ -137,6 +137,58 @@ selectors.card // hashed CSS Modules class name
137
137
  > `--hashed` requires wiring `@knighted/css/loader-bridge` to handle `?knighted-css` queries so
138
138
  > the generated proxies can read `knightedCss` and `knightedCssModules` at build time.
139
139
 
140
+ > [!NOTE]
141
+ > `--hashed` builds the selector list from compiled CSS. The generated sidecar can therefore
142
+ > include class names that are not exported by the module (e.g. sprinkles output), while the
143
+ > runtime `selectors` map only includes exported locals from the loader bridge.
144
+
145
+ Prefer module-level imports without the double extension? Use declaration mode to emit `.d.ts` augmentations next to JS/TS modules that import styles:
146
+
147
+ ```sh
148
+ knighted-css-generate-types --root . --include src --mode declaration
149
+ ```
150
+
151
+ ```ts
152
+ import Button, { knightedCss, stableSelectors } from './button.js'
153
+ ```
154
+
155
+ See [docs/type-generation.md](../../docs/type-generation.md#mode-quick-reference) for a quick comparison of module vs declaration mode tradeoffs.
156
+
157
+ > [!IMPORTANT]
158
+ > Declaration mode requires a resolver plugin to append `?knighted-css` (and `&combined` when applicable)
159
+ > at build time so runtime exports match the generated types.
160
+
161
+ Install the resolver plugin via `@knighted/css/plugin` and wire it into your bundler resolver:
162
+
163
+ ```js
164
+ // rspack.config.js
165
+ import { knightedCssResolverPlugin } from '@knighted/css/plugin'
166
+
167
+ export default {
168
+ resolve: {
169
+ plugins: [knightedCssResolverPlugin()],
170
+ },
171
+ }
172
+ ```
173
+
174
+ If you want the resolver to only match sidecars generated by the CLI, enable strict mode and provide a manifest (written by `knighted-css-generate-types --manifest`):
175
+
176
+ ```js
177
+ import path from 'node:path'
178
+ import { knightedCssResolverPlugin } from '@knighted/css/plugin'
179
+
180
+ export default {
181
+ resolve: {
182
+ plugins: [
183
+ knightedCssResolverPlugin({
184
+ strictSidecar: true,
185
+ manifestPath: path.resolve('.knighted-css/knighted-manifest.json'),
186
+ }),
187
+ ],
188
+ },
189
+ }
190
+ ```
191
+
140
192
  Refer to [docs/type-generation.md](../../docs/type-generation.md) for CLI options and workflow tips.
141
193
 
142
194
  ### Combined + runtime selectors
@@ -66,6 +66,7 @@ function getImportMetaUrl() {
66
66
  }
67
67
  const SELECTOR_REFERENCE = '.knighted-css';
68
68
  const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts';
69
+ const DECLARATION_SUFFIX = '.d.ts';
69
70
  const STYLE_EXTENSIONS = css_js_1.DEFAULT_EXTENSIONS.map(ext => ext.toLowerCase());
70
71
  const SCRIPT_EXTENSIONS = Array.from(SUPPORTED_EXTENSIONS);
71
72
  const RESOLUTION_EXTENSIONS = Array.from(new Set([...SCRIPT_EXTENSIONS, ...STYLE_EXTENSIONS]));
@@ -75,11 +76,16 @@ const EXTENSION_FALLBACKS = {
75
76
  '.cjs': ['.cts', '.cjs', '.js', '.ts', '.tsx'],
76
77
  '.jsx': ['.tsx', '.jsx'],
77
78
  };
79
+ const DECLARATION_MODE_WARNING = 'Declaration mode requires a resolver plugin to append ?knighted-css (and &combined when applicable) so runtime exports match the generated types.';
78
80
  async function generateTypes(options = {}) {
79
81
  const rootDir = await resolveRootDir(node_path_1.default.resolve(options.rootDir ?? process.cwd()));
80
82
  const include = normalizeIncludeOptions(options.include, rootDir);
81
83
  const cacheDir = node_path_1.default.resolve(options.outDir ?? node_path_1.default.join(rootDir, '.knighted-css'));
82
84
  const tsconfig = loadTsconfigResolutionContext(rootDir);
85
+ const mode = options.mode ?? 'module';
86
+ const manifestPath = options.manifestPath
87
+ ? node_path_1.default.resolve(rootDir, options.manifestPath)
88
+ : undefined;
83
89
  await promises_1.default.mkdir(cacheDir, { recursive: true });
84
90
  const internalOptions = {
85
91
  rootDir,
@@ -90,6 +96,8 @@ async function generateTypes(options = {}) {
90
96
  hashed: options.hashed,
91
97
  tsconfig,
92
98
  resolver: options.resolver,
99
+ mode,
100
+ manifestPath,
93
101
  };
94
102
  return generateDeclarations(internalOptions);
95
103
  }
@@ -108,79 +116,176 @@ async function generateDeclarations(options) {
108
116
  const selectorModulesManifestPath = node_path_1.default.join(options.cacheDir, 'selector-modules.json');
109
117
  const previousSelectorManifest = await readManifest(selectorModulesManifestPath);
110
118
  const nextSelectorManifest = {};
119
+ const sidecarManifest = {};
111
120
  const selectorCache = new Map();
112
121
  const processedSelectors = new Set();
113
122
  const proxyInfoCache = new Map();
114
123
  const warnings = [];
115
124
  let selectorModuleWrites = 0;
116
- for (const filePath of files) {
117
- const matches = await findSpecifierImports(filePath);
118
- for (const match of matches) {
119
- const cleaned = match.specifier.trim();
120
- const inlineFree = stripInlineLoader(cleaned);
121
- const { resource } = splitResourceAndQuery(inlineFree);
122
- const selectorSource = extractSelectorSourceSpecifier(resource);
123
- if (!selectorSource) {
125
+ if (options.mode === 'declaration') {
126
+ warnings.push(DECLARATION_MODE_WARNING);
127
+ }
128
+ if (options.mode === 'declaration') {
129
+ for (const filePath of files) {
130
+ if (!isScriptResource(filePath)) {
124
131
  continue;
125
132
  }
126
- const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
127
- const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig, options.resolver, resolverFactory, RESOLUTION_EXTENSIONS);
128
- if (!resolvedPath) {
129
- warnings.push(`Unable to resolve ${selectorSource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
133
+ if (!isWithinRoot(filePath, options.rootDir)) {
134
+ warnings.push(`Skipping declaration output for ${relativeToRoot(filePath, options.rootDir)} because it is outside the project root.`);
135
+ continue;
136
+ }
137
+ const manifestKey = buildSelectorModuleManifestKey(filePath);
138
+ if (processedSelectors.has(manifestKey)) {
139
+ continue;
140
+ }
141
+ const hasStyles = await hasStyleImports(filePath, {
142
+ rootDir: options.rootDir,
143
+ tsconfig: options.tsconfig,
144
+ resolver: options.resolver,
145
+ resolverFactory,
146
+ });
147
+ if (!hasStyles) {
148
+ processedSelectors.add(manifestKey);
130
149
  continue;
131
150
  }
132
- const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
151
+ const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
152
+ const cacheKey = `${filePath}::${resolvedNamespace}::declaration`;
133
153
  let selectorMap = selectorCache.get(cacheKey);
134
154
  if (!selectorMap) {
135
155
  try {
136
- const shouldUseCssModules = resolvedPath.endsWith('.module.css');
137
- const { css } = await activeCssWithMeta(resolvedPath, {
156
+ let cssResult = await activeCssWithMeta(filePath, {
138
157
  cwd: options.rootDir,
139
158
  peerResolver,
140
159
  autoStable: options.autoStable ? { namespace: resolvedNamespace } : undefined,
141
- lightningcss: options.autoStable && shouldUseCssModules
142
- ? { cssModules: true }
143
- : undefined,
144
160
  resolver: options.resolver,
145
161
  });
162
+ if (cssResult.files.length === 0 || cssResult.css.trim().length === 0) {
163
+ processedSelectors.add(manifestKey);
164
+ continue;
165
+ }
166
+ if (options.autoStable &&
167
+ cssResult.files.some(file => isCssModuleResource(file))) {
168
+ cssResult = await activeCssWithMeta(filePath, {
169
+ cwd: options.rootDir,
170
+ peerResolver,
171
+ autoStable: options.autoStable
172
+ ? { namespace: resolvedNamespace }
173
+ : undefined,
174
+ lightningcss: { cssModules: true },
175
+ resolver: options.resolver,
176
+ });
177
+ }
146
178
  selectorMap = options.hashed
147
- ? collectSelectorTokensFromCss(css)
179
+ ? collectSelectorTokensFromCss(cssResult.css)
148
180
  : (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
149
- css,
181
+ css: cssResult.css,
150
182
  namespace: resolvedNamespace,
151
- resourcePath: resolvedPath,
183
+ resourcePath: filePath,
152
184
  emitWarning: message => warnings.push(message),
153
185
  }).selectorMap;
154
186
  }
155
187
  catch (error) {
156
- warnings.push(`Failed to extract CSS for ${relativeToRoot(resolvedPath, options.rootDir)}: ${formatErrorMessage(error)}`);
188
+ warnings.push(`Failed to extract CSS for ${relativeToRoot(filePath, options.rootDir)}: ${formatErrorMessage(error)}`);
189
+ processedSelectors.add(manifestKey);
157
190
  continue;
158
191
  }
159
192
  selectorCache.set(cacheKey, selectorMap);
160
193
  }
161
- if (!isWithinRoot(resolvedPath, options.rootDir)) {
162
- warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
194
+ if (!selectorMap || selectorMap.size === 0) {
195
+ processedSelectors.add(manifestKey);
163
196
  continue;
164
197
  }
165
- const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
166
- if (processedSelectors.has(manifestKey)) {
198
+ const proxyInfo = await resolveDeclarationProxyInfo(manifestKey, filePath, proxyInfoCache);
199
+ if (!proxyInfo) {
200
+ processedSelectors.add(manifestKey);
167
201
  continue;
168
202
  }
169
- const proxyInfo = await resolveProxyInfo(manifestKey, selectorSource, resolvedPath, proxyInfoCache);
170
- const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, selectorSource, proxyInfo ?? undefined, options.hashed ?? false);
203
+ const moduleWrite = await ensureDeclarationModule(filePath, selectorMap, previousSelectorManifest, nextSelectorManifest, proxyInfo, options.hashed ?? false);
204
+ if (options.manifestPath) {
205
+ sidecarManifest[manifestKey] = { file: buildDeclarationPath(filePath) };
206
+ }
171
207
  if (moduleWrite) {
172
208
  selectorModuleWrites += 1;
173
209
  }
174
210
  processedSelectors.add(manifestKey);
175
211
  }
176
212
  }
213
+ else {
214
+ for (const filePath of files) {
215
+ const matches = await findSpecifierImports(filePath);
216
+ for (const match of matches) {
217
+ const cleaned = match.specifier.trim();
218
+ const inlineFree = stripInlineLoader(cleaned);
219
+ const { resource } = splitResourceAndQuery(inlineFree);
220
+ const selectorSource = extractSelectorSourceSpecifier(resource);
221
+ if (!selectorSource) {
222
+ continue;
223
+ }
224
+ const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
225
+ const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig, options.resolver, resolverFactory, RESOLUTION_EXTENSIONS);
226
+ if (!resolvedPath) {
227
+ warnings.push(`Unable to resolve ${selectorSource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
228
+ continue;
229
+ }
230
+ const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
231
+ let selectorMap = selectorCache.get(cacheKey);
232
+ if (!selectorMap) {
233
+ try {
234
+ const shouldUseCssModules = resolvedPath.endsWith('.module.css');
235
+ const { css } = await activeCssWithMeta(resolvedPath, {
236
+ cwd: options.rootDir,
237
+ peerResolver,
238
+ autoStable: options.autoStable
239
+ ? { namespace: resolvedNamespace }
240
+ : undefined,
241
+ lightningcss: options.autoStable && shouldUseCssModules
242
+ ? { cssModules: true }
243
+ : undefined,
244
+ resolver: options.resolver,
245
+ });
246
+ selectorMap = options.hashed
247
+ ? collectSelectorTokensFromCss(css)
248
+ : (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
249
+ css,
250
+ namespace: resolvedNamespace,
251
+ resourcePath: resolvedPath,
252
+ emitWarning: message => warnings.push(message),
253
+ }).selectorMap;
254
+ }
255
+ catch (error) {
256
+ warnings.push(`Failed to extract CSS for ${relativeToRoot(resolvedPath, options.rootDir)}: ${formatErrorMessage(error)}`);
257
+ continue;
258
+ }
259
+ selectorCache.set(cacheKey, selectorMap);
260
+ }
261
+ if (!isWithinRoot(resolvedPath, options.rootDir)) {
262
+ warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
263
+ continue;
264
+ }
265
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
266
+ if (processedSelectors.has(manifestKey)) {
267
+ continue;
268
+ }
269
+ const proxyInfo = await resolveProxyInfo(manifestKey, selectorSource, resolvedPath, proxyInfoCache);
270
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, selectorSource, proxyInfo ?? undefined, options.hashed ?? false);
271
+ if (moduleWrite) {
272
+ selectorModuleWrites += 1;
273
+ }
274
+ processedSelectors.add(manifestKey);
275
+ }
276
+ }
277
+ }
177
278
  const selectorModulesRemoved = await removeStaleSelectorModules(previousSelectorManifest, nextSelectorManifest);
178
279
  await writeManifest(selectorModulesManifestPath, nextSelectorManifest);
280
+ if (options.manifestPath && options.mode === 'declaration') {
281
+ await writeSidecarManifest(options.manifestPath, sidecarManifest);
282
+ }
179
283
  return {
180
284
  selectorModulesWritten: selectorModuleWrites,
181
285
  selectorModulesRemoved,
182
286
  warnings,
183
287
  manifestPath: selectorModulesManifestPath,
288
+ sidecarManifestPath: options.mode === 'declaration' ? options.manifestPath : undefined,
184
289
  };
185
290
  }
186
291
  function normalizeIncludeOptions(include, rootDir) {
@@ -359,6 +464,50 @@ function buildSelectorModulePath(resolvedPath) {
359
464
  const base = ext ? resolvedPath.slice(0, -ext.length) : resolvedPath;
360
465
  return `${base}${SELECTOR_MODULE_SUFFIX}`;
361
466
  }
467
+ function buildDeclarationModuleSpecifier(resolvedPath) {
468
+ const ext = node_path_1.default.extname(resolvedPath).toLowerCase();
469
+ const baseName = node_path_1.default.basename(resolvedPath, ext);
470
+ const mappedExt = ext === '.mjs' || ext === '.mts'
471
+ ? '.mjs'
472
+ : ext === '.cjs' || ext === '.cts'
473
+ ? '.cjs'
474
+ : '.js';
475
+ return `./${baseName}${mappedExt}`;
476
+ }
477
+ function buildDeclarationPath(resolvedPath) {
478
+ if (resolvedPath.endsWith(DECLARATION_SUFFIX)) {
479
+ return resolvedPath;
480
+ }
481
+ return `${resolvedPath}${DECLARATION_SUFFIX}`;
482
+ }
483
+ function formatSelectorTypeLiteral(selectors) {
484
+ const entries = Array.from(selectors.keys()).sort((a, b) => a.localeCompare(b));
485
+ const typeLines = entries.map(token => ` readonly ${JSON.stringify(token)}: string`);
486
+ return typeLines.length > 0
487
+ ? `{
488
+ ${typeLines.join('\n')}
489
+ }`
490
+ : 'Record<string, string>';
491
+ }
492
+ function formatDeclarationSource(selectors, proxyInfo, options = {}) {
493
+ const header = '// Generated by @knighted/css/generate-types\n// Do not edit.';
494
+ const isHashed = options.hashed === true;
495
+ const marker = isHashed ? '// @knighted-css:hashed' : '// @knighted-css';
496
+ const exportName = isHashed ? 'selectors' : 'stableSelectors';
497
+ const typeLiteral = formatSelectorTypeLiteral(selectors);
498
+ const shouldEmit = (name) => !proxyInfo.exportedNames?.has(name);
499
+ const lines = [
500
+ header,
501
+ marker,
502
+ '',
503
+ `declare module '${proxyInfo.moduleSpecifier}' {`,
504
+ shouldEmit('knightedCss') ? ' export const knightedCss: string' : '',
505
+ shouldEmit(exportName) ? ` export const ${exportName}: ${typeLiteral}` : '',
506
+ '}',
507
+ 'export {}',
508
+ ].filter(Boolean);
509
+ return `${lines.join('\n')}\n`;
510
+ }
362
511
  function formatSelectorModuleSource(selectors, proxyInfo, options = {}) {
363
512
  const header = '// Generated by @knighted/css/generate-types\n// Do not edit.';
364
513
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
@@ -436,6 +585,10 @@ async function readManifest(manifestPath) {
436
585
  async function writeManifest(manifestPath, manifest) {
437
586
  await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
438
587
  }
588
+ async function writeSidecarManifest(manifestPath, manifest) {
589
+ await promises_1.default.mkdir(node_path_1.default.dirname(manifestPath), { recursive: true });
590
+ await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
591
+ }
439
592
  async function removeStaleSelectorModules(previous, next) {
440
593
  const stale = Object.entries(previous).filter(([key]) => !next[key]);
441
594
  let removed = 0;
@@ -480,6 +633,19 @@ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, n
480
633
  nextManifest[manifestKey] = { file: targetPath, hash };
481
634
  return needsWrite;
482
635
  }
636
+ async function ensureDeclarationModule(resolvedPath, selectors, previousManifest, nextManifest, proxyInfo, hashed) {
637
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
638
+ const targetPath = buildDeclarationPath(resolvedPath);
639
+ const source = formatDeclarationSource(selectors, proxyInfo, { hashed });
640
+ const hash = hashContent(source);
641
+ const previousEntry = previousManifest[manifestKey];
642
+ const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
643
+ if (needsWrite) {
644
+ await promises_1.default.writeFile(targetPath, source, 'utf8');
645
+ }
646
+ nextManifest[manifestKey] = { file: targetPath, hash };
647
+ return needsWrite;
648
+ }
483
649
  async function fileExists(target) {
484
650
  try {
485
651
  await promises_1.default.access(target);
@@ -621,6 +787,60 @@ function isStyleResource(filePath) {
621
787
  const normalized = filePath.toLowerCase();
622
788
  return STYLE_EXTENSIONS.some(ext => normalized.endsWith(ext));
623
789
  }
790
+ function isCssModuleResource(filePath) {
791
+ return /\.module\.(css|scss|sass|less)$/i.test(filePath);
792
+ }
793
+ function isScriptResource(filePath) {
794
+ const normalized = filePath.toLowerCase();
795
+ if (normalized.endsWith('.d.ts')) {
796
+ return false;
797
+ }
798
+ return SCRIPT_EXTENSIONS.some(ext => normalized.endsWith(ext));
799
+ }
800
+ async function hasStyleImports(filePath, options) {
801
+ let source;
802
+ try {
803
+ source = await promises_1.default.readFile(filePath, 'utf8');
804
+ }
805
+ catch {
806
+ return false;
807
+ }
808
+ const candidates = new Set();
809
+ try {
810
+ const analysis = await (0, lexer_js_1.analyzeModule)(source, filePath);
811
+ for (const specifier of analysis.imports) {
812
+ if (specifier) {
813
+ candidates.add(specifier);
814
+ }
815
+ }
816
+ }
817
+ catch {
818
+ // fall back to regex scanning below
819
+ }
820
+ const importRegex = /(import|require)\s*(?:\(|[^'"`]*)(['"])([^'"`]+)\2/g;
821
+ let match;
822
+ while ((match = importRegex.exec(source)) !== null) {
823
+ const specifier = match[3];
824
+ if (specifier) {
825
+ candidates.add(specifier);
826
+ }
827
+ }
828
+ for (const specifier of candidates) {
829
+ const cleaned = stripInlineLoader(specifier.trim());
830
+ const { resource } = splitResourceAndQuery(cleaned);
831
+ if (!resource) {
832
+ continue;
833
+ }
834
+ if (isStyleResource(resource)) {
835
+ return true;
836
+ }
837
+ const resolved = await resolveImportPath(resource, filePath, options.rootDir, options.tsconfig, options.resolver, options.resolverFactory, RESOLUTION_EXTENSIONS);
838
+ if (resolved && isStyleResource(resolved)) {
839
+ return true;
840
+ }
841
+ }
842
+ return false;
843
+ }
624
844
  function collectSelectorTokensFromCss(css) {
625
845
  const tokens = new Set();
626
846
  const pattern = /\.([A-Za-z_-][A-Za-z0-9_-]*)\b/g;
@@ -653,6 +873,21 @@ async function resolveProxyInfo(manifestKey, selectorSource, resolvedPath, cache
653
873
  cache.set(manifestKey, proxyInfo);
654
874
  return proxyInfo;
655
875
  }
876
+ async function resolveDeclarationProxyInfo(manifestKey, resolvedPath, cache) {
877
+ const cached = cache.get(manifestKey);
878
+ if (cached !== undefined) {
879
+ return cached;
880
+ }
881
+ const defaultSignal = await getDefaultExportSignal(resolvedPath);
882
+ const exportedNames = await getNamedExports(resolvedPath);
883
+ const proxyInfo = {
884
+ moduleSpecifier: buildDeclarationModuleSpecifier(resolvedPath),
885
+ includeDefault: defaultSignal === 'has-default',
886
+ exportedNames,
887
+ };
888
+ cache.set(manifestKey, proxyInfo);
889
+ return proxyInfo;
890
+ }
656
891
  function buildProxyModuleSpecifier(resolvedPath, selectorSource) {
657
892
  const resolvedExt = node_path_1.default.extname(resolvedPath);
658
893
  const baseName = node_path_1.default.basename(resolvedPath, resolvedExt);
@@ -670,6 +905,16 @@ async function getDefaultExportSignal(filePath) {
670
905
  return 'unknown';
671
906
  }
672
907
  }
908
+ async function getNamedExports(filePath) {
909
+ try {
910
+ const source = await promises_1.default.readFile(filePath, 'utf8');
911
+ const analysis = await (0, lexer_js_1.analyzeModule)(source, filePath);
912
+ return new Set(analysis.exports ?? []);
913
+ }
914
+ catch {
915
+ return new Set();
916
+ }
917
+ }
673
918
  function createProjectPeerResolver(rootDir) {
674
919
  const resolver = getProjectRequire(rootDir);
675
920
  return async (name) => {
@@ -742,6 +987,8 @@ async function runGenerateTypesCli(argv = process.argv.slice(2)) {
742
987
  autoStable: parsed.autoStable,
743
988
  hashed: parsed.hashed,
744
989
  resolver,
990
+ mode: parsed.mode,
991
+ manifestPath: parsed.manifestPath,
745
992
  });
746
993
  reportCliResult(result);
747
994
  }
@@ -759,10 +1006,20 @@ function parseCliArgs(argv) {
759
1006
  let autoStable = false;
760
1007
  let hashed = false;
761
1008
  let resolver;
1009
+ let mode = 'module';
1010
+ let manifestPath;
762
1011
  for (let i = 0; i < argv.length; i += 1) {
763
1012
  const arg = argv[i];
764
1013
  if (arg === '--help' || arg === '-h') {
765
- return { rootDir, include, outDir, stableNamespace, autoStable, help: true };
1014
+ return {
1015
+ rootDir,
1016
+ include,
1017
+ outDir,
1018
+ stableNamespace,
1019
+ autoStable,
1020
+ mode,
1021
+ help: true,
1022
+ };
766
1023
  }
767
1024
  if (arg === '--auto-stable') {
768
1025
  autoStable = true;
@@ -796,6 +1053,14 @@ function parseCliArgs(argv) {
796
1053
  outDir = value;
797
1054
  continue;
798
1055
  }
1056
+ if (arg === '--manifest') {
1057
+ const value = argv[++i];
1058
+ if (!value) {
1059
+ throw new Error('Missing value for --manifest');
1060
+ }
1061
+ manifestPath = value;
1062
+ continue;
1063
+ }
799
1064
  if (arg === '--stable-namespace') {
800
1065
  const value = argv[++i];
801
1066
  if (!value) {
@@ -812,6 +1077,17 @@ function parseCliArgs(argv) {
812
1077
  resolver = value;
813
1078
  continue;
814
1079
  }
1080
+ if (arg === '--mode') {
1081
+ const value = argv[++i];
1082
+ if (!value) {
1083
+ throw new Error('Missing value for --mode');
1084
+ }
1085
+ if (value !== 'module' && value !== 'declaration') {
1086
+ throw new Error(`Unknown mode: ${value}`);
1087
+ }
1088
+ mode = value;
1089
+ continue;
1090
+ }
815
1091
  if (arg.startsWith('-')) {
816
1092
  throw new Error(`Unknown flag: ${arg}`);
817
1093
  }
@@ -820,20 +1096,35 @@ function parseCliArgs(argv) {
820
1096
  if (autoStable && hashed) {
821
1097
  throw new Error('Cannot combine --auto-stable with --hashed');
822
1098
  }
823
- return { rootDir, include, outDir, stableNamespace, autoStable, hashed, resolver };
1099
+ if (manifestPath && mode !== 'declaration') {
1100
+ throw new Error('Cannot use --manifest unless --mode is declaration');
1101
+ }
1102
+ return {
1103
+ rootDir,
1104
+ include,
1105
+ outDir,
1106
+ stableNamespace,
1107
+ autoStable,
1108
+ hashed,
1109
+ resolver,
1110
+ mode,
1111
+ manifestPath,
1112
+ };
824
1113
  }
825
1114
  function printHelp() {
826
1115
  console.log(`Usage: knighted-css-generate-types [options]
827
1116
 
828
1117
  Options:
829
- -r, --root <path> Project root directory (default: cwd)
830
- -i, --include <path> Additional directories/files to scan (repeatable)
831
- --out-dir <path> Directory to store selector module manifest cache
832
- --stable-namespace <name> Stable namespace prefix for generated selector maps
833
- --auto-stable Enable autoStable when extracting CSS for selectors
834
- --hashed Emit selectors backed by loader-bridge hashed modules
835
- --resolver <path> Path or package name exporting a CssResolver
836
- -h, --help Show this help message
1118
+ -r, --root <path> Project root directory (default: cwd)
1119
+ -i, --include <path> Additional directories/files to scan (repeatable)
1120
+ --out-dir <path> Directory to store selector module manifest cache
1121
+ --stable-namespace <name> Stable namespace prefix for generated selector maps
1122
+ --auto-stable Enable autoStable when extracting CSS for selectors
1123
+ --hashed Emit selectors backed by loader-bridge hashed modules
1124
+ --resolver <path> Path or package name exporting a CssResolver
1125
+ --mode <module|declaration> Emit selector modules (module) or declaration files (declaration)
1126
+ --manifest <path> Write a sidecar manifest (declaration mode only)
1127
+ -h, --help Show this help message
837
1128
  `);
838
1129
  }
839
1130
  function reportCliResult(result) {
@@ -844,6 +1135,9 @@ function reportCliResult(result) {
844
1135
  console.log(`[knighted-css] Selector modules updated: wrote ${result.selectorModulesWritten}, removed ${result.selectorModulesRemoved}.`);
845
1136
  }
846
1137
  console.log(`[knighted-css] Manifest: ${result.manifestPath}`);
1138
+ if (result.sidecarManifestPath) {
1139
+ console.log(`[knighted-css] Sidecar manifest: ${result.sidecarManifestPath}`);
1140
+ }
847
1141
  for (const warning of result.warnings) {
848
1142
  console.warn(`[knighted-css] ${warning}`);
849
1143
  }
@@ -873,23 +1167,31 @@ exports.__generateTypesInternals = {
873
1167
  setImportMetaUrlProvider,
874
1168
  isNonRelativeSpecifier,
875
1169
  isStyleResource,
1170
+ isCssModuleResource,
876
1171
  resolveProxyInfo,
1172
+ resolveDeclarationProxyInfo,
877
1173
  resolveWithExtensionFallback,
878
1174
  resolveIndexFallback,
879
1175
  createProjectPeerResolver,
880
1176
  getProjectRequire,
881
1177
  loadTsconfigResolutionContext,
882
1178
  resolveWithTsconfigPaths,
1179
+ hasStyleImports,
883
1180
  loadResolverModule,
884
1181
  parseCliArgs,
885
1182
  printHelp,
886
1183
  reportCliResult,
887
1184
  buildSelectorModuleManifestKey,
888
1185
  buildSelectorModulePath,
1186
+ buildDeclarationModuleSpecifier,
889
1187
  formatSelectorModuleSource,
1188
+ buildDeclarationPath,
1189
+ formatDeclarationSource,
1190
+ ensureDeclarationModule,
890
1191
  ensureSelectorModule,
891
1192
  removeStaleSelectorModules,
892
1193
  readManifest,
893
1194
  writeManifest,
1195
+ writeSidecarManifest,
894
1196
  };
895
1197
  //# sourceMappingURL=generateTypes.cjs.map