@pre.style/core 3.0.0-alpha.0 → 3.1.0-alpha.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,9 +1,12 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { resolve as resolvePath, sep } from 'node:path';
2
+ import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
+ import { transform } from 'lightningcss';
3
4
  import SweatMap from 'sweatmap';
4
- import { parseBlock, PreStyleError, valueOf } from './block.js';
5
+ import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
5
6
  import { print } from './print.js';
6
7
  import { lvhfaRank, withClass } from './selectors.js';
8
+ import { KEYFRAMES_PREFIX } from './keyframes.js';
9
+ import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
7
10
  const RANGES = {
8
11
  Digits: { start: '0030', end: '0039' },
9
12
  Uppercase: { start: '0041', end: '005A' },
@@ -44,12 +47,22 @@ export class Compiler {
44
47
  // most deeply nested library first.
45
48
  #prepend;
46
49
  #libraries;
50
+ // The layer the output is wrapped in, as parsed, or null for none.
51
+ #layer;
47
52
  #blocks = new Map();
48
- // One name list for every class, and one for prefixes (the at-rules and
49
- // selector variant a class is under), which only the `cn` table uses.
53
+ #stylesheets = new Map();
54
+ #root;
55
+ // One name list for every class, one for prefixes (the at-rules and
56
+ // selector variant a class is under), which only the `cn` table uses, and
57
+ // one for localized keyframes.
50
58
  #names;
51
- // How often each class and prefix is used across scanned blocks.
52
- #counts = { atoms: new Map(), prefixes: new Map() };
59
+ // How often each class, prefix, and localized keyframes is used across
60
+ // scanned blocks.
61
+ #counts = {
62
+ atoms: new Map(),
63
+ prefixes: new Map(),
64
+ keyframes: new Map(),
65
+ };
53
66
  #atoms = new Map();
54
67
  #globals = new Map();
55
68
  // The current global for each overridable at-rule.
@@ -60,13 +73,23 @@ export class Compiler {
60
73
  this.#names = {
61
74
  atoms: new SweatMap(options.quirksMode ? QUIRKS_NAME_OPTIONS : NAME_OPTIONS),
62
75
  prefixes: new SweatMap(NAME_OPTIONS),
76
+ // Keyframes names are case-sensitive, even in quirks mode. They reach
77
+ // four characters only after about 227,000 names, so they're never
78
+ // `none` or a CSS-wide keyword.
79
+ keyframes: new SweatMap(NAME_OPTIONS),
63
80
  };
81
+ this.#layer = layerRule(options.layer);
64
82
  const root = options.root ?? process.cwd();
83
+ this.#root = root;
65
84
  this.#prepend = readPrepend(root, options.prependFiles);
66
85
  this.#libraries = (options.libraries ?? [])
67
86
  .map((library) => {
68
87
  const dir = resolvePath(root, library.dir);
69
- return { dir: dir + sep, prepend: readPrepend(dir, library.prependFiles) };
88
+ return {
89
+ dir: dir + sep,
90
+ prepend: readPrepend(dir, library.prependFiles),
91
+ keyframeLocalization: library.keyframeLocalization ?? false,
92
+ };
70
93
  })
71
94
  .toSorted((a, b) => b.dir.length - a.dir.length);
72
95
  this.#parseOptions = {
@@ -84,18 +107,23 @@ export class Compiler {
84
107
  // block before `assignNames`.
85
108
  scan(block) {
86
109
  const parsed = this.#parse(block);
87
- for (const atom of parsed.atoms) {
88
- count(this.#counts.atoms, atomKey(atom));
89
- const prefix = prefixKey(atom);
90
- if (prefix !== '')
91
- count(this.#counts.prefixes, prefix);
92
- }
93
110
  this.#register(block, parsed);
111
+ this.#count(this.#resolveBlock(block, parsed, []));
112
+ for (const global of parsed.globals)
113
+ this.#countKeyframes(global.key);
114
+ }
115
+ // Pass 1 of naming for a stylesheet file: counts what each class uses.
116
+ scanStylesheet(sheet) {
117
+ const parsed = this.#parseSheet(sheet);
118
+ for (const name of parsed.classes.keys())
119
+ this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
120
+ for (const global of parsed.globals)
121
+ this.#countKeyframes(global.key);
94
122
  }
95
123
  // Pass 2 of naming: gives the most-used entries the shortest names. Ties
96
124
  // are broken by key, so names don't depend on the order files are read.
97
125
  assignNames() {
98
- for (const list of ['atoms', 'prefixes']) {
126
+ for (const list of ['atoms', 'prefixes', 'keyframes']) {
99
127
  const sorted = [...this.#counts[list]].toSorted(([a, countA], [b, countB]) => countB - countA || (a < b ? -1 : a > b ? 1 : 0));
100
128
  for (const [key] of sorted)
101
129
  this.#names[list].set(key);
@@ -106,7 +134,38 @@ export class Compiler {
106
134
  classString(block) {
107
135
  const parsed = this.#parse(block);
108
136
  this.#register(block, parsed);
109
- return parsed.atoms.map((atom) => this.#nameOf(this.#atoms.get(atomKey(atom)))).join(' ');
137
+ return this.#classString(this.#resolveBlock(block, parsed, []));
138
+ }
139
+ // The files a block's class string depends on, through `composes … from`.
140
+ dependencies(block) {
141
+ const dependencies = [];
142
+ this.#resolveBlock(block, this.#parse(block), dependencies);
143
+ return [...new Set(dependencies)];
144
+ }
145
+ // The classes a stylesheet file exports, without compiling or keeping
146
+ // anything, for tools that only need the names (such as the TypeScript
147
+ // content mapper).
148
+ classNames(sheet) {
149
+ const { options } = this.#library(sheet.file);
150
+ try {
151
+ const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
152
+ return [...parsed.classes.keys()];
153
+ }
154
+ catch (error) {
155
+ throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
156
+ }
157
+ }
158
+ // Each class of a stylesheet file, with its class string.
159
+ stylesheet(sheet) {
160
+ const parsed = this.#parseSheet(sheet);
161
+ const dependencies = [...parsed.dependencies];
162
+ const exports = [];
163
+ for (const name of parsed.classes.keys())
164
+ exports.push([
165
+ name,
166
+ this.#classString(this.#resolveClass(sheet.file, parsed, name, [], dependencies)),
167
+ ]);
168
+ return { exports, dependencies: [...new Set(dependencies)] };
110
169
  }
111
170
  css() {
112
171
  const sources = [];
@@ -124,8 +183,25 @@ export class Compiler {
124
183
  .filter((g) => g.global.hoist !== false)
125
184
  .toSorted((a, b) => a.global.hoist - b.global.hoist);
126
185
  const rest = globals.filter((g) => g.global.hoist === false);
127
- const rules = [...hoisted, ...rest].map((g) => withLoc(g.global.rule, loc(g.source)));
186
+ const named = (g) => withLoc(hasPlaceholder(g.global.key) ? this.#named(g.global.rule) : g.global.rule, loc(g.source));
187
+ let rules = [...hoisted, ...rest].map(named);
128
188
  rules.push(...this.#atomRules(loc));
189
+ // `@import` and `@namespace` can't be inside a layer, so they stay
190
+ // before it. `@layer` statements go inside, so the layers they order
191
+ // are the same ones blocks' `@layer` rules add to.
192
+ if (this.#layer !== null) {
193
+ rules = [
194
+ ...rules.filter(outside),
195
+ {
196
+ type: 'layer-block',
197
+ value: {
198
+ ...valueOf(this.#layer),
199
+ rules: rules.filter((r) => !outside(r)),
200
+ loc: loc(null),
201
+ },
202
+ },
203
+ ];
204
+ }
129
205
  return print(rules, sources, this.#parseOptions.lightningcss, this.#options.sourceMap ?? false);
130
206
  }
131
207
  // Classes on the element have an empty prefix.
@@ -141,17 +217,29 @@ export class Compiler {
141
217
  }
142
218
  return table;
143
219
  }
220
+ // A library's blocks and files get its prepended files and keyframes
221
+ // setting instead of the app's.
222
+ #library(file) {
223
+ const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
224
+ const { prepend, keyframeLocalization } = library === -1
225
+ ? { prepend: this.#prepend, keyframeLocalization: this.#options.keyframeLocalization }
226
+ : this.#libraries[library];
227
+ return {
228
+ library,
229
+ options: {
230
+ ...this.#parseOptions,
231
+ prepend,
232
+ keyframeLocalization: keyframeLocalization ?? false,
233
+ },
234
+ };
235
+ }
144
236
  #parse(block) {
145
- // A library's blocks get its prepended files instead of the app's.
146
- const library = block.file === undefined
147
- ? -1
148
- : this.#libraries.findIndex(({ dir }) => block.file.startsWith(dir));
149
- const prepend = library === -1 ? this.#prepend : this.#libraries[library].prepend;
237
+ const { library, options } = this.#library(block.file);
150
238
  const cacheKey = `${library}\n${block.lang}\n${block.code}`;
151
239
  let parsed = this.#blocks.get(cacheKey);
152
240
  if (parsed === undefined) {
153
241
  try {
154
- parsed = parseBlock(block.code, block.lang, { ...this.#parseOptions, prepend });
242
+ parsed = parseBlock(block.code, block.lang, options);
155
243
  }
156
244
  catch (error) {
157
245
  throw error instanceof PreStyleError ? locate(error, block) : error;
@@ -160,6 +248,146 @@ export class Compiler {
160
248
  }
161
249
  return parsed;
162
250
  }
251
+ // Parses a stylesheet file and registers its classes and global rules, so
252
+ // the output includes them once the file is used.
253
+ #parseSheet(sheet) {
254
+ const { library, options } = this.#library(sheet.file);
255
+ const cacheKey = `${library}\n${sheet.lang}\n${sheet.file}\n${sheet.code}`;
256
+ let parsed = this.#stylesheets.get(cacheKey);
257
+ if (parsed === undefined) {
258
+ try {
259
+ parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), options);
260
+ }
261
+ catch (error) {
262
+ throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
263
+ }
264
+ this.#stylesheets.set(cacheKey, parsed);
265
+ }
266
+ const block = fileBlock(sheet.file);
267
+ for (const target of parsed.classes.values())
268
+ this.#register(block, { atoms: target.atoms, globals: [], composes: [] });
269
+ this.#register(block, { atoms: [], globals: parsed.globals, composes: [] });
270
+ return parsed;
271
+ }
272
+ // Reads a file named by `composes … from`.
273
+ #readSheet(file) {
274
+ let code;
275
+ try {
276
+ code = readFileSync(file, 'utf8');
277
+ }
278
+ catch (error) {
279
+ throw new PreStyleError(`Can't read ${file}, named by \`composes\`.`, null, { cause: error });
280
+ }
281
+ return this.#parseSheet({ code, lang: file.endsWith('.scss') ? 'scss' : 'css', file });
282
+ }
283
+ // A class's atoms and other names, after its `composes`. Composed atoms
284
+ // come first, so the class's own declarations replace the ones they
285
+ // conflict with, as in `cn`. `stack` holds the classes being resolved, to
286
+ // find cycles. Files composed from are added to `dependencies`.
287
+ #resolveClass(file, parsed, name, stack, dependencies) {
288
+ const target = parsed.classes.get(name);
289
+ if (target === undefined)
290
+ throw new PreStyleError(`${file} has no class \`${name}\` to compose.`, null);
291
+ const id = `${file}\0${name}`;
292
+ if (stack.includes(id))
293
+ throw new PreStyleError(`\`composes\` cycle: ${[...stack, id].map((entry) => entry.replace('\0', ' .')).join(' -> ')}.`, null);
294
+ const composed = this.#resolveComposes(file, parsed, target.composes, [...stack, id], dependencies);
295
+ if (target.marker)
296
+ composed.extras.push(markerKey(relative(this.#root, file), name));
297
+ return {
298
+ atoms: composed.atoms.length === 0
299
+ ? target.atoms
300
+ : dropOverridden([...composed.atoms, ...target.atoms]),
301
+ extras: composed.extras,
302
+ };
303
+ }
304
+ #resolveBlock(block, parsed, dependencies) {
305
+ if (parsed.composes.length === 0)
306
+ return { atoms: parsed.atoms, extras: [] };
307
+ for (const c of parsed.composes) {
308
+ if (c.from.type === 'local')
309
+ throw new PreStyleError('Blocks have no classes of their own to compose. Use `composes: a from "./file.css";` or `composes: a from global;`.', c.position);
310
+ }
311
+ const file = block.file ?? resolvePath(this.#root, 'block.css');
312
+ let composed;
313
+ try {
314
+ composed = this.#resolveComposes(file, null, parsed.composes, [], dependencies);
315
+ }
316
+ catch (error) {
317
+ throw error instanceof PreStyleError ? locate(error, block) : error;
318
+ }
319
+ return { atoms: dropOverridden([...composed.atoms, ...parsed.atoms]), extras: composed.extras };
320
+ }
321
+ #resolveComposes(file, parsed, composes, stack, dependencies) {
322
+ const atoms = [];
323
+ const extras = [];
324
+ for (const c of composes) {
325
+ if (c.from.type === 'global') {
326
+ extras.push(...c.names);
327
+ continue;
328
+ }
329
+ let from = file;
330
+ let sheet = parsed;
331
+ if (c.from.type === 'file') {
332
+ from = resolvePath(dirname(file), c.from.path);
333
+ dependencies.push(from);
334
+ sheet = this.#readSheet(from);
335
+ dependencies.push(...sheet.dependencies);
336
+ }
337
+ for (const name of c.names) {
338
+ const resolved = this.#resolveClass(from, sheet, name, stack, dependencies);
339
+ atoms.push(...resolved.atoms);
340
+ extras.push(...resolved.extras);
341
+ }
342
+ }
343
+ return { atoms, extras };
344
+ }
345
+ #count(resolved) {
346
+ for (const atom of resolved.atoms) {
347
+ count(this.#counts.atoms, atomKey(atom));
348
+ const prefix = prefixKey(atom);
349
+ if (prefix !== '')
350
+ count(this.#counts.prefixes, prefix);
351
+ this.#countKeyframes(atom.declaration.key);
352
+ }
353
+ for (const extra of resolved.extras)
354
+ if (extra.startsWith(MARKER_PREFIX))
355
+ count(this.#counts.atoms, extra);
356
+ }
357
+ #classString(resolved) {
358
+ const names = resolved.atoms.map((atom) => this.#nameOf(this.#atoms.get(atomKey(atom))));
359
+ for (const extra of resolved.extras)
360
+ names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
361
+ return [...new Set(names)].join(' ');
362
+ }
363
+ // Counts the localized keyframes a JSON key names.
364
+ #countKeyframes(json) {
365
+ if (!json.includes(KEYFRAMES_KEY))
366
+ return;
367
+ for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
368
+ count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
369
+ }
370
+ // Replaces markers in selectors, and localized keyframes names, with their
371
+ // names.
372
+ #named(value) {
373
+ const visit = (node) => {
374
+ if (Array.isArray(node))
375
+ return node.map(visit);
376
+ if (node === null || typeof node !== 'object')
377
+ return node;
378
+ const record = node;
379
+ if (record.type === 'class' &&
380
+ typeof record.name === 'string' &&
381
+ record.name.startsWith(MARKER_PREFIX))
382
+ return { ...record, name: this.#names.atoms.set(record.name) };
383
+ if (record.type === 'ident' &&
384
+ typeof record.value === 'string' &&
385
+ record.value.startsWith(KEYFRAMES_PREFIX))
386
+ return { ...record, value: this.#names.keyframes.set(record.value) };
387
+ return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, visit(v)]));
388
+ };
389
+ return visit(value);
390
+ }
163
391
  #register(block, parsed) {
164
392
  for (const atom of parsed.atoms) {
165
393
  const key = atomKey(atom);
@@ -211,12 +439,17 @@ export class Compiler {
211
439
  const rules = sortAtoms(group.atoms).map((record) => {
212
440
  const { atom, source } = record;
213
441
  const className = this.#nameOf(record);
214
- const selectors = atom.variant
442
+ let selectors = atom.variant
215
443
  ? withClass(atom.variant.selectors, className)
216
444
  : [[{ type: 'class', name: className }]];
445
+ if (atom.variant?.key.includes(MARKER_KEY))
446
+ selectors = this.#named(selectors);
447
+ const value = atom.declaration.key.includes(KEYFRAMES_KEY)
448
+ ? this.#named(atom.declaration.value)
449
+ : atom.declaration.value;
217
450
  const declarations = atom.declaration.important
218
- ? { importantDeclarations: [atom.declaration.value] }
219
- : { declarations: [atom.declaration.value] };
451
+ ? { importantDeclarations: [value] }
452
+ : { declarations: [value] };
220
453
  return {
221
454
  type: 'style',
222
455
  value: { selectors, declarations, rules: [], loc: loc(source) },
@@ -233,6 +466,42 @@ export class Compiler {
233
466
  return emit(root);
234
467
  }
235
468
  }
469
+ // Markers and localized keyframes names as they appear in JSON keys, which
470
+ // escape the `\0`.
471
+ const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
472
+ const KEYFRAMES_KEY = JSON.stringify(KEYFRAMES_PREFIX).slice(1, -1);
473
+ const KEYFRAMES_KEY_RE = new RegExp(`${RegExp.escape(KEYFRAMES_KEY)}([\\w-]+)`, 'g');
474
+ // Rules that can't be inside a layer.
475
+ const outside = (rule) => rule.type === 'import' || rule.type === 'namespace';
476
+ const hasPlaceholder = (json) => json.includes(MARKER_KEY) || json.includes(KEYFRAMES_KEY);
477
+ // Parses the `layer` option into an `@layer` block rule.
478
+ function layerRule(layer) {
479
+ if (layer === undefined || layer === false)
480
+ return null;
481
+ if (typeof layer !== 'string')
482
+ throw new PreStyleError('`layer` must be `false` or a string.', null);
483
+ let rule;
484
+ try {
485
+ transform({
486
+ filename: 'layer.css',
487
+ code: Buffer.from(`@layer ${layer} {}`),
488
+ visitor: {
489
+ StyleSheetExit(sheet) {
490
+ if (sheet.rules.length === 1)
491
+ rule = sheet.rules[0];
492
+ },
493
+ },
494
+ });
495
+ }
496
+ catch {
497
+ // Reported below.
498
+ }
499
+ if (rule?.type !== 'layer-block' || (layer.trim() === '') !== (rule.value.name == null))
500
+ 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);
501
+ return rule;
502
+ }
503
+ // A stylesheet file, for positions and source maps.
504
+ const fileBlock = (file) => ({ code: '', lang: 'css', file, line: 1, column: 1 });
236
505
  // Reads prepended files, relative to `dir`, once.
237
506
  function readPrepend(dir, files) {
238
507
  const read = (list = []) => list.map((file) => readFileSync(resolvePath(dir, file), 'utf8')).join('\n');
@@ -0,0 +1,16 @@
1
+ import type { PrependFiles } from './compiler.ts';
2
+ import type { LibraryConfig } from './libraries.ts';
3
+ export interface PackageConfig {
4
+ include?: string | string[];
5
+ exclude?: string | string[];
6
+ prependFiles?: PrependFiles;
7
+ libraries?: Record<string, LibraryConfig | boolean>;
8
+ quirksMode?: boolean;
9
+ keyframeLocalization?: boolean;
10
+ }
11
+ export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
12
+ export declare function readConfig(root: string): {
13
+ dir: string;
14
+ config: PackageConfig;
15
+ warnings: string[];
16
+ };
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ const KEYS = new Set([
4
+ 'include',
5
+ 'exclude',
6
+ 'prependFiles',
7
+ 'libraries',
8
+ 'quirksMode',
9
+ 'keyframeLocalization',
10
+ ]);
11
+ const isStrings = (value) => typeof value === 'string' ||
12
+ (Array.isArray(value) && value.every((item) => typeof item === 'string'));
13
+ const isPrependFiles = (value) => (Array.isArray(value) && isStrings(value)) ||
14
+ (value !== null &&
15
+ typeof value === 'object' &&
16
+ !Array.isArray(value) &&
17
+ Object.entries(value).every(([lang, files]) => (lang === 'css' || lang === 'scss') && Array.isArray(files) && isStrings(files)));
18
+ // Checks a `pre.style` field's shared keys, and returns warnings for keys
19
+ // it doesn't know. `where` names the field in errors.
20
+ export function checkField(field, where, keys) {
21
+ const fail = (key, expected) => {
22
+ throw new Error(`[pre.style] ${where}: \`${key}\` must be ${expected}.`);
23
+ };
24
+ if (field.include !== undefined && !isStrings(field.include))
25
+ fail('include', 'a glob or an array of globs');
26
+ if (field.exclude !== undefined && !isStrings(field.exclude))
27
+ fail('exclude', 'a glob or an array of globs');
28
+ if (field.prependFiles !== undefined && !isPrependFiles(field.prependFiles))
29
+ fail('prependFiles', 'an array of paths, or an object with `css` and `scss` arrays');
30
+ if (field.quirksMode !== undefined && typeof field.quirksMode !== 'boolean')
31
+ fail('quirksMode', '`true` or `false`');
32
+ if (field.keyframeLocalization !== undefined && typeof field.keyframeLocalization !== 'boolean')
33
+ fail('keyframeLocalization', '`true` or `false`');
34
+ if (field.libraries !== undefined &&
35
+ (field.libraries === null ||
36
+ typeof field.libraries !== 'object' ||
37
+ Array.isArray(field.libraries)))
38
+ fail('libraries', 'an object of package names');
39
+ return Object.keys(field)
40
+ .filter((key) => !keys.has(key))
41
+ .map((key) => `${where}: unknown key \`${key}\`.`);
42
+ }
43
+ // Reads the `pre.style` field from the package.json nearest `root`. `dir`
44
+ // is the package's directory, which the field's paths are relative to.
45
+ export function readConfig(root) {
46
+ for (let dir = root;; dir = dirname(dir)) {
47
+ const file = join(dir, 'package.json');
48
+ if (existsSync(file)) {
49
+ const field = JSON.parse(readFileSync(file, 'utf8'))['pre.style'];
50
+ if (field === undefined)
51
+ return { dir, config: {}, warnings: [] };
52
+ const where = `"pre.style" in ${file}`;
53
+ if (field === null || typeof field !== 'object' || Array.isArray(field))
54
+ throw new Error(`[pre.style] ${where} must be an object.`);
55
+ const warnings = checkField(field, where, KEYS);
56
+ return { dir, config: field, warnings };
57
+ }
58
+ if (dirname(dir) === dir)
59
+ return { dir: root, config: {}, warnings: [] };
60
+ }
61
+ }
@@ -1,5 +1,10 @@
1
1
  export { Compiler, createCompiler } from './compiler.ts';
2
- export type { Block, CompilerOptions, Library, PrependFiles } from './compiler.ts';
2
+ export type { Block, CompiledStylesheet, CompilerOptions, Library, PrependFiles, 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';
6
+ export { checkField, readConfig } from './config.ts';
7
+ export type { PackageConfig } from './config.ts';
8
+ export { findLibraries } from './libraries.ts';
9
+ export type { LibraryConfig, ResolvedLibrary } from './libraries.ts';
10
+ export { isIdentifier } from './stylesheet.ts';
@@ -1,2 +1,5 @@
1
1
  export { Compiler, createCompiler } from './compiler.js';
2
2
  export { PreStyleError } from './block.js';
3
+ export { checkField, readConfig } from './config.js';
4
+ export { findLibraries } from './libraries.js';
5
+ export { isIdentifier } from './stylesheet.js';
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ import type { PrependFiles } from './compiler.ts';
2
+ export interface LibraryConfig {
3
+ include?: string | string[];
4
+ exclude?: string | string[];
5
+ prependFiles?: PrependFiles;
6
+ keyframeLocalization?: boolean;
7
+ }
8
+ export interface ResolvedLibrary {
9
+ name: string;
10
+ dir: string;
11
+ include: string[];
12
+ exclude: string[];
13
+ prependFiles?: PrependFiles;
14
+ keyframeLocalization?: boolean;
15
+ }
16
+ export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];