@knighted/css 1.0.10 → 1.1.0-rc.1

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.
@@ -1,19 +1,23 @@
1
1
  import { type TransformOptions as LightningTransformOptions } from 'lightningcss';
2
2
  import { type SpecificitySelector, type SpecificityStrategy } from './helpers.cjs';
3
+ import { type AutoStableOption } from './autoStableSelectors.cjs';
3
4
  import type { ModuleGraphOptions } from './moduleGraph.cjs';
4
5
  import type { CssResolver } from './types.cjs';
6
+ export type { AutoStableOption } from './autoStableSelectors.cjs';
5
7
  export type { CssResolver } from './types.cjs';
6
8
  export type { ModuleGraphOptions } from './moduleGraph.cjs';
7
9
  export declare const DEFAULT_EXTENSIONS: string[];
8
10
  type LightningCssConfig = boolean | Partial<Omit<LightningTransformOptions<never>, 'code'>>;
9
11
  type PeerLoader = (name: string) => Promise<unknown>;
12
+ type StrictLightningVisitor = Exclude<LightningTransformOptions<never>['visitor'], undefined>;
10
13
  export interface CssOptions {
11
14
  extensions?: string[];
12
15
  cwd?: string;
13
16
  filter?: (filePath: string) => boolean;
14
17
  lightningcss?: LightningCssConfig;
18
+ autoStable?: AutoStableOption;
15
19
  specificityBoost?: {
16
- visitor?: LightningTransformOptions<never>['visitor'];
20
+ visitor?: StrictLightningVisitor;
17
21
  strategy?: SpecificityStrategy;
18
22
  match?: SpecificitySelector[];
19
23
  };
@@ -31,6 +35,7 @@ export interface VanillaCompileResult {
31
35
  export interface CssResult {
32
36
  css: string;
33
37
  files: string[];
38
+ exports?: Record<string, string | string[]>;
34
39
  }
35
40
  export declare function css(entry: string, options?: CssOptions): Promise<string>;
36
41
  export declare function cssWithMeta(entry: string, options?: CssOptions): Promise<CssResult>;
@@ -65,6 +65,13 @@ function getImportMetaUrl() {
65
65
  }
66
66
  const SELECTOR_REFERENCE = '.knighted-css';
67
67
  const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts';
68
+ const STYLE_EXTENSIONS = css_js_1.DEFAULT_EXTENSIONS.map(ext => ext.toLowerCase());
69
+ const EXTENSION_FALLBACKS = {
70
+ '.js': ['.ts', '.tsx', '.jsx', '.mjs', '.cjs'],
71
+ '.mjs': ['.mts', '.mjs', '.js', '.ts', '.tsx'],
72
+ '.cjs': ['.cts', '.cjs', '.js', '.ts', '.tsx'],
73
+ '.jsx': ['.tsx', '.jsx'],
74
+ };
68
75
  async function generateTypes(options = {}) {
69
76
  const rootDir = node_path_1.default.resolve(options.rootDir ?? process.cwd());
70
77
  const include = normalizeIncludeOptions(options.include, rootDir);
@@ -76,6 +83,7 @@ async function generateTypes(options = {}) {
76
83
  include,
77
84
  cacheDir,
78
85
  stableNamespace: options.stableNamespace,
86
+ autoStable: options.autoStable,
79
87
  tsconfig,
80
88
  };
81
89
  return generateDeclarations(internalOptions);
@@ -88,6 +96,7 @@ async function generateDeclarations(options) {
88
96
  const nextSelectorManifest = {};
89
97
  const selectorCache = new Map();
90
98
  const processedSelectors = new Set();
99
+ const proxyInfoCache = new Map();
91
100
  const warnings = [];
92
101
  let selectorModuleWrites = 0;
93
102
  for (const filePath of files) {
@@ -110,9 +119,14 @@ async function generateDeclarations(options) {
110
119
  let selectorMap = selectorCache.get(cacheKey);
111
120
  if (!selectorMap) {
112
121
  try {
122
+ const shouldUseCssModules = resolvedPath.endsWith('.module.css');
113
123
  const { css } = await activeCssWithMeta(resolvedPath, {
114
124
  cwd: options.rootDir,
115
125
  peerResolver,
126
+ autoStable: options.autoStable ? { namespace: resolvedNamespace } : undefined,
127
+ lightningcss: options.autoStable && shouldUseCssModules
128
+ ? { cssModules: true }
129
+ : undefined,
116
130
  });
117
131
  selectorMap = (0, stableSelectorsLiteral_js_1.buildStableSelectorsLiteral)({
118
132
  css,
@@ -135,7 +149,8 @@ async function generateDeclarations(options) {
135
149
  if (processedSelectors.has(manifestKey)) {
136
150
  continue;
137
151
  }
138
- const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest);
152
+ const proxyInfo = await resolveProxyInfo(manifestKey, selectorSource, resolvedPath, proxyInfoCache);
153
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest, proxyInfo ?? undefined);
139
154
  if (moduleWrite) {
140
155
  selectorModuleWrites += 1;
141
156
  }
@@ -257,6 +272,13 @@ function extractSelectorSourceSpecifier(specifier) {
257
272
  if (!base) {
258
273
  return undefined;
259
274
  }
275
+ /**
276
+ * Handles specifiers like "./entry.knighted-css.ts" where the base has no
277
+ * extension but the selector suffix includes one.
278
+ */
279
+ if (suffix && !node_path_1.default.extname(base)) {
280
+ return `${base}${suffix}`;
281
+ }
260
282
  return base;
261
283
  }
262
284
  const projectRequireCache = new Map();
@@ -264,14 +286,14 @@ async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tscon
264
286
  if (!resourceSpecifier)
265
287
  return undefined;
266
288
  if (resourceSpecifier.startsWith('.')) {
267
- return node_path_1.default.resolve(node_path_1.default.dirname(importerPath), resourceSpecifier);
289
+ return resolveWithExtensionFallback(node_path_1.default.resolve(node_path_1.default.dirname(importerPath), resourceSpecifier));
268
290
  }
269
291
  if (resourceSpecifier.startsWith('/')) {
270
- return node_path_1.default.resolve(rootDir, resourceSpecifier.slice(1));
292
+ return resolveWithExtensionFallback(node_path_1.default.resolve(rootDir, resourceSpecifier.slice(1)));
271
293
  }
272
294
  const tsconfigResolved = await resolveWithTsconfigPaths(resourceSpecifier, tsconfig);
273
295
  if (tsconfigResolved) {
274
- return tsconfigResolved;
296
+ return resolveWithExtensionFallback(tsconfigResolved);
275
297
  }
276
298
  const requireFromRoot = getProjectRequire(rootDir);
277
299
  try {
@@ -285,10 +307,15 @@ function buildSelectorModuleManifestKey(resolvedPath) {
285
307
  return resolvedPath.split(node_path_1.default.sep).join('/');
286
308
  }
287
309
  function buildSelectorModulePath(resolvedPath) {
288
- return `${resolvedPath}${SELECTOR_MODULE_SUFFIX}`;
310
+ if (isStyleResource(resolvedPath)) {
311
+ return `${resolvedPath}${SELECTOR_MODULE_SUFFIX}`;
312
+ }
313
+ const ext = node_path_1.default.extname(resolvedPath);
314
+ const base = ext ? resolvedPath.slice(0, -ext.length) : resolvedPath;
315
+ return `${base}${SELECTOR_MODULE_SUFFIX}`;
289
316
  }
290
- function formatSelectorModuleSource(selectors) {
291
- const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
317
+ function formatSelectorModuleSource(selectors, proxyInfo) {
318
+ const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n\n';
292
319
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
293
320
  const lines = entries.map(([token, selector]) => ` ${JSON.stringify(token)}: ${JSON.stringify(selector)},`);
294
321
  const literal = lines.length > 0
@@ -296,14 +323,21 @@ function formatSelectorModuleSource(selectors) {
296
323
  ${lines.join('\n')}
297
324
  } as const`
298
325
  : '{} as const';
299
- return `${header}
326
+ const proxyLines = [];
327
+ if (proxyInfo) {
328
+ proxyLines.push(`export * from '${proxyInfo.moduleSpecifier}'`);
329
+ if (proxyInfo.includeDefault) {
330
+ proxyLines.push(`export { default } from '${proxyInfo.moduleSpecifier}'`);
331
+ }
332
+ proxyLines.push(`export { knightedCss } from '${proxyInfo.moduleSpecifier}?knighted-css'`);
333
+ proxyLines.push('');
334
+ }
335
+ const defaultExport = proxyInfo ? '' : '\nexport default stableSelectors\n';
336
+ return `${header}${proxyLines.join('\n')}
300
337
  export const stableSelectors = ${literal}
301
338
 
302
339
  export type KnightedCssStableSelectors = typeof stableSelectors
303
- export type KnightedCssStableSelectorToken = keyof typeof stableSelectors
304
-
305
- export default stableSelectors
306
- `;
340
+ export type KnightedCssStableSelectorToken = keyof typeof stableSelectors${defaultExport}`;
307
341
  }
308
342
  function hashContent(content) {
309
343
  return node_crypto_1.default.createHash('sha1').update(content).digest('hex');
@@ -347,10 +381,10 @@ function isWithinRoot(filePath, rootDir) {
347
381
  const relative = node_path_1.default.relative(rootDir, filePath);
348
382
  return relative === '' || (!relative.startsWith('..') && !node_path_1.default.isAbsolute(relative));
349
383
  }
350
- async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest) {
384
+ async function ensureSelectorModule(resolvedPath, selectors, previousManifest, nextManifest, proxyInfo) {
351
385
  const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
352
386
  const targetPath = buildSelectorModulePath(resolvedPath);
353
- const source = formatSelectorModuleSource(selectors);
387
+ const source = formatSelectorModuleSource(selectors, proxyInfo);
354
388
  const hash = hashContent(source);
355
389
  const previousEntry = previousManifest[manifestKey];
356
390
  const needsWrite = previousEntry?.hash !== hash || !(await fileExists(targetPath));
@@ -387,6 +421,62 @@ async function resolveWithTsconfigPaths(specifier, tsconfig) {
387
421
  }
388
422
  return undefined;
389
423
  }
424
+ async function resolveWithExtensionFallback(candidatePath) {
425
+ try {
426
+ const stat = await promises_1.default.stat(candidatePath);
427
+ if (stat.isFile()) {
428
+ return candidatePath;
429
+ }
430
+ }
431
+ catch {
432
+ // continue to resolution fallbacks
433
+ }
434
+ const ext = node_path_1.default.extname(candidatePath);
435
+ const base = ext ? candidatePath.slice(0, -ext.length) : candidatePath;
436
+ if (!ext) {
437
+ const resolved = await resolveWithExtensionList(base, Array.from(SUPPORTED_EXTENSIONS));
438
+ if (resolved) {
439
+ return resolved;
440
+ }
441
+ }
442
+ if (ext && EXTENSION_FALLBACKS[ext]) {
443
+ const resolved = await resolveWithExtensionList(base, EXTENSION_FALLBACKS[ext]);
444
+ if (resolved) {
445
+ return resolved;
446
+ }
447
+ }
448
+ const indexResolved = await resolveIndexFallback(candidatePath);
449
+ if (indexResolved) {
450
+ return indexResolved;
451
+ }
452
+ /*
453
+ * Return the original candidate to preserve existing behavior when nothing
454
+ * resolves (callers may still want a best-effort path for warnings).
455
+ */
456
+ return candidatePath;
457
+ }
458
+ async function resolveWithExtensionList(base, extensions) {
459
+ for (const extension of extensions) {
460
+ const candidate = `${base}${extension}`;
461
+ if (await fileExists(candidate)) {
462
+ return candidate;
463
+ }
464
+ }
465
+ return undefined;
466
+ }
467
+ async function resolveIndexFallback(candidatePath) {
468
+ try {
469
+ const stat = await promises_1.default.stat(candidatePath);
470
+ if (!stat.isDirectory()) {
471
+ return undefined;
472
+ }
473
+ }
474
+ catch {
475
+ return undefined;
476
+ }
477
+ const base = node_path_1.default.join(candidatePath, 'index');
478
+ return resolveWithExtensionList(base, Array.from(SUPPORTED_EXTENSIONS));
479
+ }
390
480
  function loadTsconfigResolutionContext(rootDir, loader = get_tsconfig_1.getTsconfig) {
391
481
  let result;
392
482
  try {
@@ -441,6 +531,43 @@ function isNonRelativeSpecifier(specifier) {
441
531
  }
442
532
  return true;
443
533
  }
534
+ function isStyleResource(filePath) {
535
+ const normalized = filePath.toLowerCase();
536
+ return STYLE_EXTENSIONS.some(ext => normalized.endsWith(ext));
537
+ }
538
+ async function resolveProxyInfo(manifestKey, selectorSource, resolvedPath, cache) {
539
+ if (isStyleResource(resolvedPath)) {
540
+ return null;
541
+ }
542
+ const cached = cache.get(manifestKey);
543
+ if (cached !== undefined) {
544
+ return cached;
545
+ }
546
+ const defaultSignal = await getDefaultExportSignal(resolvedPath);
547
+ const proxyInfo = {
548
+ moduleSpecifier: buildProxyModuleSpecifier(resolvedPath, selectorSource),
549
+ includeDefault: defaultSignal === 'has-default',
550
+ };
551
+ cache.set(manifestKey, proxyInfo);
552
+ return proxyInfo;
553
+ }
554
+ function buildProxyModuleSpecifier(resolvedPath, selectorSource) {
555
+ const resolvedExt = node_path_1.default.extname(resolvedPath);
556
+ const baseName = node_path_1.default.basename(resolvedPath, resolvedExt);
557
+ const selectorExt = node_path_1.default.extname(selectorSource);
558
+ const fileName = selectorExt ? `${baseName}${selectorExt}` : `${baseName}.js`;
559
+ return `./${fileName}`;
560
+ }
561
+ async function getDefaultExportSignal(filePath) {
562
+ try {
563
+ const source = await promises_1.default.readFile(filePath, 'utf8');
564
+ const analysis = await (0, lexer_js_1.analyzeModule)(source, filePath);
565
+ return analysis.defaultSignal;
566
+ }
567
+ catch {
568
+ return 'unknown';
569
+ }
570
+ }
444
571
  function createProjectPeerResolver(rootDir) {
445
572
  const resolver = getProjectRequire(rootDir);
446
573
  return async (name) => {
@@ -484,6 +611,7 @@ async function runGenerateTypesCli(argv = process.argv.slice(2)) {
484
611
  include: parsed.include,
485
612
  outDir: parsed.outDir,
486
613
  stableNamespace: parsed.stableNamespace,
614
+ autoStable: parsed.autoStable,
487
615
  });
488
616
  reportCliResult(result);
489
617
  }
@@ -498,10 +626,15 @@ function parseCliArgs(argv) {
498
626
  const include = [];
499
627
  let outDir;
500
628
  let stableNamespace;
629
+ let autoStable = false;
501
630
  for (let i = 0; i < argv.length; i += 1) {
502
631
  const arg = argv[i];
503
632
  if (arg === '--help' || arg === '-h') {
504
- return { rootDir, include, outDir, stableNamespace, help: true };
633
+ return { rootDir, include, outDir, stableNamespace, autoStable, help: true };
634
+ }
635
+ if (arg === '--auto-stable') {
636
+ autoStable = true;
637
+ continue;
505
638
  }
506
639
  if (arg === '--root' || arg === '-r') {
507
640
  const value = argv[++i];
@@ -540,7 +673,7 @@ function parseCliArgs(argv) {
540
673
  }
541
674
  include.push(arg);
542
675
  }
543
- return { rootDir, include, outDir, stableNamespace };
676
+ return { rootDir, include, outDir, stableNamespace, autoStable };
544
677
  }
545
678
  function printHelp() {
546
679
  console.log(`Usage: knighted-css-generate-types [options]
@@ -550,6 +683,7 @@ Options:
550
683
  -i, --include <path> Additional directories/files to scan (repeatable)
551
684
  --out-dir <path> Directory to store selector module manifest cache
552
685
  --stable-namespace <name> Stable namespace prefix for generated selector maps
686
+ --auto-stable Enable autoStable when extracting CSS for selectors
553
687
  -h, --help Show this help message
554
688
  `);
555
689
  }
@@ -589,6 +723,8 @@ exports.__generateTypesInternals = {
589
723
  setModuleTypeDetector,
590
724
  setImportMetaUrlProvider,
591
725
  isNonRelativeSpecifier,
726
+ isStyleResource,
727
+ resolveWithExtensionFallback,
592
728
  createProjectPeerResolver,
593
729
  getProjectRequire,
594
730
  loadTsconfigResolutionContext,