@pre.style/core 3.1.0-alpha.0 → 3.1.0-alpha.2

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,10 +1,13 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
+ import { transform } from 'lightningcss';
3
4
  import SweatMap from 'sweatmap';
4
5
  import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
5
6
  import { print } from './print.js';
6
- import { lvhfaRank, withClass } from './selectors.js';
7
+ import { lvhfaRank, specificity, withClass } from './selectors.js';
8
+ import { KEYFRAMES_PREFIX } from './keyframes.js';
7
9
  import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
10
+ import { SHORTHANDS } from '../shorthands.js';
8
11
  const RANGES = {
9
12
  Digits: { start: '0030', end: '0039' },
10
13
  Uppercase: { start: '0041', end: '005A' },
@@ -45,14 +48,22 @@ export class Compiler {
45
48
  // most deeply nested library first.
46
49
  #prepend;
47
50
  #libraries;
51
+ // The layer the output is wrapped in, as parsed, or null for none.
52
+ #layer;
48
53
  #blocks = new Map();
49
54
  #stylesheets = new Map();
50
55
  #root;
51
- // One name list for every class, and one for prefixes (the at-rules and
52
- // selector variant a class is under), which only the `cn` table uses.
56
+ // One name list for every class, one for prefixes (the at-rules and
57
+ // selector variant a class is under), which only the `cn` table uses, and
58
+ // one for localized keyframes.
53
59
  #names;
54
- // How often each class and prefix is used across scanned blocks.
55
- #counts = { atoms: new Map(), prefixes: new Map() };
60
+ // How often each class, prefix, and localized keyframes is used across
61
+ // scanned blocks.
62
+ #counts = {
63
+ atoms: new Map(),
64
+ prefixes: new Map(),
65
+ keyframes: new Map(),
66
+ };
56
67
  #atoms = new Map();
57
68
  #globals = new Map();
58
69
  // The current global for each overridable at-rule.
@@ -63,14 +74,23 @@ export class Compiler {
63
74
  this.#names = {
64
75
  atoms: new SweatMap(options.quirksMode ? QUIRKS_NAME_OPTIONS : NAME_OPTIONS),
65
76
  prefixes: new SweatMap(NAME_OPTIONS),
77
+ // Keyframes names are case-sensitive, even in quirks mode. They reach
78
+ // four characters only after about 227,000 names, so they're never
79
+ // `none` or a CSS-wide keyword.
80
+ keyframes: new SweatMap(NAME_OPTIONS),
66
81
  };
82
+ this.#layer = layerRule(options.layer);
67
83
  const root = options.root ?? process.cwd();
68
84
  this.#root = root;
69
85
  this.#prepend = readPrepend(root, options.prependFiles);
70
86
  this.#libraries = (options.libraries ?? [])
71
87
  .map((library) => {
72
88
  const dir = resolvePath(root, library.dir);
73
- return { dir: dir + sep, prepend: readPrepend(dir, library.prependFiles) };
89
+ return {
90
+ dir: dir + sep,
91
+ prepend: readPrepend(dir, library.prependFiles),
92
+ keyframeLocalization: library.keyframeLocalization ?? false,
93
+ };
74
94
  })
75
95
  .toSorted((a, b) => b.dir.length - a.dir.length);
76
96
  this.#parseOptions = {
@@ -90,17 +110,21 @@ export class Compiler {
90
110
  const parsed = this.#parse(block);
91
111
  this.#register(block, parsed);
92
112
  this.#count(this.#resolveBlock(block, parsed, []));
113
+ for (const global of parsed.globals)
114
+ this.#countKeyframes(global.key);
93
115
  }
94
116
  // Pass 1 of naming for a stylesheet file: counts what each class uses.
95
117
  scanStylesheet(sheet) {
96
118
  const parsed = this.#parseSheet(sheet);
97
119
  for (const name of parsed.classes.keys())
98
120
  this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
121
+ for (const global of parsed.globals)
122
+ this.#countKeyframes(global.key);
99
123
  }
100
124
  // Pass 2 of naming: gives the most-used entries the shortest names. Ties
101
125
  // are broken by key, so names don't depend on the order files are read.
102
126
  assignNames() {
103
- for (const list of ['atoms', 'prefixes']) {
127
+ for (const list of ['atoms', 'prefixes', 'keyframes']) {
104
128
  const sorted = [...this.#counts[list]].toSorted(([a, countA], [b, countB]) => countB - countA || (a < b ? -1 : a > b ? 1 : 0));
105
129
  for (const [key] of sorted)
106
130
  this.#names[list].set(key);
@@ -123,15 +147,43 @@ export class Compiler {
123
147
  // anything, for tools that only need the names (such as the TypeScript
124
148
  // content mapper).
125
149
  classNames(sheet) {
126
- const { prepend } = this.#library(sheet.file);
150
+ const { options } = this.#library(sheet.file);
127
151
  try {
128
- const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), { ...this.#parseOptions, prepend });
152
+ const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
129
153
  return [...parsed.classes.keys()];
130
154
  }
131
155
  catch (error) {
132
156
  throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
133
157
  }
134
158
  }
159
+ // Where each of the given classes is used, in output order. Classes that
160
+ // aren't in the output are left out. Sources are only kept with `sourceMap`
161
+ // on, and only list what has been compiled so far.
162
+ usages(classNames) {
163
+ const wanted = new Set(classNames);
164
+ const usages = [];
165
+ let order = 0;
166
+ const walk = (group) => {
167
+ for (const record of sortAtoms(group.atoms)) {
168
+ order++;
169
+ if (record.className !== null && wanted.has(record.className))
170
+ usages.push(this.#usage(record, order));
171
+ }
172
+ for (const child of sortGroups([...group.children.values()]))
173
+ walk(child);
174
+ };
175
+ walk(this.#atomGroups());
176
+ return usages;
177
+ }
178
+ // Drops the places a file uses atoms, because it changed. They're added back
179
+ // as its blocks are compiled again. The atoms stay, like the rest of dev's
180
+ // output, which only grows.
181
+ forgetSources(file) {
182
+ for (const record of this.#atoms.values())
183
+ for (const [key, source] of record.sources)
184
+ if (source.file === file)
185
+ record.sources.delete(key);
186
+ }
135
187
  // Each class of a stylesheet file, with its class string.
136
188
  stylesheet(sheet) {
137
189
  const parsed = this.#parseSheet(sheet);
@@ -160,8 +212,25 @@ export class Compiler {
160
212
  .filter((g) => g.global.hoist !== false)
161
213
  .toSorted((a, b) => a.global.hoist - b.global.hoist);
162
214
  const rest = globals.filter((g) => g.global.hoist === false);
163
- const rules = [...hoisted, ...rest].map((g) => withLoc(g.global.key.includes(MARKER_KEY) ? this.#named(g.global.rule) : g.global.rule, loc(g.source)));
215
+ const named = (g) => withLoc(hasPlaceholder(g.global.key) ? this.#named(g.global.rule) : g.global.rule, loc(g.source));
216
+ let rules = [...hoisted, ...rest].map(named);
164
217
  rules.push(...this.#atomRules(loc));
218
+ // `@import` and `@namespace` can't be inside a layer, so they stay
219
+ // before it. `@layer` statements go inside, so the layers they order
220
+ // are the same ones blocks' `@layer` rules add to.
221
+ if (this.#layer !== null) {
222
+ rules = [
223
+ ...rules.filter(outside),
224
+ {
225
+ type: 'layer-block',
226
+ value: {
227
+ ...valueOf(this.#layer),
228
+ rules: rules.filter((r) => !outside(r)),
229
+ loc: loc(null),
230
+ },
231
+ },
232
+ ];
233
+ }
165
234
  return print(rules, sources, this.#parseOptions.lightningcss, this.#options.sourceMap ?? false);
166
235
  }
167
236
  // Classes on the element have an empty prefix.
@@ -177,19 +246,29 @@ export class Compiler {
177
246
  }
178
247
  return table;
179
248
  }
180
- // A library's blocks and files get its prepended files instead of the
181
- // app's.
249
+ // A library's blocks and files get its prepended files and keyframes
250
+ // setting instead of the app's.
182
251
  #library(file) {
183
252
  const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
184
- return { library, prepend: library === -1 ? this.#prepend : this.#libraries[library].prepend };
253
+ const { prepend, keyframeLocalization } = library === -1
254
+ ? { prepend: this.#prepend, keyframeLocalization: this.#options.keyframeLocalization }
255
+ : this.#libraries[library];
256
+ return {
257
+ library,
258
+ options: {
259
+ ...this.#parseOptions,
260
+ prepend,
261
+ keyframeLocalization: keyframeLocalization ?? false,
262
+ },
263
+ };
185
264
  }
186
265
  #parse(block) {
187
- const { library, prepend } = this.#library(block.file);
266
+ const { library, options } = this.#library(block.file);
188
267
  const cacheKey = `${library}\n${block.lang}\n${block.code}`;
189
268
  let parsed = this.#blocks.get(cacheKey);
190
269
  if (parsed === undefined) {
191
270
  try {
192
- parsed = parseBlock(block.code, block.lang, { ...this.#parseOptions, prepend });
271
+ parsed = parseBlock(block.code, block.lang, options);
193
272
  }
194
273
  catch (error) {
195
274
  throw error instanceof PreStyleError ? locate(error, block) : error;
@@ -201,15 +280,12 @@ export class Compiler {
201
280
  // Parses a stylesheet file and registers its classes and global rules, so
202
281
  // the output includes them once the file is used.
203
282
  #parseSheet(sheet) {
204
- const { library, prepend } = this.#library(sheet.file);
283
+ const { library, options } = this.#library(sheet.file);
205
284
  const cacheKey = `${library}\n${sheet.lang}\n${sheet.file}\n${sheet.code}`;
206
285
  let parsed = this.#stylesheets.get(cacheKey);
207
286
  if (parsed === undefined) {
208
287
  try {
209
- parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), {
210
- ...this.#parseOptions,
211
- prepend,
212
- });
288
+ parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
213
289
  }
214
290
  catch (error) {
215
291
  throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
@@ -301,6 +377,7 @@ export class Compiler {
301
377
  const prefix = prefixKey(atom);
302
378
  if (prefix !== '')
303
379
  count(this.#counts.prefixes, prefix);
380
+ this.#countKeyframes(atom.declaration.key);
304
381
  }
305
382
  for (const extra of resolved.extras)
306
383
  if (extra.startsWith(MARKER_PREFIX))
@@ -312,7 +389,15 @@ export class Compiler {
312
389
  names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
313
390
  return [...new Set(names)].join(' ');
314
391
  }
315
- // Replaces markers in selectors with their names.
392
+ // Counts the localized keyframes a JSON key names.
393
+ #countKeyframes(json) {
394
+ if (!json.includes(KEYFRAMES_KEY))
395
+ return;
396
+ for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
397
+ count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
398
+ }
399
+ // Replaces markers in selectors, and localized keyframes names, with their
400
+ // names.
316
401
  #named(value) {
317
402
  const visit = (node) => {
318
403
  if (Array.isArray(node))
@@ -324,6 +409,10 @@ export class Compiler {
324
409
  typeof record.name === 'string' &&
325
410
  record.name.startsWith(MARKER_PREFIX))
326
411
  return { ...record, name: this.#names.atoms.set(record.name) };
412
+ if (record.type === 'ident' &&
413
+ typeof record.value === 'string' &&
414
+ record.value.startsWith(KEYFRAMES_PREFIX))
415
+ return { ...record, value: this.#names.keyframes.set(record.value) };
327
416
  return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, visit(v)]));
328
417
  };
329
418
  return visit(value);
@@ -331,14 +420,21 @@ export class Compiler {
331
420
  #register(block, parsed) {
332
421
  for (const atom of parsed.atoms) {
333
422
  const key = atomKey(atom);
334
- if (!this.#atoms.has(key)) {
335
- this.#atoms.set(key, {
423
+ const source = sourceOf(block, atom.position);
424
+ let record = this.#atoms.get(key);
425
+ if (record === undefined) {
426
+ record = {
336
427
  atom,
337
428
  className: null,
338
429
  order: this.#order++,
339
- source: sourceOf(block, atom.position),
340
- });
430
+ source,
431
+ sources: new Map(),
432
+ printed: null,
433
+ };
434
+ this.#atoms.set(key, record);
341
435
  }
436
+ if (source !== null && this.#options.sourceMap)
437
+ record.sources.set(`${source.file}:${source.line}:${source.column}`, source);
342
438
  }
343
439
  for (const global of parsed.globals) {
344
440
  if (this.#globals.has(global.key))
@@ -360,8 +456,8 @@ export class Compiler {
360
456
  #nameOf(record) {
361
457
  return (record.className ??= this.#names.atoms.set(atomKey(record.atom)));
362
458
  }
363
- #atomRules(loc) {
364
- // Group atoms by their at-rules, nesting groups the same way.
459
+ // Atoms grouped by their at-rules, nesting groups the same way.
460
+ #atomGroups() {
365
461
  const root = { segment: null, order: 0, atoms: [], children: new Map() };
366
462
  for (const record of this.#atoms.values()) {
367
463
  let group = root;
@@ -375,22 +471,35 @@ export class Compiler {
375
471
  }
376
472
  group.atoms.push(record);
377
473
  }
474
+ return root;
475
+ }
476
+ #atomSelectors(record) {
477
+ const { atom } = record;
478
+ const className = this.#nameOf(record);
479
+ const selectors = atom.variant
480
+ ? withClass(atom.variant.selectors, className)
481
+ : [[{ type: 'class', name: className }]];
482
+ return atom.variant?.key.includes(MARKER_KEY) ? this.#named(selectors) : selectors;
483
+ }
484
+ #atomRule(record, loc) {
485
+ const { atom } = record;
486
+ const value = atom.declaration.key.includes(KEYFRAMES_KEY)
487
+ ? this.#named(atom.declaration.value)
488
+ : atom.declaration.value;
489
+ const declarations = atom.declaration.important
490
+ ? { importantDeclarations: [value] }
491
+ : { declarations: [value] };
492
+ return {
493
+ type: 'style',
494
+ value: { selectors: this.#atomSelectors(record), declarations, rules: [], loc },
495
+ };
496
+ }
497
+ #atomRules(loc) {
378
498
  const emit = (group) => {
379
499
  const rules = sortAtoms(group.atoms).map((record) => {
380
- const { atom, source } = record;
381
- const className = this.#nameOf(record);
382
- let selectors = atom.variant
383
- ? withClass(atom.variant.selectors, className)
384
- : [[{ type: 'class', name: className }]];
385
- if (atom.variant?.key.includes(MARKER_KEY))
386
- selectors = this.#named(selectors);
387
- const declarations = atom.declaration.important
388
- ? { importantDeclarations: [atom.declaration.value] }
389
- : { declarations: [atom.declaration.value] };
390
- return {
391
- type: 'style',
392
- value: { selectors, declarations, rules: [], loc: loc(source) },
393
- };
500
+ // The first place it's still used, since `forgetSources` drops places.
501
+ const [source = record.source] = record.sources.values();
502
+ return this.#atomRule(record, loc(source));
394
503
  });
395
504
  for (const child of sortGroups([...group.children.values()])) {
396
505
  rules.push({
@@ -400,11 +509,72 @@ export class Compiler {
400
509
  }
401
510
  return rules;
402
511
  };
403
- return emit(root);
512
+ return emit(this.#atomGroups());
513
+ }
514
+ #usage(record, order) {
515
+ const { atom } = record;
516
+ const loc = { source_index: 0, line: 0, column: 1 };
517
+ const rule = this.#atomRule(record, loc);
518
+ // Printed for reading, so without the user's `minify` and `targets`.
519
+ const { drafts, nonStandard, errorRecovery } = this.#parseOptions.lightningcss;
520
+ const text = (rules) => print([rules], [], { drafts, nonStandard, errorRecovery }, false).code;
521
+ const code = (record.printed ??= text(rule));
522
+ const open = code.indexOf(' {\n');
523
+ return {
524
+ className: record.className,
525
+ selector: code.slice(0, open).replaceAll('\n', ' '),
526
+ atRules: atom.atRules.map((segment) => {
527
+ const wrapped = { ...valueOf(segment.rule), rules: [rule] };
528
+ const printed = text({ type: segment.rule.type, value: wrapped });
529
+ return printed.slice(0, printed.indexOf(' {\n'));
530
+ }),
531
+ declaration: code
532
+ .slice(open + 3, code.lastIndexOf('\n}'))
533
+ .trim()
534
+ .replace(/;$/, ''),
535
+ property: atom.property,
536
+ covers: Object.hasOwn(SHORTHANDS, atom.property) ? SHORTHANDS[atom.property] : [],
537
+ important: atom.declaration.important,
538
+ specificity: specificity(this.#atomSelectors(record)),
539
+ order,
540
+ sources: [...record.sources.values()],
541
+ };
404
542
  }
405
543
  }
406
- // Markers as they appear in JSON keys, which escape the `\0`.
544
+ // Markers and localized keyframes names as they appear in JSON keys, which
545
+ // escape the `\0`.
407
546
  const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
547
+ const KEYFRAMES_KEY = JSON.stringify(KEYFRAMES_PREFIX).slice(1, -1);
548
+ const KEYFRAMES_KEY_RE = new RegExp(`${RegExp.escape(KEYFRAMES_KEY)}([\\w-]+)`, 'g');
549
+ // Rules that can't be inside a layer.
550
+ const outside = (rule) => rule.type === 'import' || rule.type === 'namespace';
551
+ const hasPlaceholder = (json) => json.includes(MARKER_KEY) || json.includes(KEYFRAMES_KEY);
552
+ // Parses the `layer` option into an `@layer` block rule.
553
+ function layerRule(layer) {
554
+ if (layer === undefined || layer === false)
555
+ return null;
556
+ if (typeof layer !== 'string')
557
+ throw new PreStyleError('`layer` must be `false` or a string.', null);
558
+ let rule;
559
+ try {
560
+ transform({
561
+ filename: 'layer.css',
562
+ code: Buffer.from(`@layer ${layer} {}`),
563
+ visitor: {
564
+ StyleSheetExit(sheet) {
565
+ if (sheet.rules.length === 1)
566
+ rule = sheet.rules[0];
567
+ },
568
+ },
569
+ });
570
+ }
571
+ catch {
572
+ // Reported below.
573
+ }
574
+ if (rule?.type !== 'layer-block' || (layer.trim() === '') !== (rule.value.name == null))
575
+ throw new PreStyleError(`\`layer\` must be a layer name, such as "pre.style" or "base.components", or "" for an unnamed layer. Got ${JSON.stringify(layer)}.`, null);
576
+ return rule;
577
+ }
408
578
  // A stylesheet file, for positions and source maps.
409
579
  const fileBlock = (file) => ({ code: '', lang: 'css', file, line: 1, column: 1 });
410
580
  // Reads prepended files, relative to `dir`, once.
@@ -6,6 +6,7 @@ export interface PackageConfig {
6
6
  prependFiles?: PrependFiles;
7
7
  libraries?: Record<string, LibraryConfig | boolean>;
8
8
  quirksMode?: boolean;
9
+ keyframeLocalization?: boolean;
9
10
  }
10
11
  export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
11
12
  export declare function readConfig(root: string): {
@@ -1,6 +1,13 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
- const KEYS = new Set(['include', 'exclude', 'prependFiles', 'libraries', 'quirksMode']);
3
+ const KEYS = new Set([
4
+ 'include',
5
+ 'exclude',
6
+ 'prependFiles',
7
+ 'libraries',
8
+ 'quirksMode',
9
+ 'keyframeLocalization',
10
+ ]);
4
11
  const isStrings = (value) => typeof value === 'string' ||
5
12
  (Array.isArray(value) && value.every((item) => typeof item === 'string'));
6
13
  const isPrependFiles = (value) => (Array.isArray(value) && isStrings(value)) ||
@@ -22,6 +29,8 @@ export function checkField(field, where, keys) {
22
29
  fail('prependFiles', 'an array of paths, or an object with `css` and `scss` arrays');
23
30
  if (field.quirksMode !== undefined && typeof field.quirksMode !== 'boolean')
24
31
  fail('quirksMode', '`true` or `false`');
32
+ if (field.keyframeLocalization !== undefined && typeof field.keyframeLocalization !== 'boolean')
33
+ fail('keyframeLocalization', '`true` or `false`');
25
34
  if (field.libraries !== undefined &&
26
35
  (field.libraries === null ||
27
36
  typeof field.libraries !== 'object' ||
@@ -1,5 +1,5 @@
1
1
  export { Compiler, createCompiler } from './compiler.ts';
2
- export type { Block, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Stylesheet, } from './compiler.ts';
2
+ export type { Block, ClassUsage, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Source, Stylesheet, } from './compiler.ts';
3
3
  export { PreStyleError } from './block.ts';
4
4
  export type { Lang, LightningOptions, Position } from './block.ts';
5
5
  export type { Printed } from './print.ts';
@@ -0,0 +1,6 @@
1
+ import type { Atom, GlobalRule } from './block.ts';
2
+ export declare const KEYFRAMES_PREFIX = "\0pre.style-keyframes:";
3
+ export declare function localizeKeyframes(globals: GlobalRule[]): {
4
+ globals: GlobalRule[];
5
+ atoms: (atoms: Atom[]) => Atom[];
6
+ };
@@ -0,0 +1,120 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { key, overrideKey, valueOf } from './block.js';
3
+ // With `keyframeLocalization`, each `@keyframes` name is replaced with a
4
+ // generated one, like styled-components' `keyframes`. In parsed rules, a
5
+ // local name is an ident with this prefix and a hash of every definition of
6
+ // the name in its scope (a block, or a stylesheet file), so identical
7
+ // keyframes share a name wherever they're defined. The compiler gives it a
8
+ // name when printing.
9
+ export const KEYFRAMES_PREFIX = '\0pre.style-keyframes:';
10
+ // Properties whose values name keyframes, without a vendor prefix.
11
+ const ANIMATION_PROPERTIES = new Set(['animation', 'animation-name']);
12
+ // Replaces each `@keyframes` name defined in a scope (in its globals, which
13
+ // include its prepended code) with a local name, in the definitions, and,
14
+ // through `atoms`, in `animation` and `animation-name` declarations. Names
15
+ // the scope doesn't define are left as they are.
16
+ export function localizeKeyframes(globals) {
17
+ const definitions = new Map();
18
+ for (const global of globals)
19
+ eachKeyframes(global.rule, (value) => {
20
+ const list = definitions.get(value.name.value) ?? [];
21
+ list.push(key({ vendorPrefix: value.vendorPrefix, keyframes: value.keyframes }));
22
+ definitions.set(value.name.value, list);
23
+ });
24
+ if (definitions.size === 0)
25
+ return { globals, atoms: (atoms) => atoms };
26
+ const local = new Map();
27
+ for (const [name, list] of definitions)
28
+ local.set(name, KEYFRAMES_PREFIX +
29
+ createHash('sha256').update(JSON.stringify(list)).digest('base64url').slice(0, 16));
30
+ return {
31
+ globals: globals.map((global) => {
32
+ let found = false;
33
+ const rule = mapRule(global.rule, (inner) => {
34
+ if (inner.type !== 'keyframes')
35
+ return inner;
36
+ found = true;
37
+ const value = valueOf(inner);
38
+ return {
39
+ ...inner,
40
+ value: {
41
+ ...value,
42
+ name: { type: 'ident', value: local.get(value.name.value) },
43
+ },
44
+ };
45
+ });
46
+ if (!found)
47
+ return global;
48
+ return {
49
+ ...global,
50
+ key: key(rule),
51
+ rule,
52
+ overrideKey: global.overrideKey === null ? null : overrideKey(rule),
53
+ };
54
+ }),
55
+ atoms: (atoms) => atoms.map((atom) => {
56
+ if (!ANIMATION_PROPERTIES.has(atom.property.replace(/^-\w+-/, '')))
57
+ return atom;
58
+ const value = renameIdents(atom.declaration.value, local);
59
+ if (value === atom.declaration.value)
60
+ return atom;
61
+ return {
62
+ ...atom,
63
+ declaration: {
64
+ ...atom.declaration,
65
+ key: key(value) + (atom.declaration.important ? '!' : ''),
66
+ value,
67
+ },
68
+ };
69
+ }),
70
+ };
71
+ }
72
+ // Calls `visit` for each `@keyframes` rule in a rule, including inside the
73
+ // at-rules a global keeps around it.
74
+ function eachKeyframes(rule, visit) {
75
+ mapRule(rule, (inner) => {
76
+ if (inner.type === 'keyframes')
77
+ visit(valueOf(inner));
78
+ return inner;
79
+ });
80
+ }
81
+ // Maps a rule and the rules nested in it.
82
+ function mapRule(rule, map) {
83
+ const value = valueOf(rule);
84
+ const mapped = Array.isArray(value.rules)
85
+ ? {
86
+ ...rule,
87
+ value: {
88
+ ...value,
89
+ rules: value.rules.map((r) => mapRule(r, map)),
90
+ },
91
+ }
92
+ : rule;
93
+ return map(mapped);
94
+ }
95
+ // Replaces names in idents and strings (parsed animation names, and tokens
96
+ // in unparsed values). Returns `value` itself when nothing changed.
97
+ function renameIdents(value, local) {
98
+ const visit = (node) => {
99
+ if (Array.isArray(node)) {
100
+ const mapped = node.map(visit);
101
+ return mapped.some((item, i) => item !== node[i]) ? mapped : node;
102
+ }
103
+ if (node === null || typeof node !== 'object')
104
+ return node;
105
+ const record = node;
106
+ if ((record.type === 'ident' || record.type === 'string') &&
107
+ typeof record.value === 'string' &&
108
+ local.has(record.value))
109
+ return { type: 'ident', value: local.get(record.value) };
110
+ let changed = false;
111
+ const entries = Object.entries(record).map(([k, v]) => {
112
+ const mapped = visit(v);
113
+ if (mapped !== v)
114
+ changed = true;
115
+ return [k, mapped];
116
+ });
117
+ return changed ? Object.fromEntries(entries) : node;
118
+ };
119
+ return visit(value);
120
+ }
@@ -3,6 +3,7 @@ export interface LibraryConfig {
3
3
  include?: string | string[];
4
4
  exclude?: string | string[];
5
5
  prependFiles?: PrependFiles;
6
+ keyframeLocalization?: boolean;
6
7
  }
7
8
  export interface ResolvedLibrary {
8
9
  name: string;
@@ -10,5 +11,6 @@ export interface ResolvedLibrary {
10
11
  include: string[];
11
12
  exclude: string[];
12
13
  prependFiles?: PrependFiles;
14
+ keyframeLocalization?: boolean;
13
15
  }
14
16
  export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];
@@ -3,7 +3,7 @@ import { dirname, join } from 'node:path';
3
3
  import { checkField } from './config.js';
4
4
  // Keys a library's field shares with the app's. `libraries` and
5
5
  // `quirksMode` only apply to the app.
6
- const SHARED_KEYS = new Set(['include', 'exclude', 'prependFiles']);
6
+ const SHARED_KEYS = new Set(['include', 'exclude', 'prependFiles', 'keyframeLocalization']);
7
7
  const APP_KEYS = new Set(['libraries', 'quirksMode']);
8
8
  const DEFAULT_INCLUDE = ['**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,astro}'];
9
9
  const readPackage = (dir) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
@@ -44,13 +44,14 @@ export function findLibraries(root, overrides = {}, warn = () => { }) {
44
44
  for (const warning of checkField(config, where, new Set([...SHARED_KEYS, ...APP_KEYS])))
45
45
  warn(warning);
46
46
  }
47
- const { include, exclude, prependFiles } = config === true ? {} : config;
47
+ const { include, exclude, prependFiles, keyframeLocalization } = config === true ? {} : config;
48
48
  libraries.push({
49
49
  name,
50
50
  dir,
51
51
  include: include === undefined ? DEFAULT_INCLUDE : [include].flat(),
52
52
  exclude: exclude === undefined ? [] : [exclude].flat(),
53
53
  prependFiles,
54
+ keyframeLocalization,
54
55
  });
55
56
  // A library's peer dependencies are installed by the app, and found the
56
57
  // same way from the library.
@@ -29,8 +29,13 @@ async function openProject(params) {
29
29
  const compiler = createCompiler({
30
30
  root: app.dir,
31
31
  prependFiles: app.config.prependFiles,
32
- libraries: libraries.map(({ dir, prependFiles }) => ({ dir, prependFiles })),
32
+ libraries: libraries.map(({ dir, prependFiles, keyframeLocalization }) => ({
33
+ dir,
34
+ prependFiles,
35
+ keyframeLocalization,
36
+ })),
33
37
  quirksMode: app.config.quirksMode,
38
+ keyframeLocalization: app.config.keyframeLocalization,
34
39
  warn: () => { },
35
40
  });
36
41
  projects.set(params.projectHandle, compiler);
@@ -6,3 +6,6 @@ export declare function compose(parent: SelectorList, child: SelectorList): Sele
6
6
  export declare function withClass(selectors: SelectorList, className: string): SelectorList;
7
7
  export declare function lvhfaRank(selectors: SelectorList): number;
8
8
  export declare function repair<T>(selectors: T): T;
9
+ type Specificity = [ids: number, classes: number, types: number];
10
+ export declare function specificity(selectors: SelectorList): Specificity;
11
+ export {};