@knighted/css 1.0.0-rc.11 → 1.0.0-rc.13

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.
@@ -16,7 +16,6 @@ const node_module_type_1 = require("node-module-type");
16
16
  const get_tsconfig_1 = require("get-tsconfig");
17
17
  const tsconfig_paths_1 = require("tsconfig-paths");
18
18
  const css_js_1 = require("./css.cjs");
19
- const loaderInternals_js_1 = require("./loaderInternals.cjs");
20
19
  const stableSelectorsLiteral_js_1 = require("./stableSelectorsLiteral.cjs");
21
20
  const stableNamespace_js_1 = require("./stableNamespace.cjs");
22
21
  let activeCssWithMeta = css_js_1.cssWithMeta;
@@ -65,22 +64,19 @@ function getImportMetaUrl() {
65
64
  }
66
65
  }
67
66
  const PACKAGE_ROOT = resolvePackageRoot();
68
- const DEFAULT_TYPES_ROOT = node_path_1.default.join(PACKAGE_ROOT, 'types-stub');
69
- const DEFAULT_OUT_DIR = node_path_1.default.join(PACKAGE_ROOT, 'node_modules', '.knighted-css');
67
+ const SELECTOR_REFERENCE = '.knighted-css';
68
+ const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts';
70
69
  async function generateTypes(options = {}) {
71
70
  const rootDir = node_path_1.default.resolve(options.rootDir ?? process.cwd());
72
71
  const include = normalizeIncludeOptions(options.include, rootDir);
73
- const outDir = node_path_1.default.resolve(options.outDir ?? DEFAULT_OUT_DIR);
74
- const typesRoot = node_path_1.default.resolve(options.typesRoot ?? DEFAULT_TYPES_ROOT);
72
+ const cacheDir = node_path_1.default.resolve(options.outDir ?? node_path_1.default.join(rootDir, '.knighted-css'));
75
73
  const tsconfig = loadTsconfigResolutionContext(rootDir);
76
74
  await es_module_lexer_1.init;
77
- await promises_1.default.mkdir(outDir, { recursive: true });
78
- await promises_1.default.mkdir(typesRoot, { recursive: true });
75
+ await promises_1.default.mkdir(cacheDir, { recursive: true });
79
76
  const internalOptions = {
80
77
  rootDir,
81
78
  include,
82
- outDir,
83
- typesRoot,
79
+ cacheDir,
84
80
  stableNamespace: options.stableNamespace,
85
81
  tsconfig,
86
82
  };
@@ -89,29 +85,27 @@ async function generateTypes(options = {}) {
89
85
  async function generateDeclarations(options) {
90
86
  const peerResolver = createProjectPeerResolver(options.rootDir);
91
87
  const files = await collectCandidateFiles(options.include);
92
- const manifestPath = node_path_1.default.join(options.outDir, 'manifest.json');
93
- const previousManifest = await readManifest(manifestPath);
94
- const nextManifest = {};
88
+ const selectorModulesManifestPath = node_path_1.default.join(options.cacheDir, 'selector-modules.json');
89
+ const previousSelectorManifest = await readManifest(selectorModulesManifestPath);
90
+ const nextSelectorManifest = {};
95
91
  const selectorCache = new Map();
96
- const processedSpecifiers = new Set();
97
- const declarations = [];
92
+ const processedSelectors = new Set();
98
93
  const warnings = [];
99
- let writes = 0;
94
+ let selectorModuleWrites = 0;
100
95
  for (const filePath of files) {
101
96
  const matches = await findSpecifierImports(filePath);
102
97
  for (const match of matches) {
103
98
  const cleaned = match.specifier.trim();
104
99
  const inlineFree = stripInlineLoader(cleaned);
105
- if (!inlineFree.includes('?knighted-css'))
106
- continue;
107
- const { resource, query } = splitResourceAndQuery(inlineFree);
108
- if (!query || !(0, loaderInternals_js_1.hasQueryFlag)(query, loaderInternals_js_1.TYPES_QUERY_FLAG)) {
100
+ const { resource } = splitResourceAndQuery(inlineFree);
101
+ const selectorSource = extractSelectorSourceSpecifier(resource);
102
+ if (!selectorSource) {
109
103
  continue;
110
104
  }
111
105
  const resolvedNamespace = (0, stableNamespace_js_1.resolveStableNamespace)(options.stableNamespace);
112
- const resolvedPath = await resolveImportPath(resource, match.importer, options.rootDir, options.tsconfig);
106
+ const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig);
113
107
  if (!resolvedPath) {
114
- warnings.push(`Unable to resolve ${resource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
108
+ warnings.push(`Unable to resolve ${selectorSource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
115
109
  continue;
116
110
  }
117
111
  const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
@@ -135,42 +129,28 @@ async function generateDeclarations(options) {
135
129
  }
136
130
  selectorCache.set(cacheKey, selectorMap);
137
131
  }
138
- const canonicalSpecifier = buildDeclarationModuleSpecifier(resolvedPath, options.outDir, query);
139
- if (processedSpecifiers.has(canonicalSpecifier)) {
132
+ if (!isWithinRoot(resolvedPath, options.rootDir)) {
133
+ warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
140
134
  continue;
141
135
  }
142
- const variant = (0, loaderInternals_js_1.determineSelectorVariant)(query);
143
- const declaration = formatModuleDeclaration(canonicalSpecifier, variant, selectorMap);
144
- const declarationHash = hashContent(declaration);
145
- const fileName = buildDeclarationFileName(canonicalSpecifier);
146
- const targetPath = node_path_1.default.join(options.outDir, fileName);
147
- const previousEntry = previousManifest[canonicalSpecifier];
148
- const needsWrite = previousEntry?.hash !== declarationHash || !(await fileExists(targetPath));
149
- if (needsWrite) {
150
- await promises_1.default.writeFile(targetPath, declaration, 'utf8');
151
- writes += 1;
136
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
137
+ if (processedSelectors.has(manifestKey)) {
138
+ continue;
152
139
  }
153
- nextManifest[canonicalSpecifier] = { file: fileName, hash: declarationHash };
154
- if (needsWrite) {
155
- declarations.push({ specifier: canonicalSpecifier, filePath: targetPath });
140
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest);
141
+ if (moduleWrite) {
142
+ selectorModuleWrites += 1;
156
143
  }
157
- processedSpecifiers.add(canonicalSpecifier);
144
+ processedSelectors.add(manifestKey);
158
145
  }
159
146
  }
160
- const removed = await removeStaleDeclarations(previousManifest, nextManifest, options.outDir);
161
- await writeManifest(manifestPath, nextManifest);
162
- const typesIndexPath = node_path_1.default.join(options.typesRoot, 'index.d.ts');
163
- await writeTypesIndex(typesIndexPath, nextManifest, options.outDir);
164
- if (Object.keys(nextManifest).length === 0) {
165
- declarations.length = 0;
166
- }
147
+ const selectorModulesRemoved = await removeStaleSelectorModules(previousSelectorManifest, nextSelectorManifest);
148
+ await writeManifest(selectorModulesManifestPath, nextSelectorManifest);
167
149
  return {
168
- written: writes,
169
- removed,
170
- declarations,
150
+ selectorModulesWritten: selectorModuleWrites,
151
+ selectorModulesRemoved,
171
152
  warnings,
172
- outDir: options.outDir,
173
- typesIndexPath,
153
+ manifestPath: selectorModulesManifestPath,
174
154
  };
175
155
  }
176
156
  function normalizeIncludeOptions(include, rootDir) {
@@ -228,18 +208,18 @@ async function findSpecifierImports(filePath) {
228
208
  catch {
229
209
  return [];
230
210
  }
231
- if (!source.includes('?knighted-css')) {
211
+ if (!source.includes(SELECTOR_REFERENCE)) {
232
212
  return [];
233
213
  }
234
214
  const matches = [];
235
215
  const [imports] = (0, es_module_lexer_1.parse)(source, filePath);
236
216
  for (const record of imports) {
237
217
  const specifier = record.n ?? source.slice(record.s, record.e);
238
- if (specifier && specifier.includes('?knighted-css')) {
218
+ if (specifier && specifier.includes(SELECTOR_REFERENCE)) {
239
219
  matches.push({ specifier, importer: filePath });
240
220
  }
241
221
  }
242
- const requireRegex = /require\((['"])([^'"`]+?\?knighted-css[^'"`]*)\1\)/g;
222
+ const requireRegex = /require\((['"])([^'"`]+?\.knighted-css[^'"`]*)\1\)/g;
243
223
  let reqMatch;
244
224
  while ((reqMatch = requireRegex.exec(source)) !== null) {
245
225
  const spec = reqMatch[2];
@@ -262,6 +242,21 @@ function splitResourceAndQuery(specifier) {
262
242
  }
263
243
  return { resource: trimmed.slice(0, queryIndex), query: trimmed.slice(queryIndex) };
264
244
  }
245
+ function extractSelectorSourceSpecifier(specifier) {
246
+ const markerIndex = specifier.indexOf(SELECTOR_REFERENCE);
247
+ if (markerIndex < 0) {
248
+ return undefined;
249
+ }
250
+ const suffix = specifier.slice(markerIndex + SELECTOR_REFERENCE.length);
251
+ if (suffix.length > 0 && !/\.(?:[cm]?[tj]s|[tj]sx)$/.test(suffix)) {
252
+ return undefined;
253
+ }
254
+ const base = specifier.slice(0, markerIndex);
255
+ if (!base) {
256
+ return undefined;
257
+ }
258
+ return base;
259
+ }
265
260
  const projectRequireCache = new Map();
266
261
  async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tsconfig) {
267
262
  if (!resourceSpecifier)
@@ -284,91 +279,29 @@ async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tscon
284
279
  return undefined;
285
280
  }
286
281
  }
287
- function buildDeclarationFileName(specifier) {
288
- const digest = node_crypto_1.default.createHash('sha1').update(specifier).digest('hex').slice(0, 12);
289
- return `knt-${digest}.d.ts`;
290
- }
291
- function formatModuleDeclaration(specifier, variant, selectors) {
292
- const literalSpecifier = JSON.stringify(specifier);
293
- const selectorType = formatSelectorType(selectors);
294
- const header = `declare module ${literalSpecifier} {`;
295
- const footer = '}';
296
- if (variant === 'types') {
297
- return `${header}
298
- export const knightedCss: string
299
- export const stableSelectors: ${selectorType}
300
- ${footer}
301
- `;
302
- }
303
- const stableLine = ` export const stableSelectors: ${selectorType}`;
304
- const shared = ` const combined: KnightedCssCombinedModule<Record<string, unknown>>
305
- export const knightedCss: string
306
- ${stableLine}`;
307
- if (variant === 'combined') {
308
- return `${header}
309
- ${shared}
310
- export default combined
311
- ${footer}
312
- `;
313
- }
314
- return `${header}
315
- ${shared}
316
- ${footer}
317
- `;
282
+ function buildSelectorModuleManifestKey(resolvedPath) {
283
+ return resolvedPath.split(node_path_1.default.sep).join('/');
318
284
  }
319
- function formatSelectorType(selectors) {
320
- if (selectors.size === 0) {
321
- return 'Readonly<Record<string, string>>';
322
- }
285
+ function buildSelectorModulePath(resolvedPath) {
286
+ return `${resolvedPath}${SELECTOR_MODULE_SUFFIX}`;
287
+ }
288
+ function formatSelectorModuleSource(selectors) {
289
+ const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
323
290
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
324
- const lines = entries.map(([token, selector]) => ` readonly ${JSON.stringify(token)}: ${JSON.stringify(selector)}`);
325
- return `Readonly<{
291
+ const lines = entries.map(([token, selector]) => ` ${JSON.stringify(token)}: ${JSON.stringify(selector)},`);
292
+ const literal = lines.length > 0
293
+ ? `{
326
294
  ${lines.join('\n')}
327
- }>`;
328
- }
329
- function buildDeclarationModuleSpecifier(resolvedPath, declarationDir, query) {
330
- const relativePath = node_path_1.default.relative(declarationDir, resolvedPath);
331
- const normalizedPath = normalizeRelativePath(relativePath);
332
- const canonicalQuery = buildCanonicalQuery(query);
333
- return `${normalizedPath}${canonicalQuery}`;
334
- }
335
- function normalizeRelativePath(relativePath) {
336
- let normalized = relativePath.split(node_path_1.default.sep).join('/');
337
- if (!normalized || normalized === '') {
338
- normalized = '.';
339
- }
340
- if (normalized === '.') {
341
- return './';
342
- }
343
- if (normalized.startsWith('./') || normalized.startsWith('../')) {
344
- return normalized;
345
- }
346
- if (normalized.startsWith('.')) {
347
- return normalized;
348
- }
349
- return `./${normalized}`;
350
- }
351
- function buildCanonicalQuery(query) {
352
- if (!query) {
353
- return '';
354
- }
355
- const sanitized = (0, loaderInternals_js_1.buildSanitizedQuery)(query);
356
- const extraParts = sanitized ? sanitized.slice(1).split('&').filter(Boolean) : [];
357
- const parts = [];
358
- parts.push('knighted-css');
359
- if ((0, loaderInternals_js_1.hasQueryFlag)(query, loaderInternals_js_1.COMBINED_QUERY_FLAG)) {
360
- parts.push(loaderInternals_js_1.COMBINED_QUERY_FLAG);
361
- }
362
- for (const flag of loaderInternals_js_1.NAMED_ONLY_QUERY_FLAGS) {
363
- if ((0, loaderInternals_js_1.hasQueryFlag)(query, flag)) {
364
- parts.push(flag);
365
- }
366
- }
367
- if ((0, loaderInternals_js_1.hasQueryFlag)(query, loaderInternals_js_1.TYPES_QUERY_FLAG)) {
368
- parts.push(loaderInternals_js_1.TYPES_QUERY_FLAG);
369
- }
370
- const merged = [...parts, ...extraParts];
371
- return merged.length > 0 ? `?${merged.join('&')}` : '';
295
+ } as const`
296
+ : '{} as const';
297
+ return `${header}
298
+ export const stableSelectors = ${literal}
299
+
300
+ export type KnightedCssStableSelectors = typeof stableSelectors
301
+ export type KnightedCssStableSelectorToken = keyof typeof stableSelectors
302
+
303
+ export default stableSelectors
304
+ `;
372
305
  }
373
306
  function hashContent(content) {
374
307
  return node_crypto_1.default.createHash('sha1').update(content).digest('hex');
@@ -385,13 +318,12 @@ async function readManifest(manifestPath) {
385
318
  async function writeManifest(manifestPath, manifest) {
386
319
  await promises_1.default.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
387
320
  }
388
- async function removeStaleDeclarations(previous, next, outDir) {
389
- const stale = Object.entries(previous).filter(([specifier]) => !next[specifier]);
321
+ async function removeStaleSelectorModules(previous, next) {
322
+ const stale = Object.entries(previous).filter(([key]) => !next[key]);
390
323
  let removed = 0;
391
324
  for (const [, entry] of stale) {
392
- const targetPath = node_path_1.default.join(outDir, entry.file);
393
325
  try {
394
- await promises_1.default.unlink(targetPath);
326
+ await promises_1.default.unlink(entry.file);
395
327
  removed += 1;
396
328
  }
397
329
  catch {
@@ -400,25 +332,6 @@ async function removeStaleDeclarations(previous, next, outDir) {
400
332
  }
401
333
  return removed;
402
334
  }
403
- async function writeTypesIndex(indexPath, manifest, outDir) {
404
- const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
405
- const references = Object.values(manifest)
406
- .sort((a, b) => a.file.localeCompare(b.file))
407
- .map(entry => {
408
- const rel = node_path_1.default
409
- .relative(node_path_1.default.dirname(indexPath), node_path_1.default.join(outDir, entry.file))
410
- .split(node_path_1.default.sep)
411
- .join('/');
412
- return `/// <reference path="${rel}" />`;
413
- });
414
- const content = references.length > 0
415
- ? `${header}
416
- ${references.join('\n')}
417
- `
418
- : `${header}
419
- `;
420
- await promises_1.default.writeFile(indexPath, content, 'utf8');
421
- }
422
335
  function formatErrorMessage(error) {
423
336
  if (error instanceof Error && typeof error.message === 'string') {
424
337
  return error.message;
@@ -428,6 +341,23 @@ function formatErrorMessage(error) {
428
341
  function relativeToRoot(filePath, rootDir) {
429
342
  return node_path_1.default.relative(rootDir, filePath) || filePath;
430
343
  }
344
+ function isWithinRoot(filePath, rootDir) {
345
+ const relative = node_path_1.default.relative(rootDir, filePath);
346
+ return relative === '' || (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative));
347
+ }
348
+ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest) {
349
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
350
+ const targetPath = buildSelectorModulePath(resolvedPath);
351
+ const source = formatSelectorModuleSource(selectors);
352
+ const hash = hashContent(source);
353
+ const previousEntry = previousManifest[manifestKey];
354
+ const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
355
+ if (needsWrite) {
356
+ await promises_1.default.writeFile(targetPath, source, 'utf8');
357
+ }
358
+ nextManifest[manifestKey] = { file: targetPath, hash };
359
+ return needsWrite;
360
+ }
431
361
  async function fileExists(target) {
432
362
  try {
433
363
  await promises_1.default.access(target);
@@ -551,7 +481,6 @@ async function runGenerateTypesCli(argv = process.argv.slice(2)) {
551
481
  rootDir: parsed.rootDir,
552
482
  include: parsed.include,
553
483
  outDir: parsed.outDir,
554
- typesRoot: parsed.typesRoot,
555
484
  stableNamespace: parsed.stableNamespace,
556
485
  });
557
486
  reportCliResult(result);
@@ -566,12 +495,11 @@ function parseCliArgs(argv) {
566
495
  let rootDir = process.cwd();
567
496
  const include = [];
568
497
  let outDir;
569
- let typesRoot;
570
498
  let stableNamespace;
571
499
  for (let i = 0; i < argv.length; i += 1) {
572
500
  const arg = argv[i];
573
501
  if (arg === '--help' || arg === '-h') {
574
- return { rootDir, include, outDir, typesRoot, stableNamespace, help: true };
502
+ return { rootDir, include, outDir, stableNamespace, help: true };
575
503
  }
576
504
  if (arg === '--root' || arg === '-r') {
577
505
  const value = argv[++i];
@@ -597,14 +525,6 @@ function parseCliArgs(argv) {
597
525
  outDir = value;
598
526
  continue;
599
527
  }
600
- if (arg === '--types-root') {
601
- const value = argv[++i];
602
- if (!value) {
603
- throw new Error('Missing value for --types-root');
604
- }
605
- typesRoot = value;
606
- continue;
607
- }
608
528
  if (arg === '--stable-namespace') {
609
529
  const value = argv[++i];
610
530
  if (!value) {
@@ -618,7 +538,7 @@ function parseCliArgs(argv) {
618
538
  }
619
539
  include.push(arg);
620
540
  }
621
- return { rootDir, include, outDir, typesRoot, stableNamespace };
541
+ return { rootDir, include, outDir, stableNamespace };
622
542
  }
623
543
  function printHelp() {
624
544
  console.log(`Usage: knighted-css-generate-types [options]
@@ -626,20 +546,19 @@ function printHelp() {
626
546
  Options:
627
547
  -r, --root <path> Project root directory (default: cwd)
628
548
  -i, --include <path> Additional directories/files to scan (repeatable)
629
- --out-dir <path> Output directory for generated declarations
630
- --types-root <path> Directory for generated @types entrypoint
549
+ --out-dir <path> Directory to store selector module manifest cache
631
550
  --stable-namespace <name> Stable namespace prefix for generated selector maps
632
551
  -h, --help Show this help message
633
552
  `);
634
553
  }
635
554
  function reportCliResult(result) {
636
- if (result.written === 0 && result.removed === 0) {
637
- console.log('[knighted-css] No changes to ?knighted-css&types declarations (cache is up to date).');
555
+ if (result.selectorModulesWritten === 0 && result.selectorModulesRemoved === 0) {
556
+ console.log('[knighted-css] Selector modules are up to date.');
638
557
  }
639
558
  else {
640
- console.log(`[knighted-css] Updated ${result.written} declaration(s), removed ${result.removed}, output in ${result.outDir}.`);
559
+ console.log(`[knighted-css] Selector modules updated: wrote ${result.selectorModulesWritten}, removed ${result.selectorModulesRemoved}.`);
641
560
  }
642
- console.log(`[knighted-css] Type references: ${result.typesIndexPath}`);
561
+ console.log(`[knighted-css] Manifest: ${result.manifestPath}`);
643
562
  for (const warning of result.warnings) {
644
563
  console.warn(`[knighted-css] ${warning}`);
645
564
  }
@@ -654,17 +573,12 @@ function setImportMetaUrlProvider(provider) {
654
573
  importMetaUrlProvider = provider ?? getImportMetaUrl;
655
574
  }
656
575
  exports.__generateTypesInternals = {
657
- writeTypesIndex,
658
576
  stripInlineLoader,
659
577
  splitResourceAndQuery,
578
+ extractSelectorSourceSpecifier,
660
579
  findSpecifierImports,
661
580
  resolveImportPath,
662
581
  resolvePackageRoot,
663
- buildDeclarationFileName,
664
- formatModuleDeclaration,
665
- formatSelectorType,
666
- buildDeclarationModuleSpecifier,
667
- buildCanonicalQuery,
668
582
  relativeToRoot,
669
583
  collectCandidateFiles,
670
584
  normalizeIncludeOptions,
@@ -680,4 +594,11 @@ exports.__generateTypesInternals = {
680
594
  parseCliArgs,
681
595
  printHelp,
682
596
  reportCliResult,
597
+ buildSelectorModuleManifestKey,
598
+ buildSelectorModulePath,
599
+ formatSelectorModuleSource,
600
+ ensureSelectorModule,
601
+ removeStaleSelectorModules,
602
+ readManifest,
603
+ writeManifest,
683
604
  };
@@ -3,38 +3,30 @@ import { moduleType } from 'node-module-type';
3
3
  import { getTsconfig } from 'get-tsconfig';
4
4
  import { type MatchPath } from 'tsconfig-paths';
5
5
  import { cssWithMeta } from './css.cjs';
6
- import { type SelectorTypeVariant } from './loaderInternals.cjs';
7
- interface ManifestEntry {
8
- file: string;
9
- hash: string;
10
- }
11
- type Manifest = Record<string, ManifestEntry>;
12
6
  interface ImportMatch {
13
7
  specifier: string;
14
8
  importer: string;
15
9
  }
16
- interface DeclarationRecord {
17
- specifier: string;
18
- filePath: string;
10
+ interface ManifestEntry {
11
+ file: string;
12
+ hash: string;
19
13
  }
14
+ type SelectorModuleManifest = Record<string, ManifestEntry>;
20
15
  interface TsconfigResolutionContext {
21
16
  absoluteBaseUrl?: string;
22
17
  matchPath?: MatchPath;
23
18
  }
24
19
  type CssWithMetaFn = typeof cssWithMeta;
25
20
  export interface GenerateTypesResult {
26
- written: number;
27
- removed: number;
28
- declarations: DeclarationRecord[];
21
+ selectorModulesWritten: number;
22
+ selectorModulesRemoved: number;
29
23
  warnings: string[];
30
- outDir: string;
31
- typesIndexPath: string;
24
+ manifestPath: string;
32
25
  }
33
26
  export interface GenerateTypesOptions {
34
27
  rootDir?: string;
35
28
  include?: string[];
36
29
  outDir?: string;
37
- typesRoot?: string;
38
30
  stableNamespace?: string;
39
31
  }
40
32
  type ModuleTypeDetector = () => ReturnType<typeof moduleType>;
@@ -48,14 +40,16 @@ declare function splitResourceAndQuery(specifier: string): {
48
40
  resource: string;
49
41
  query: string;
50
42
  };
43
+ declare function extractSelectorSourceSpecifier(specifier: string): string | undefined;
51
44
  declare function resolveImportPath(resourceSpecifier: string, importerPath: string, rootDir: string, tsconfig?: TsconfigResolutionContext): Promise<string | undefined>;
52
- declare function buildDeclarationFileName(specifier: string): string;
53
- declare function formatModuleDeclaration(specifier: string, variant: SelectorTypeVariant, selectors: Map<string, string>): string;
54
- declare function formatSelectorType(selectors: Map<string, string>): string;
55
- declare function buildDeclarationModuleSpecifier(resolvedPath: string, declarationDir: string, query: string): string;
56
- declare function buildCanonicalQuery(query: string): string;
57
- declare function writeTypesIndex(indexPath: string, manifest: Manifest, outDir: string): Promise<void>;
45
+ declare function buildSelectorModuleManifestKey(resolvedPath: string): string;
46
+ declare function buildSelectorModulePath(resolvedPath: string): string;
47
+ declare function formatSelectorModuleSource(selectors: Map<string, string>): string;
48
+ declare function readManifest(manifestPath: string): Promise<SelectorModuleManifest>;
49
+ declare function writeManifest(manifestPath: string, manifest: SelectorModuleManifest): Promise<void>;
50
+ declare function removeStaleSelectorModules(previous: SelectorModuleManifest, next: SelectorModuleManifest): Promise<number>;
58
51
  declare function relativeToRoot(filePath: string, rootDir: string): string;
52
+ declare function ensureSelectorModule(resolvedPath: string, selectors: Map<string, string>, previousManifest: SelectorModuleManifest, nextManifest: SelectorModuleManifest): Promise<boolean>;
59
53
  declare function resolveWithTsconfigPaths(specifier: string, tsconfig?: TsconfigResolutionContext): Promise<string | undefined>;
60
54
  declare function loadTsconfigResolutionContext(rootDir: string, loader?: typeof getTsconfig): TsconfigResolutionContext | undefined;
61
55
  declare function normalizeTsconfigPaths(paths: Record<string, string[] | string> | undefined): Record<string, string[]> | undefined;
@@ -67,7 +61,6 @@ export interface ParsedCliArgs {
67
61
  rootDir: string;
68
62
  include?: string[];
69
63
  outDir?: string;
70
- typesRoot?: string;
71
64
  stableNamespace?: string;
72
65
  help?: boolean;
73
66
  }
@@ -78,17 +71,12 @@ declare function setCssWithMetaImplementation(impl?: CssWithMetaFn): void;
78
71
  declare function setModuleTypeDetector(detector?: ModuleTypeDetector): void;
79
72
  declare function setImportMetaUrlProvider(provider?: () => string | undefined): void;
80
73
  export declare const __generateTypesInternals: {
81
- writeTypesIndex: typeof writeTypesIndex;
82
74
  stripInlineLoader: typeof stripInlineLoader;
83
75
  splitResourceAndQuery: typeof splitResourceAndQuery;
76
+ extractSelectorSourceSpecifier: typeof extractSelectorSourceSpecifier;
84
77
  findSpecifierImports: typeof findSpecifierImports;
85
78
  resolveImportPath: typeof resolveImportPath;
86
79
  resolvePackageRoot: typeof resolvePackageRoot;
87
- buildDeclarationFileName: typeof buildDeclarationFileName;
88
- formatModuleDeclaration: typeof formatModuleDeclaration;
89
- formatSelectorType: typeof formatSelectorType;
90
- buildDeclarationModuleSpecifier: typeof buildDeclarationModuleSpecifier;
91
- buildCanonicalQuery: typeof buildCanonicalQuery;
92
80
  relativeToRoot: typeof relativeToRoot;
93
81
  collectCandidateFiles: typeof collectCandidateFiles;
94
82
  normalizeIncludeOptions: typeof normalizeIncludeOptions;
@@ -104,5 +92,12 @@ export declare const __generateTypesInternals: {
104
92
  parseCliArgs: typeof parseCliArgs;
105
93
  printHelp: typeof printHelp;
106
94
  reportCliResult: typeof reportCliResult;
95
+ buildSelectorModuleManifestKey: typeof buildSelectorModuleManifestKey;
96
+ buildSelectorModulePath: typeof buildSelectorModulePath;
97
+ formatSelectorModuleSource: typeof formatSelectorModuleSource;
98
+ ensureSelectorModule: typeof ensureSelectorModule;
99
+ removeStaleSelectorModules: typeof removeStaleSelectorModules;
100
+ readManifest: typeof readManifest;
101
+ writeManifest: typeof writeManifest;
107
102
  };
108
103
  export {};
@@ -6,8 +6,10 @@ exports.stableClass = stableClass;
6
6
  exports.stableSelector = stableSelector;
7
7
  exports.createStableClassFactory = createStableClassFactory;
8
8
  exports.stableClassName = stableClassName;
9
+ exports.mergeStableClass = mergeStableClass;
9
10
  const DEFAULT_NAMESPACE = 'knighted';
10
11
  const defaultJoin = (values) => values.filter(Boolean).join(' ');
12
+ const toArray = (value) => Array.isArray(value) ? value : [value];
11
13
  const normalizeToken = (token) => {
12
14
  const sanitized = token
13
15
  .trim()
@@ -42,3 +44,30 @@ function stableClassName(styles, key, options) {
42
44
  return join([hashed, stable]);
43
45
  }
44
46
  exports.stableClassFromModule = stableClassName;
47
+ function mergeStableClass(input) {
48
+ if ('token' in input) {
49
+ return mergeSingle(input);
50
+ }
51
+ return mergeBatch(input);
52
+ }
53
+ function mergeSingle(input) {
54
+ const join = input.join ?? defaultJoin;
55
+ const hashed = toArray(input.hashed);
56
+ const stable = input.selector?.trim().length
57
+ ? input.selector
58
+ : stableClass(input.token, { namespace: input.namespace });
59
+ return join([...hashed, stable]);
60
+ }
61
+ function mergeBatch(input) {
62
+ const join = input.join ?? defaultJoin;
63
+ const output = {};
64
+ for (const key of Object.keys(input.hashed)) {
65
+ const hashedValue = input.hashed[key];
66
+ const selector = input.selectors?.[String(key)];
67
+ const stable = selector?.trim().length
68
+ ? selector
69
+ : stableClass(String(key), { namespace: input.namespace });
70
+ output[key] = join([...toArray(hashedValue), stable]);
71
+ }
72
+ return output;
73
+ }
@@ -5,9 +5,24 @@ export interface StableClassNameOptions extends StableSelectorOptions {
5
5
  token?: string;
6
6
  join?: (values: string[]) => string;
7
7
  }
8
+ export interface MergeStableClassSingleInput extends StableSelectorOptions {
9
+ hashed: string | string[];
10
+ selector?: string;
11
+ token: string;
12
+ join?: (values: string[]) => string;
13
+ }
14
+ export interface MergeStableClassBatchInput<Hashed extends Record<string, string | string[]>, Selectors extends Record<string, string> | undefined = Record<string, string>> extends StableSelectorOptions {
15
+ hashed: Hashed;
16
+ selectors?: Selectors;
17
+ join?: (values: string[]) => string;
18
+ }
8
19
  export declare function stableToken(token: string, options?: StableSelectorOptions): string;
9
20
  export declare function stableClass(token: string, options?: StableSelectorOptions): string;
10
21
  export declare function stableSelector(token: string, options?: StableSelectorOptions): string;
11
22
  export declare function createStableClassFactory(options?: StableSelectorOptions): (token: string) => string;
12
23
  export declare function stableClassName<T extends Record<string, string>>(styles: T, key: keyof T | string, options?: StableClassNameOptions): string;
13
24
  export declare const stableClassFromModule: typeof stableClassName;
25
+ export declare function mergeStableClass(input: MergeStableClassSingleInput): string;
26
+ export declare function mergeStableClass<Hashed extends Record<string, string | string[]>>(input: MergeStableClassBatchInput<Hashed>): {
27
+ [Key in keyof Hashed]: string;
28
+ };
@@ -3,38 +3,30 @@ import { moduleType } from 'node-module-type';
3
3
  import { getTsconfig } from 'get-tsconfig';
4
4
  import { type MatchPath } from 'tsconfig-paths';
5
5
  import { cssWithMeta } from './css.js';
6
- import { type SelectorTypeVariant } from './loaderInternals.js';
7
- interface ManifestEntry {
8
- file: string;
9
- hash: string;
10
- }
11
- type Manifest = Record<string, ManifestEntry>;
12
6
  interface ImportMatch {
13
7
  specifier: string;
14
8
  importer: string;
15
9
  }
16
- interface DeclarationRecord {
17
- specifier: string;
18
- filePath: string;
10
+ interface ManifestEntry {
11
+ file: string;
12
+ hash: string;
19
13
  }
14
+ type SelectorModuleManifest = Record<string, ManifestEntry>;
20
15
  interface TsconfigResolutionContext {
21
16
  absoluteBaseUrl?: string;
22
17
  matchPath?: MatchPath;
23
18
  }
24
19
  type CssWithMetaFn = typeof cssWithMeta;
25
20
  export interface GenerateTypesResult {
26
- written: number;
27
- removed: number;
28
- declarations: DeclarationRecord[];
21
+ selectorModulesWritten: number;
22
+ selectorModulesRemoved: number;
29
23
  warnings: string[];
30
- outDir: string;
31
- typesIndexPath: string;
24
+ manifestPath: string;
32
25
  }
33
26
  export interface GenerateTypesOptions {
34
27
  rootDir?: string;
35
28
  include?: string[];
36
29
  outDir?: string;
37
- typesRoot?: string;
38
30
  stableNamespace?: string;
39
31
  }
40
32
  type ModuleTypeDetector = () => ReturnType<typeof moduleType>;
@@ -48,14 +40,16 @@ declare function splitResourceAndQuery(specifier: string): {
48
40
  resource: string;
49
41
  query: string;
50
42
  };
43
+ declare function extractSelectorSourceSpecifier(specifier: string): string | undefined;
51
44
  declare function resolveImportPath(resourceSpecifier: string, importerPath: string, rootDir: string, tsconfig?: TsconfigResolutionContext): Promise<string | undefined>;
52
- declare function buildDeclarationFileName(specifier: string): string;
53
- declare function formatModuleDeclaration(specifier: string, variant: SelectorTypeVariant, selectors: Map<string, string>): string;
54
- declare function formatSelectorType(selectors: Map<string, string>): string;
55
- declare function buildDeclarationModuleSpecifier(resolvedPath: string, declarationDir: string, query: string): string;
56
- declare function buildCanonicalQuery(query: string): string;
57
- declare function writeTypesIndex(indexPath: string, manifest: Manifest, outDir: string): Promise<void>;
45
+ declare function buildSelectorModuleManifestKey(resolvedPath: string): string;
46
+ declare function buildSelectorModulePath(resolvedPath: string): string;
47
+ declare function formatSelectorModuleSource(selectors: Map<string, string>): string;
48
+ declare function readManifest(manifestPath: string): Promise<SelectorModuleManifest>;
49
+ declare function writeManifest(manifestPath: string, manifest: SelectorModuleManifest): Promise<void>;
50
+ declare function removeStaleSelectorModules(previous: SelectorModuleManifest, next: SelectorModuleManifest): Promise<number>;
58
51
  declare function relativeToRoot(filePath: string, rootDir: string): string;
52
+ declare function ensureSelectorModule(resolvedPath: string, selectors: Map<string, string>, previousManifest: SelectorModuleManifest, nextManifest: SelectorModuleManifest): Promise<boolean>;
59
53
  declare function resolveWithTsconfigPaths(specifier: string, tsconfig?: TsconfigResolutionContext): Promise<string | undefined>;
60
54
  declare function loadTsconfigResolutionContext(rootDir: string, loader?: typeof getTsconfig): TsconfigResolutionContext | undefined;
61
55
  declare function normalizeTsconfigPaths(paths: Record<string, string[] | string> | undefined): Record<string, string[]> | undefined;
@@ -67,7 +61,6 @@ export interface ParsedCliArgs {
67
61
  rootDir: string;
68
62
  include?: string[];
69
63
  outDir?: string;
70
- typesRoot?: string;
71
64
  stableNamespace?: string;
72
65
  help?: boolean;
73
66
  }
@@ -78,17 +71,12 @@ declare function setCssWithMetaImplementation(impl?: CssWithMetaFn): void;
78
71
  declare function setModuleTypeDetector(detector?: ModuleTypeDetector): void;
79
72
  declare function setImportMetaUrlProvider(provider?: () => string | undefined): void;
80
73
  export declare const __generateTypesInternals: {
81
- writeTypesIndex: typeof writeTypesIndex;
82
74
  stripInlineLoader: typeof stripInlineLoader;
83
75
  splitResourceAndQuery: typeof splitResourceAndQuery;
76
+ extractSelectorSourceSpecifier: typeof extractSelectorSourceSpecifier;
84
77
  findSpecifierImports: typeof findSpecifierImports;
85
78
  resolveImportPath: typeof resolveImportPath;
86
79
  resolvePackageRoot: typeof resolvePackageRoot;
87
- buildDeclarationFileName: typeof buildDeclarationFileName;
88
- formatModuleDeclaration: typeof formatModuleDeclaration;
89
- formatSelectorType: typeof formatSelectorType;
90
- buildDeclarationModuleSpecifier: typeof buildDeclarationModuleSpecifier;
91
- buildCanonicalQuery: typeof buildCanonicalQuery;
92
80
  relativeToRoot: typeof relativeToRoot;
93
81
  collectCandidateFiles: typeof collectCandidateFiles;
94
82
  normalizeIncludeOptions: typeof normalizeIncludeOptions;
@@ -104,5 +92,12 @@ export declare const __generateTypesInternals: {
104
92
  parseCliArgs: typeof parseCliArgs;
105
93
  printHelp: typeof printHelp;
106
94
  reportCliResult: typeof reportCliResult;
95
+ buildSelectorModuleManifestKey: typeof buildSelectorModuleManifestKey;
96
+ buildSelectorModulePath: typeof buildSelectorModulePath;
97
+ formatSelectorModuleSource: typeof formatSelectorModuleSource;
98
+ ensureSelectorModule: typeof ensureSelectorModule;
99
+ removeStaleSelectorModules: typeof removeStaleSelectorModules;
100
+ readManifest: typeof readManifest;
101
+ writeManifest: typeof writeManifest;
107
102
  };
108
103
  export {};
@@ -8,7 +8,6 @@ import { moduleType } from 'node-module-type';
8
8
  import { getTsconfig } from 'get-tsconfig';
9
9
  import { createMatchPath } from 'tsconfig-paths';
10
10
  import { cssWithMeta } from './css.js';
11
- import { determineSelectorVariant, hasQueryFlag, TYPES_QUERY_FLAG, buildSanitizedQuery, COMBINED_QUERY_FLAG, NAMED_ONLY_QUERY_FLAGS, } from './loaderInternals.js';
12
11
  import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js';
13
12
  import { resolveStableNamespace } from './stableNamespace.js';
14
13
  let activeCssWithMeta = cssWithMeta;
@@ -57,22 +56,19 @@ function getImportMetaUrl() {
57
56
  }
58
57
  }
59
58
  const PACKAGE_ROOT = resolvePackageRoot();
60
- const DEFAULT_TYPES_ROOT = path.join(PACKAGE_ROOT, 'types-stub');
61
- const DEFAULT_OUT_DIR = path.join(PACKAGE_ROOT, 'node_modules', '.knighted-css');
59
+ const SELECTOR_REFERENCE = '.knighted-css';
60
+ const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts';
62
61
  export async function generateTypes(options = {}) {
63
62
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
64
63
  const include = normalizeIncludeOptions(options.include, rootDir);
65
- const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR);
66
- const typesRoot = path.resolve(options.typesRoot ?? DEFAULT_TYPES_ROOT);
64
+ const cacheDir = path.resolve(options.outDir ?? path.join(rootDir, '.knighted-css'));
67
65
  const tsconfig = loadTsconfigResolutionContext(rootDir);
68
66
  await init;
69
- await fs.mkdir(outDir, { recursive: true });
70
- await fs.mkdir(typesRoot, { recursive: true });
67
+ await fs.mkdir(cacheDir, { recursive: true });
71
68
  const internalOptions = {
72
69
  rootDir,
73
70
  include,
74
- outDir,
75
- typesRoot,
71
+ cacheDir,
76
72
  stableNamespace: options.stableNamespace,
77
73
  tsconfig,
78
74
  };
@@ -81,29 +77,27 @@ export async function generateTypes(options = {}) {
81
77
  async function generateDeclarations(options) {
82
78
  const peerResolver = createProjectPeerResolver(options.rootDir);
83
79
  const files = await collectCandidateFiles(options.include);
84
- const manifestPath = path.join(options.outDir, 'manifest.json');
85
- const previousManifest = await readManifest(manifestPath);
86
- const nextManifest = {};
80
+ const selectorModulesManifestPath = path.join(options.cacheDir, 'selector-modules.json');
81
+ const previousSelectorManifest = await readManifest(selectorModulesManifestPath);
82
+ const nextSelectorManifest = {};
87
83
  const selectorCache = new Map();
88
- const processedSpecifiers = new Set();
89
- const declarations = [];
84
+ const processedSelectors = new Set();
90
85
  const warnings = [];
91
- let writes = 0;
86
+ let selectorModuleWrites = 0;
92
87
  for (const filePath of files) {
93
88
  const matches = await findSpecifierImports(filePath);
94
89
  for (const match of matches) {
95
90
  const cleaned = match.specifier.trim();
96
91
  const inlineFree = stripInlineLoader(cleaned);
97
- if (!inlineFree.includes('?knighted-css'))
98
- continue;
99
- const { resource, query } = splitResourceAndQuery(inlineFree);
100
- if (!query || !hasQueryFlag(query, TYPES_QUERY_FLAG)) {
92
+ const { resource } = splitResourceAndQuery(inlineFree);
93
+ const selectorSource = extractSelectorSourceSpecifier(resource);
94
+ if (!selectorSource) {
101
95
  continue;
102
96
  }
103
97
  const resolvedNamespace = resolveStableNamespace(options.stableNamespace);
104
- const resolvedPath = await resolveImportPath(resource, match.importer, options.rootDir, options.tsconfig);
98
+ const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig);
105
99
  if (!resolvedPath) {
106
- warnings.push(`Unable to resolve ${resource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
100
+ warnings.push(`Unable to resolve ${selectorSource} referenced by ${relativeToRoot(match.importer, options.rootDir)}.`);
107
101
  continue;
108
102
  }
109
103
  const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
@@ -127,42 +121,28 @@ async function generateDeclarations(options) {
127
121
  }
128
122
  selectorCache.set(cacheKey, selectorMap);
129
123
  }
130
- const canonicalSpecifier = buildDeclarationModuleSpecifier(resolvedPath, options.outDir, query);
131
- if (processedSpecifiers.has(canonicalSpecifier)) {
124
+ if (!isWithinRoot(resolvedPath, options.rootDir)) {
125
+ warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
132
126
  continue;
133
127
  }
134
- const variant = determineSelectorVariant(query);
135
- const declaration = formatModuleDeclaration(canonicalSpecifier, variant, selectorMap);
136
- const declarationHash = hashContent(declaration);
137
- const fileName = buildDeclarationFileName(canonicalSpecifier);
138
- const targetPath = path.join(options.outDir, fileName);
139
- const previousEntry = previousManifest[canonicalSpecifier];
140
- const needsWrite = previousEntry?.hash !== declarationHash || !(await fileExists(targetPath));
141
- if (needsWrite) {
142
- await fs.writeFile(targetPath, declaration, 'utf8');
143
- writes += 1;
128
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
129
+ if (processedSelectors.has(manifestKey)) {
130
+ continue;
144
131
  }
145
- nextManifest[canonicalSpecifier] = { file: fileName, hash: declarationHash };
146
- if (needsWrite) {
147
- declarations.push({ specifier: canonicalSpecifier, filePath: targetPath });
132
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest);
133
+ if (moduleWrite) {
134
+ selectorModuleWrites += 1;
148
135
  }
149
- processedSpecifiers.add(canonicalSpecifier);
136
+ processedSelectors.add(manifestKey);
150
137
  }
151
138
  }
152
- const removed = await removeStaleDeclarations(previousManifest, nextManifest, options.outDir);
153
- await writeManifest(manifestPath, nextManifest);
154
- const typesIndexPath = path.join(options.typesRoot, 'index.d.ts');
155
- await writeTypesIndex(typesIndexPath, nextManifest, options.outDir);
156
- if (Object.keys(nextManifest).length === 0) {
157
- declarations.length = 0;
158
- }
139
+ const selectorModulesRemoved = await removeStaleSelectorModules(previousSelectorManifest, nextSelectorManifest);
140
+ await writeManifest(selectorModulesManifestPath, nextSelectorManifest);
159
141
  return {
160
- written: writes,
161
- removed,
162
- declarations,
142
+ selectorModulesWritten: selectorModuleWrites,
143
+ selectorModulesRemoved,
163
144
  warnings,
164
- outDir: options.outDir,
165
- typesIndexPath,
145
+ manifestPath: selectorModulesManifestPath,
166
146
  };
167
147
  }
168
148
  function normalizeIncludeOptions(include, rootDir) {
@@ -220,18 +200,18 @@ async function findSpecifierImports(filePath) {
220
200
  catch {
221
201
  return [];
222
202
  }
223
- if (!source.includes('?knighted-css')) {
203
+ if (!source.includes(SELECTOR_REFERENCE)) {
224
204
  return [];
225
205
  }
226
206
  const matches = [];
227
207
  const [imports] = parse(source, filePath);
228
208
  for (const record of imports) {
229
209
  const specifier = record.n ?? source.slice(record.s, record.e);
230
- if (specifier && specifier.includes('?knighted-css')) {
210
+ if (specifier && specifier.includes(SELECTOR_REFERENCE)) {
231
211
  matches.push({ specifier, importer: filePath });
232
212
  }
233
213
  }
234
- const requireRegex = /require\((['"])([^'"`]+?\?knighted-css[^'"`]*)\1\)/g;
214
+ const requireRegex = /require\((['"])([^'"`]+?\.knighted-css[^'"`]*)\1\)/g;
235
215
  let reqMatch;
236
216
  while ((reqMatch = requireRegex.exec(source)) !== null) {
237
217
  const spec = reqMatch[2];
@@ -254,6 +234,21 @@ function splitResourceAndQuery(specifier) {
254
234
  }
255
235
  return { resource: trimmed.slice(0, queryIndex), query: trimmed.slice(queryIndex) };
256
236
  }
237
+ function extractSelectorSourceSpecifier(specifier) {
238
+ const markerIndex = specifier.indexOf(SELECTOR_REFERENCE);
239
+ if (markerIndex < 0) {
240
+ return undefined;
241
+ }
242
+ const suffix = specifier.slice(markerIndex + SELECTOR_REFERENCE.length);
243
+ if (suffix.length > 0 && !/\.(?:[cm]?[tj]s|[tj]sx)$/.test(suffix)) {
244
+ return undefined;
245
+ }
246
+ const base = specifier.slice(0, markerIndex);
247
+ if (!base) {
248
+ return undefined;
249
+ }
250
+ return base;
251
+ }
257
252
  const projectRequireCache = new Map();
258
253
  async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tsconfig) {
259
254
  if (!resourceSpecifier)
@@ -276,91 +271,29 @@ async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tscon
276
271
  return undefined;
277
272
  }
278
273
  }
279
- function buildDeclarationFileName(specifier) {
280
- const digest = crypto.createHash('sha1').update(specifier).digest('hex').slice(0, 12);
281
- return `knt-${digest}.d.ts`;
282
- }
283
- function formatModuleDeclaration(specifier, variant, selectors) {
284
- const literalSpecifier = JSON.stringify(specifier);
285
- const selectorType = formatSelectorType(selectors);
286
- const header = `declare module ${literalSpecifier} {`;
287
- const footer = '}';
288
- if (variant === 'types') {
289
- return `${header}
290
- export const knightedCss: string
291
- export const stableSelectors: ${selectorType}
292
- ${footer}
293
- `;
294
- }
295
- const stableLine = ` export const stableSelectors: ${selectorType}`;
296
- const shared = ` const combined: KnightedCssCombinedModule<Record<string, unknown>>
297
- export const knightedCss: string
298
- ${stableLine}`;
299
- if (variant === 'combined') {
300
- return `${header}
301
- ${shared}
302
- export default combined
303
- ${footer}
304
- `;
305
- }
306
- return `${header}
307
- ${shared}
308
- ${footer}
309
- `;
274
+ function buildSelectorModuleManifestKey(resolvedPath) {
275
+ return resolvedPath.split(path.sep).join('/');
310
276
  }
311
- function formatSelectorType(selectors) {
312
- if (selectors.size === 0) {
313
- return 'Readonly<Record<string, string>>';
314
- }
277
+ function buildSelectorModulePath(resolvedPath) {
278
+ return `${resolvedPath}${SELECTOR_MODULE_SUFFIX}`;
279
+ }
280
+ function formatSelectorModuleSource(selectors) {
281
+ const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
315
282
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
316
- const lines = entries.map(([token, selector]) => ` readonly ${JSON.stringify(token)}: ${JSON.stringify(selector)}`);
317
- return `Readonly<{
283
+ const lines = entries.map(([token, selector]) => ` ${JSON.stringify(token)}: ${JSON.stringify(selector)},`);
284
+ const literal = lines.length > 0
285
+ ? `{
318
286
  ${lines.join('\n')}
319
- }>`;
320
- }
321
- function buildDeclarationModuleSpecifier(resolvedPath, declarationDir, query) {
322
- const relativePath = path.relative(declarationDir, resolvedPath);
323
- const normalizedPath = normalizeRelativePath(relativePath);
324
- const canonicalQuery = buildCanonicalQuery(query);
325
- return `${normalizedPath}${canonicalQuery}`;
326
- }
327
- function normalizeRelativePath(relativePath) {
328
- let normalized = relativePath.split(path.sep).join('/');
329
- if (!normalized || normalized === '') {
330
- normalized = '.';
331
- }
332
- if (normalized === '.') {
333
- return './';
334
- }
335
- if (normalized.startsWith('./') || normalized.startsWith('../')) {
336
- return normalized;
337
- }
338
- if (normalized.startsWith('.')) {
339
- return normalized;
340
- }
341
- return `./${normalized}`;
342
- }
343
- function buildCanonicalQuery(query) {
344
- if (!query) {
345
- return '';
346
- }
347
- const sanitized = buildSanitizedQuery(query);
348
- const extraParts = sanitized ? sanitized.slice(1).split('&').filter(Boolean) : [];
349
- const parts = [];
350
- parts.push('knighted-css');
351
- if (hasQueryFlag(query, COMBINED_QUERY_FLAG)) {
352
- parts.push(COMBINED_QUERY_FLAG);
353
- }
354
- for (const flag of NAMED_ONLY_QUERY_FLAGS) {
355
- if (hasQueryFlag(query, flag)) {
356
- parts.push(flag);
357
- }
358
- }
359
- if (hasQueryFlag(query, TYPES_QUERY_FLAG)) {
360
- parts.push(TYPES_QUERY_FLAG);
361
- }
362
- const merged = [...parts, ...extraParts];
363
- return merged.length > 0 ? `?${merged.join('&')}` : '';
287
+ } as const`
288
+ : '{} as const';
289
+ return `${header}
290
+ export const stableSelectors = ${literal}
291
+
292
+ export type KnightedCssStableSelectors = typeof stableSelectors
293
+ export type KnightedCssStableSelectorToken = keyof typeof stableSelectors
294
+
295
+ export default stableSelectors
296
+ `;
364
297
  }
365
298
  function hashContent(content) {
366
299
  return crypto.createHash('sha1').update(content).digest('hex');
@@ -377,13 +310,12 @@ async function readManifest(manifestPath) {
377
310
  async function writeManifest(manifestPath, manifest) {
378
311
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
379
312
  }
380
- async function removeStaleDeclarations(previous, next, outDir) {
381
- const stale = Object.entries(previous).filter(([specifier]) => !next[specifier]);
313
+ async function removeStaleSelectorModules(previous, next) {
314
+ const stale = Object.entries(previous).filter(([key]) => !next[key]);
382
315
  let removed = 0;
383
316
  for (const [, entry] of stale) {
384
- const targetPath = path.join(outDir, entry.file);
385
317
  try {
386
- await fs.unlink(targetPath);
318
+ await fs.unlink(entry.file);
387
319
  removed += 1;
388
320
  }
389
321
  catch {
@@ -392,25 +324,6 @@ async function removeStaleDeclarations(previous, next, outDir) {
392
324
  }
393
325
  return removed;
394
326
  }
395
- async function writeTypesIndex(indexPath, manifest, outDir) {
396
- const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
397
- const references = Object.values(manifest)
398
- .sort((a, b) => a.file.localeCompare(b.file))
399
- .map(entry => {
400
- const rel = path
401
- .relative(path.dirname(indexPath), path.join(outDir, entry.file))
402
- .split(path.sep)
403
- .join('/');
404
- return `/// <reference path="${rel}" />`;
405
- });
406
- const content = references.length > 0
407
- ? `${header}
408
- ${references.join('\n')}
409
- `
410
- : `${header}
411
- `;
412
- await fs.writeFile(indexPath, content, 'utf8');
413
- }
414
327
  function formatErrorMessage(error) {
415
328
  if (error instanceof Error && typeof error.message === 'string') {
416
329
  return error.message;
@@ -420,6 +333,23 @@ function formatErrorMessage(error) {
420
333
  function relativeToRoot(filePath, rootDir) {
421
334
  return path.relative(rootDir, filePath) || filePath;
422
335
  }
336
+ function isWithinRoot(filePath, rootDir) {
337
+ const relative = path.relative(rootDir, filePath);
338
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
339
+ }
340
+ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest) {
341
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
342
+ const targetPath = buildSelectorModulePath(resolvedPath);
343
+ const source = formatSelectorModuleSource(selectors);
344
+ const hash = hashContent(source);
345
+ const previousEntry = previousManifest[manifestKey];
346
+ const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
347
+ if (needsWrite) {
348
+ await fs.writeFile(targetPath, source, 'utf8');
349
+ }
350
+ nextManifest[manifestKey] = { file: targetPath, hash };
351
+ return needsWrite;
352
+ }
423
353
  async function fileExists(target) {
424
354
  try {
425
355
  await fs.access(target);
@@ -543,7 +473,6 @@ export async function runGenerateTypesCli(argv = process.argv.slice(2)) {
543
473
  rootDir: parsed.rootDir,
544
474
  include: parsed.include,
545
475
  outDir: parsed.outDir,
546
- typesRoot: parsed.typesRoot,
547
476
  stableNamespace: parsed.stableNamespace,
548
477
  });
549
478
  reportCliResult(result);
@@ -558,12 +487,11 @@ function parseCliArgs(argv) {
558
487
  let rootDir = process.cwd();
559
488
  const include = [];
560
489
  let outDir;
561
- let typesRoot;
562
490
  let stableNamespace;
563
491
  for (let i = 0; i < argv.length; i += 1) {
564
492
  const arg = argv[i];
565
493
  if (arg === '--help' || arg === '-h') {
566
- return { rootDir, include, outDir, typesRoot, stableNamespace, help: true };
494
+ return { rootDir, include, outDir, stableNamespace, help: true };
567
495
  }
568
496
  if (arg === '--root' || arg === '-r') {
569
497
  const value = argv[++i];
@@ -589,14 +517,6 @@ function parseCliArgs(argv) {
589
517
  outDir = value;
590
518
  continue;
591
519
  }
592
- if (arg === '--types-root') {
593
- const value = argv[++i];
594
- if (!value) {
595
- throw new Error('Missing value for --types-root');
596
- }
597
- typesRoot = value;
598
- continue;
599
- }
600
520
  if (arg === '--stable-namespace') {
601
521
  const value = argv[++i];
602
522
  if (!value) {
@@ -610,7 +530,7 @@ function parseCliArgs(argv) {
610
530
  }
611
531
  include.push(arg);
612
532
  }
613
- return { rootDir, include, outDir, typesRoot, stableNamespace };
533
+ return { rootDir, include, outDir, stableNamespace };
614
534
  }
615
535
  function printHelp() {
616
536
  console.log(`Usage: knighted-css-generate-types [options]
@@ -618,20 +538,19 @@ function printHelp() {
618
538
  Options:
619
539
  -r, --root <path> Project root directory (default: cwd)
620
540
  -i, --include <path> Additional directories/files to scan (repeatable)
621
- --out-dir <path> Output directory for generated declarations
622
- --types-root <path> Directory for generated @types entrypoint
541
+ --out-dir <path> Directory to store selector module manifest cache
623
542
  --stable-namespace <name> Stable namespace prefix for generated selector maps
624
543
  -h, --help Show this help message
625
544
  `);
626
545
  }
627
546
  function reportCliResult(result) {
628
- if (result.written === 0 && result.removed === 0) {
629
- console.log('[knighted-css] No changes to ?knighted-css&types declarations (cache is up to date).');
547
+ if (result.selectorModulesWritten === 0 && result.selectorModulesRemoved === 0) {
548
+ console.log('[knighted-css] Selector modules are up to date.');
630
549
  }
631
550
  else {
632
- console.log(`[knighted-css] Updated ${result.written} declaration(s), removed ${result.removed}, output in ${result.outDir}.`);
551
+ console.log(`[knighted-css] Selector modules updated: wrote ${result.selectorModulesWritten}, removed ${result.selectorModulesRemoved}.`);
633
552
  }
634
- console.log(`[knighted-css] Type references: ${result.typesIndexPath}`);
553
+ console.log(`[knighted-css] Manifest: ${result.manifestPath}`);
635
554
  for (const warning of result.warnings) {
636
555
  console.warn(`[knighted-css] ${warning}`);
637
556
  }
@@ -646,17 +565,12 @@ function setImportMetaUrlProvider(provider) {
646
565
  importMetaUrlProvider = provider ?? getImportMetaUrl;
647
566
  }
648
567
  export const __generateTypesInternals = {
649
- writeTypesIndex,
650
568
  stripInlineLoader,
651
569
  splitResourceAndQuery,
570
+ extractSelectorSourceSpecifier,
652
571
  findSpecifierImports,
653
572
  resolveImportPath,
654
573
  resolvePackageRoot,
655
- buildDeclarationFileName,
656
- formatModuleDeclaration,
657
- formatSelectorType,
658
- buildDeclarationModuleSpecifier,
659
- buildCanonicalQuery,
660
574
  relativeToRoot,
661
575
  collectCandidateFiles,
662
576
  normalizeIncludeOptions,
@@ -672,4 +586,11 @@ export const __generateTypesInternals = {
672
586
  parseCliArgs,
673
587
  printHelp,
674
588
  reportCliResult,
589
+ buildSelectorModuleManifestKey,
590
+ buildSelectorModulePath,
591
+ formatSelectorModuleSource,
592
+ ensureSelectorModule,
593
+ removeStaleSelectorModules,
594
+ readManifest,
595
+ writeManifest,
675
596
  };
@@ -5,9 +5,24 @@ export interface StableClassNameOptions extends StableSelectorOptions {
5
5
  token?: string;
6
6
  join?: (values: string[]) => string;
7
7
  }
8
+ export interface MergeStableClassSingleInput extends StableSelectorOptions {
9
+ hashed: string | string[];
10
+ selector?: string;
11
+ token: string;
12
+ join?: (values: string[]) => string;
13
+ }
14
+ export interface MergeStableClassBatchInput<Hashed extends Record<string, string | string[]>, Selectors extends Record<string, string> | undefined = Record<string, string>> extends StableSelectorOptions {
15
+ hashed: Hashed;
16
+ selectors?: Selectors;
17
+ join?: (values: string[]) => string;
18
+ }
8
19
  export declare function stableToken(token: string, options?: StableSelectorOptions): string;
9
20
  export declare function stableClass(token: string, options?: StableSelectorOptions): string;
10
21
  export declare function stableSelector(token: string, options?: StableSelectorOptions): string;
11
22
  export declare function createStableClassFactory(options?: StableSelectorOptions): (token: string) => string;
12
23
  export declare function stableClassName<T extends Record<string, string>>(styles: T, key: keyof T | string, options?: StableClassNameOptions): string;
13
24
  export declare const stableClassFromModule: typeof stableClassName;
25
+ export declare function mergeStableClass(input: MergeStableClassSingleInput): string;
26
+ export declare function mergeStableClass<Hashed extends Record<string, string | string[]>>(input: MergeStableClassBatchInput<Hashed>): {
27
+ [Key in keyof Hashed]: string;
28
+ };
@@ -1,5 +1,6 @@
1
1
  const DEFAULT_NAMESPACE = 'knighted';
2
2
  const defaultJoin = (values) => values.filter(Boolean).join(' ');
3
+ const toArray = (value) => Array.isArray(value) ? value : [value];
3
4
  const normalizeToken = (token) => {
4
5
  const sanitized = token
5
6
  .trim()
@@ -34,3 +35,30 @@ export function stableClassName(styles, key, options) {
34
35
  return join([hashed, stable]);
35
36
  }
36
37
  export const stableClassFromModule = stableClassName;
38
+ export function mergeStableClass(input) {
39
+ if ('token' in input) {
40
+ return mergeSingle(input);
41
+ }
42
+ return mergeBatch(input);
43
+ }
44
+ function mergeSingle(input) {
45
+ const join = input.join ?? defaultJoin;
46
+ const hashed = toArray(input.hashed);
47
+ const stable = input.selector?.trim().length
48
+ ? input.selector
49
+ : stableClass(input.token, { namespace: input.namespace });
50
+ return join([...hashed, stable]);
51
+ }
52
+ function mergeBatch(input) {
53
+ const join = input.join ?? defaultJoin;
54
+ const output = {};
55
+ for (const key of Object.keys(input.hashed)) {
56
+ const hashedValue = input.hashed[key];
57
+ const selector = input.selectors?.[String(key)];
58
+ const stable = selector?.trim().length
59
+ ? selector
60
+ : stableClass(String(key), { namespace: input.namespace });
61
+ output[key] = join([...toArray(hashedValue), stable]);
62
+ }
63
+ return output;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/css",
3
- "version": "1.0.0-rc.11",
3
+ "version": "1.0.0-rc.13",
4
4
  "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.",
5
5
  "type": "module",
6
6
  "main": "./dist/css.js",