@knighted/css 1.0.0-rc.9 → 1.0.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.
@@ -5,10 +5,12 @@ import { createRequire } from 'node:module';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { init, parse } from 'es-module-lexer';
7
7
  import { moduleType } from 'node-module-type';
8
+ import { getTsconfig } from 'get-tsconfig';
9
+ import { createMatchPath } from 'tsconfig-paths';
8
10
  import { cssWithMeta } from './css.js';
9
- import { determineSelectorVariant, hasQueryFlag, TYPES_QUERY_FLAG, } from './loaderInternals.js';
10
11
  import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js';
11
12
  import { resolveStableNamespace } from './stableNamespace.js';
13
+ let activeCssWithMeta = cssWithMeta;
12
14
  const DEFAULT_SKIP_DIRS = new Set([
13
15
  'node_modules',
14
16
  '.git',
@@ -32,12 +34,14 @@ const SUPPORTED_EXTENSIONS = new Set([
32
34
  '.mjs',
33
35
  '.cjs',
34
36
  ]);
37
+ let moduleTypeDetector = moduleType;
38
+ let importMetaUrlProvider = getImportMetaUrl;
35
39
  function resolvePackageRoot() {
36
- const detectedType = moduleType();
40
+ const detectedType = moduleTypeDetector();
37
41
  if (detectedType === 'commonjs' && typeof __dirname === 'string') {
38
42
  return path.resolve(__dirname, '..');
39
43
  }
40
- const moduleUrl = getImportMetaUrl();
44
+ const moduleUrl = importMetaUrlProvider();
41
45
  if (moduleUrl) {
42
46
  return path.resolve(path.dirname(fileURLToPath(moduleUrl)), '..');
43
47
  }
@@ -52,61 +56,55 @@ function getImportMetaUrl() {
52
56
  }
53
57
  }
54
58
  const PACKAGE_ROOT = resolvePackageRoot();
55
- const DEFAULT_TYPES_ROOT = path.join(PACKAGE_ROOT, 'types-stub');
56
- 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';
57
61
  export async function generateTypes(options = {}) {
58
62
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
59
63
  const include = normalizeIncludeOptions(options.include, rootDir);
60
- const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR);
61
- const typesRoot = path.resolve(options.typesRoot ?? DEFAULT_TYPES_ROOT);
64
+ const cacheDir = path.resolve(options.outDir ?? path.join(rootDir, '.knighted-css'));
65
+ const tsconfig = loadTsconfigResolutionContext(rootDir);
62
66
  await init;
63
- await fs.mkdir(outDir, { recursive: true });
64
- await fs.mkdir(typesRoot, { recursive: true });
67
+ await fs.mkdir(cacheDir, { recursive: true });
65
68
  const internalOptions = {
66
69
  rootDir,
67
70
  include,
68
- outDir,
69
- typesRoot,
71
+ cacheDir,
70
72
  stableNamespace: options.stableNamespace,
73
+ tsconfig,
71
74
  };
72
75
  return generateDeclarations(internalOptions);
73
76
  }
74
77
  async function generateDeclarations(options) {
75
78
  const peerResolver = createProjectPeerResolver(options.rootDir);
76
79
  const files = await collectCandidateFiles(options.include);
77
- const manifestPath = path.join(options.outDir, 'manifest.json');
78
- const previousManifest = await readManifest(manifestPath);
79
- const nextManifest = {};
80
+ const selectorModulesManifestPath = path.join(options.cacheDir, 'selector-modules.json');
81
+ const previousSelectorManifest = await readManifest(selectorModulesManifestPath);
82
+ const nextSelectorManifest = {};
80
83
  const selectorCache = new Map();
81
- const processedSpecifiers = new Set();
82
- const declarations = [];
84
+ const processedSelectors = new Set();
83
85
  const warnings = [];
84
- let writes = 0;
86
+ let selectorModuleWrites = 0;
85
87
  for (const filePath of files) {
86
88
  const matches = await findSpecifierImports(filePath);
87
89
  for (const match of matches) {
88
90
  const cleaned = match.specifier.trim();
89
91
  const inlineFree = stripInlineLoader(cleaned);
90
- if (!inlineFree.includes('?knighted-css'))
91
- continue;
92
- const { resource, query } = splitResourceAndQuery(inlineFree);
93
- if (!query || !hasQueryFlag(query, TYPES_QUERY_FLAG)) {
94
- continue;
95
- }
96
- if (processedSpecifiers.has(cleaned)) {
92
+ const { resource } = splitResourceAndQuery(inlineFree);
93
+ const selectorSource = extractSelectorSourceSpecifier(resource);
94
+ if (!selectorSource) {
97
95
  continue;
98
96
  }
99
97
  const resolvedNamespace = resolveStableNamespace(options.stableNamespace);
100
- const resolvedPath = await resolveImportPath(resource, match.importer, options.rootDir);
98
+ const resolvedPath = await resolveImportPath(selectorSource, match.importer, options.rootDir, options.tsconfig);
101
99
  if (!resolvedPath) {
102
- 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)}.`);
103
101
  continue;
104
102
  }
105
103
  const cacheKey = `${resolvedPath}::${resolvedNamespace}`;
106
104
  let selectorMap = selectorCache.get(cacheKey);
107
105
  if (!selectorMap) {
108
106
  try {
109
- const { css } = await cssWithMeta(resolvedPath, {
107
+ const { css } = await activeCssWithMeta(resolvedPath, {
110
108
  cwd: options.rootDir,
111
109
  peerResolver,
112
110
  });
@@ -123,38 +121,28 @@ async function generateDeclarations(options) {
123
121
  }
124
122
  selectorCache.set(cacheKey, selectorMap);
125
123
  }
126
- const variant = determineSelectorVariant(query);
127
- const declaration = formatModuleDeclaration(cleaned, variant, selectorMap);
128
- const declarationHash = hashContent(declaration);
129
- const fileName = buildDeclarationFileName(cleaned);
130
- const targetPath = path.join(options.outDir, fileName);
131
- const previousEntry = previousManifest[cleaned];
132
- const needsWrite = previousEntry?.hash !== declarationHash || !(await fileExists(targetPath));
133
- if (needsWrite) {
134
- await fs.writeFile(targetPath, declaration, 'utf8');
135
- writes += 1;
124
+ if (!isWithinRoot(resolvedPath, options.rootDir)) {
125
+ warnings.push(`Skipping selector module for ${relativeToRoot(resolvedPath, options.rootDir)} because it is outside the project root.`);
126
+ continue;
127
+ }
128
+ const manifestKey = buildSelectorModuleManifestKey(resolvedPath);
129
+ if (processedSelectors.has(manifestKey)) {
130
+ continue;
136
131
  }
137
- nextManifest[cleaned] = { file: fileName, hash: declarationHash };
138
- if (needsWrite) {
139
- declarations.push({ specifier: cleaned, filePath: targetPath });
132
+ const moduleWrite = await ensureSelectorModule(resolvedPath, selectorMap, previousSelectorManifest, nextSelectorManifest);
133
+ if (moduleWrite) {
134
+ selectorModuleWrites += 1;
140
135
  }
141
- processedSpecifiers.add(cleaned);
136
+ processedSelectors.add(manifestKey);
142
137
  }
143
138
  }
144
- const removed = await removeStaleDeclarations(previousManifest, nextManifest, options.outDir);
145
- await writeManifest(manifestPath, nextManifest);
146
- const typesIndexPath = path.join(options.typesRoot, 'index.d.ts');
147
- await writeTypesIndex(typesIndexPath, nextManifest, options.outDir);
148
- if (Object.keys(nextManifest).length === 0) {
149
- declarations.length = 0;
150
- }
139
+ const selectorModulesRemoved = await removeStaleSelectorModules(previousSelectorManifest, nextSelectorManifest);
140
+ await writeManifest(selectorModulesManifestPath, nextSelectorManifest);
151
141
  return {
152
- written: writes,
153
- removed,
154
- declarations,
142
+ selectorModulesWritten: selectorModuleWrites,
143
+ selectorModulesRemoved,
155
144
  warnings,
156
- outDir: options.outDir,
157
- typesIndexPath,
145
+ manifestPath: selectorModulesManifestPath,
158
146
  };
159
147
  }
160
148
  function normalizeIncludeOptions(include, rootDir) {
@@ -212,18 +200,18 @@ async function findSpecifierImports(filePath) {
212
200
  catch {
213
201
  return [];
214
202
  }
215
- if (!source.includes('?knighted-css')) {
203
+ if (!source.includes(SELECTOR_REFERENCE)) {
216
204
  return [];
217
205
  }
218
206
  const matches = [];
219
207
  const [imports] = parse(source, filePath);
220
208
  for (const record of imports) {
221
209
  const specifier = record.n ?? source.slice(record.s, record.e);
222
- if (specifier && specifier.includes('?knighted-css')) {
210
+ if (specifier && specifier.includes(SELECTOR_REFERENCE)) {
223
211
  matches.push({ specifier, importer: filePath });
224
212
  }
225
213
  }
226
- const requireRegex = /require\((['"])([^'"`]+?\?knighted-css[^'"`]*)\1\)/g;
214
+ const requireRegex = /require\((['"])([^'"`]+?\.knighted-css[^'"`]*)\1\)/g;
227
215
  let reqMatch;
228
216
  while ((reqMatch = requireRegex.exec(source)) !== null) {
229
217
  const spec = reqMatch[2];
@@ -246,8 +234,23 @@ function splitResourceAndQuery(specifier) {
246
234
  }
247
235
  return { resource: trimmed.slice(0, queryIndex), query: trimmed.slice(queryIndex) };
248
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
+ }
249
252
  const projectRequireCache = new Map();
250
- async function resolveImportPath(resourceSpecifier, importerPath, rootDir) {
253
+ async function resolveImportPath(resourceSpecifier, importerPath, rootDir, tsconfig) {
251
254
  if (!resourceSpecifier)
252
255
  return undefined;
253
256
  if (resourceSpecifier.startsWith('.')) {
@@ -256,6 +259,10 @@ async function resolveImportPath(resourceSpecifier, importerPath, rootDir) {
256
259
  if (resourceSpecifier.startsWith('/')) {
257
260
  return path.resolve(rootDir, resourceSpecifier.slice(1));
258
261
  }
262
+ const tsconfigResolved = await resolveWithTsconfigPaths(resourceSpecifier, tsconfig);
263
+ if (tsconfigResolved) {
264
+ return tsconfigResolved;
265
+ }
259
266
  const requireFromRoot = getProjectRequire(rootDir);
260
267
  try {
261
268
  return requireFromRoot.resolve(resourceSpecifier);
@@ -264,47 +271,29 @@ async function resolveImportPath(resourceSpecifier, importerPath, rootDir) {
264
271
  return undefined;
265
272
  }
266
273
  }
267
- function buildDeclarationFileName(specifier) {
268
- const digest = crypto.createHash('sha1').update(specifier).digest('hex').slice(0, 12);
269
- return `knt-${digest}.d.ts`;
270
- }
271
- function formatModuleDeclaration(specifier, variant, selectors) {
272
- const literalSpecifier = JSON.stringify(specifier);
273
- const selectorType = formatSelectorType(selectors);
274
- const header = `declare module ${literalSpecifier} {`;
275
- const footer = '}';
276
- if (variant === 'types') {
277
- return `${header}
278
- export const knightedCss: string
279
- export const stableSelectors: ${selectorType}
280
- ${footer}
281
- `;
282
- }
283
- const stableLine = ` export const stableSelectors: ${selectorType}`;
284
- const shared = ` const combined: KnightedCssCombinedModule<Record<string, unknown>>
285
- export const knightedCss: string
286
- ${stableLine}`;
287
- if (variant === 'combined') {
288
- return `${header}
289
- ${shared}
290
- export default combined
291
- ${footer}
292
- `;
293
- }
294
- return `${header}
295
- ${shared}
296
- ${footer}
297
- `;
274
+ function buildSelectorModuleManifestKey(resolvedPath) {
275
+ return resolvedPath.split(path.sep).join('/');
298
276
  }
299
- function formatSelectorType(selectors) {
300
- if (selectors.size === 0) {
301
- return 'Readonly<Record<string, string>>';
302
- }
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';
303
282
  const entries = Array.from(selectors.entries()).sort(([a], [b]) => a.localeCompare(b));
304
- const lines = entries.map(([token, selector]) => ` readonly ${JSON.stringify(token)}: ${JSON.stringify(selector)}`);
305
- return `Readonly<{
283
+ const lines = entries.map(([token, selector]) => ` ${JSON.stringify(token)}: ${JSON.stringify(selector)},`);
284
+ const literal = lines.length > 0
285
+ ? `{
306
286
  ${lines.join('\n')}
307
- }>`;
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
+ `;
308
297
  }
309
298
  function hashContent(content) {
310
299
  return crypto.createHash('sha1').update(content).digest('hex');
@@ -321,13 +310,12 @@ async function readManifest(manifestPath) {
321
310
  async function writeManifest(manifestPath, manifest) {
322
311
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
323
312
  }
324
- async function removeStaleDeclarations(previous, next, outDir) {
325
- 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]);
326
315
  let removed = 0;
327
316
  for (const [, entry] of stale) {
328
- const targetPath = path.join(outDir, entry.file);
329
317
  try {
330
- await fs.unlink(targetPath);
318
+ await fs.unlink(entry.file);
331
319
  removed += 1;
332
320
  }
333
321
  catch {
@@ -336,25 +324,6 @@ async function removeStaleDeclarations(previous, next, outDir) {
336
324
  }
337
325
  return removed;
338
326
  }
339
- async function writeTypesIndex(indexPath, manifest, outDir) {
340
- const header = '// Generated by @knighted/css/generate-types\n// Do not edit.\n';
341
- const references = Object.values(manifest)
342
- .sort((a, b) => a.file.localeCompare(b.file))
343
- .map(entry => {
344
- const rel = path
345
- .relative(path.dirname(indexPath), path.join(outDir, entry.file))
346
- .split(path.sep)
347
- .join('/');
348
- return `/// <reference path="${rel}" />`;
349
- });
350
- const content = references.length > 0
351
- ? `${header}
352
- ${references.join('\n')}
353
- `
354
- : `${header}
355
- `;
356
- await fs.writeFile(indexPath, content, 'utf8');
357
- }
358
327
  function formatErrorMessage(error) {
359
328
  if (error instanceof Error && typeof error.message === 'string') {
360
329
  return error.message;
@@ -364,6 +333,23 @@ function formatErrorMessage(error) {
364
333
  function relativeToRoot(filePath, rootDir) {
365
334
  return path.relative(rootDir, filePath) || filePath;
366
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
+ }
367
353
  async function fileExists(target) {
368
354
  try {
369
355
  await fs.access(target);
@@ -373,6 +359,78 @@ async function fileExists(target) {
373
359
  return false;
374
360
  }
375
361
  }
362
+ async function resolveWithTsconfigPaths(specifier, tsconfig) {
363
+ if (!tsconfig) {
364
+ return undefined;
365
+ }
366
+ if (tsconfig.matchPath) {
367
+ const matched = tsconfig.matchPath(specifier);
368
+ if (matched && (await fileExists(matched))) {
369
+ return matched;
370
+ }
371
+ }
372
+ if (tsconfig.absoluteBaseUrl && isNonRelativeSpecifier(specifier)) {
373
+ const candidate = path.join(tsconfig.absoluteBaseUrl, specifier.split('/').join(path.sep));
374
+ if (await fileExists(candidate)) {
375
+ return candidate;
376
+ }
377
+ }
378
+ return undefined;
379
+ }
380
+ function loadTsconfigResolutionContext(rootDir, loader = getTsconfig) {
381
+ let result;
382
+ try {
383
+ result = loader(rootDir);
384
+ }
385
+ catch {
386
+ return undefined;
387
+ }
388
+ if (!result) {
389
+ return undefined;
390
+ }
391
+ const compilerOptions = result.config.compilerOptions ?? {};
392
+ const configDir = path.dirname(result.path);
393
+ const absoluteBaseUrl = compilerOptions.baseUrl
394
+ ? path.resolve(configDir, compilerOptions.baseUrl)
395
+ : undefined;
396
+ const normalizedPaths = normalizeTsconfigPaths(compilerOptions.paths);
397
+ const matchPath = absoluteBaseUrl && normalizedPaths
398
+ ? createMatchPath(absoluteBaseUrl, normalizedPaths)
399
+ : undefined;
400
+ if (!absoluteBaseUrl && !matchPath) {
401
+ return undefined;
402
+ }
403
+ return { absoluteBaseUrl, matchPath };
404
+ }
405
+ function normalizeTsconfigPaths(paths) {
406
+ if (!paths) {
407
+ return undefined;
408
+ }
409
+ const normalized = {};
410
+ for (const [pattern, replacements] of Object.entries(paths)) {
411
+ if (!replacements) {
412
+ continue;
413
+ }
414
+ const values = Array.isArray(replacements) ? replacements : [replacements];
415
+ if (values.length === 0) {
416
+ continue;
417
+ }
418
+ normalized[pattern] = values;
419
+ }
420
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
421
+ }
422
+ function isNonRelativeSpecifier(specifier) {
423
+ if (!specifier) {
424
+ return false;
425
+ }
426
+ if (specifier.startsWith('.') || specifier.startsWith('/')) {
427
+ return false;
428
+ }
429
+ if (/^[a-z][\w+.-]*:/i.test(specifier)) {
430
+ return false;
431
+ }
432
+ return true;
433
+ }
376
434
  function createProjectPeerResolver(rootDir) {
377
435
  const resolver = getProjectRequire(rootDir);
378
436
  return async (name) => {
@@ -415,7 +473,6 @@ export async function runGenerateTypesCli(argv = process.argv.slice(2)) {
415
473
  rootDir: parsed.rootDir,
416
474
  include: parsed.include,
417
475
  outDir: parsed.outDir,
418
- typesRoot: parsed.typesRoot,
419
476
  stableNamespace: parsed.stableNamespace,
420
477
  });
421
478
  reportCliResult(result);
@@ -430,12 +487,11 @@ function parseCliArgs(argv) {
430
487
  let rootDir = process.cwd();
431
488
  const include = [];
432
489
  let outDir;
433
- let typesRoot;
434
490
  let stableNamespace;
435
491
  for (let i = 0; i < argv.length; i += 1) {
436
492
  const arg = argv[i];
437
493
  if (arg === '--help' || arg === '-h') {
438
- return { rootDir, include, outDir, typesRoot, stableNamespace, help: true };
494
+ return { rootDir, include, outDir, stableNamespace, help: true };
439
495
  }
440
496
  if (arg === '--root' || arg === '-r') {
441
497
  const value = argv[++i];
@@ -461,14 +517,6 @@ function parseCliArgs(argv) {
461
517
  outDir = value;
462
518
  continue;
463
519
  }
464
- if (arg === '--types-root') {
465
- const value = argv[++i];
466
- if (!value) {
467
- throw new Error('Missing value for --types-root');
468
- }
469
- typesRoot = value;
470
- continue;
471
- }
472
520
  if (arg === '--stable-namespace') {
473
521
  const value = argv[++i];
474
522
  if (!value) {
@@ -482,7 +530,7 @@ function parseCliArgs(argv) {
482
530
  }
483
531
  include.push(arg);
484
532
  }
485
- return { rootDir, include, outDir, typesRoot, stableNamespace };
533
+ return { rootDir, include, outDir, stableNamespace };
486
534
  }
487
535
  function printHelp() {
488
536
  console.log(`Usage: knighted-css-generate-types [options]
@@ -490,32 +538,59 @@ function printHelp() {
490
538
  Options:
491
539
  -r, --root <path> Project root directory (default: cwd)
492
540
  -i, --include <path> Additional directories/files to scan (repeatable)
493
- --out-dir <path> Output directory for generated declarations
494
- --types-root <path> Directory for generated @types entrypoint
541
+ --out-dir <path> Directory to store selector module manifest cache
495
542
  --stable-namespace <name> Stable namespace prefix for generated selector maps
496
543
  -h, --help Show this help message
497
544
  `);
498
545
  }
499
546
  function reportCliResult(result) {
500
- if (result.written === 0 && result.removed === 0) {
501
- 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.');
502
549
  }
503
550
  else {
504
- 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}.`);
505
552
  }
506
- console.log(`[knighted-css] Type references: ${result.typesIndexPath}`);
553
+ console.log(`[knighted-css] Manifest: ${result.manifestPath}`);
507
554
  for (const warning of result.warnings) {
508
555
  console.warn(`[knighted-css] ${warning}`);
509
556
  }
510
557
  }
558
+ function setCssWithMetaImplementation(impl) {
559
+ activeCssWithMeta = impl ?? cssWithMeta;
560
+ }
561
+ function setModuleTypeDetector(detector) {
562
+ moduleTypeDetector = detector ?? moduleType;
563
+ }
564
+ function setImportMetaUrlProvider(provider) {
565
+ importMetaUrlProvider = provider ?? getImportMetaUrl;
566
+ }
511
567
  export const __generateTypesInternals = {
512
568
  stripInlineLoader,
513
569
  splitResourceAndQuery,
514
- buildDeclarationFileName,
515
- formatModuleDeclaration,
516
- formatSelectorType,
570
+ extractSelectorSourceSpecifier,
571
+ findSpecifierImports,
572
+ resolveImportPath,
573
+ resolvePackageRoot,
574
+ relativeToRoot,
575
+ collectCandidateFiles,
517
576
  normalizeIncludeOptions,
577
+ normalizeTsconfigPaths,
578
+ setCssWithMetaImplementation,
579
+ setModuleTypeDetector,
580
+ setImportMetaUrlProvider,
581
+ isNonRelativeSpecifier,
582
+ createProjectPeerResolver,
583
+ getProjectRequire,
584
+ loadTsconfigResolutionContext,
585
+ resolveWithTsconfigPaths,
518
586
  parseCliArgs,
519
587
  printHelp,
520
588
  reportCliResult,
589
+ buildSelectorModuleManifestKey,
590
+ buildSelectorModulePath,
591
+ formatSelectorModuleSource,
592
+ ensureSelectorModule,
593
+ removeStaleSelectorModules,
594
+ readManifest,
595
+ writeManifest,
521
596
  };
@@ -0,0 +1,4 @@
1
+ import type { KnightedCssCombinedModule } from './loader.js';
2
+ type KnightedCssCombinedExtras = Readonly<Record<string, unknown>>;
3
+ export declare function asKnightedCssCombinedModule<TModule, TExtras extends KnightedCssCombinedExtras = Record<never, never>>(module: unknown): KnightedCssCombinedModule<TModule, TExtras>;
4
+ export {};
@@ -0,0 +1,3 @@
1
+ export function asKnightedCssCombinedModule(module) {
2
+ return module;
3
+ }
package/dist/loader.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { LoaderDefinitionFunction, PitchLoaderDefinitionFunction } from 'webpack';
2
2
  import { type CssOptions } from './css.js';
3
- export type KnightedCssCombinedModule<TModule> = TModule & {
3
+ type KnightedCssCombinedExtras = Readonly<Record<string, unknown>>;
4
+ export type KnightedCssCombinedModule<TModule, TExtras extends KnightedCssCombinedExtras = Record<never, never>> = TModule & TExtras & {
4
5
  knightedCss: string;
5
6
  };
6
7
  export interface KnightedCssVanillaOptions {
package/dist/loader.js CHANGED
@@ -15,6 +15,7 @@ const loader = async function loader(source) {
15
15
  namespace: resolvedNamespace,
16
16
  resourcePath: this.resourcePath,
17
17
  emitWarning: message => emitKnightedWarning(this, message),
18
+ target: 'js',
18
19
  })
19
20
  : undefined;
20
21
  const injection = buildInjection(css, {
@@ -77,6 +78,7 @@ export const pitch = function pitch() {
77
78
  namespace: resolvedNamespace,
78
79
  resourcePath: this.resourcePath,
79
80
  emitWarning: message => emitKnightedWarning(this, message),
81
+ target: 'js',
80
82
  })
81
83
  : undefined;
82
84
  return createCombinedModule(request, css, {
@@ -191,7 +191,10 @@ function normalizeSpecifier(raw) {
191
191
  if (!trimmed || trimmed.startsWith('\0')) {
192
192
  return '';
193
193
  }
194
- const queryIndex = trimmed.search(/[?#]/);
194
+ const querySearchOffset = trimmed.startsWith('#') ? 1 : 0;
195
+ const remainder = trimmed.slice(querySearchOffset);
196
+ const queryMatchIndex = remainder.search(/[?#]/);
197
+ const queryIndex = queryMatchIndex === -1 ? -1 : querySearchOffset + queryMatchIndex;
195
198
  const withoutQuery = queryIndex === -1 ? trimmed : trimmed.slice(0, queryIndex);
196
199
  if (!withoutQuery) {
197
200
  return '';
@@ -297,9 +300,7 @@ function createResolverFactory(cwd, extensions, scriptExtensions, graphOptions)
297
300
  options.extensionAlias = extensionAlias;
298
301
  }
299
302
  const tsconfigOption = resolveResolverTsconfig(graphOptions?.tsConfig, cwd);
300
- if (tsconfigOption) {
301
- options.tsconfig = tsconfigOption;
302
- }
303
+ options.tsconfig = tsconfigOption ?? 'auto';
303
304
  return new ResolverFactory(options);
304
305
  }
305
306
  function buildExtensionAlias(scriptExtensions) {
@@ -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
+ }