@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.
- package/README.md +154 -72
- package/dist/compiler/block.d.ts +2 -0
- package/dist/compiler/block.js +9 -7
- package/dist/compiler/compiler.d.ts +22 -0
- package/dist/compiler/compiler.js +213 -43
- package/dist/compiler/config.d.ts +1 -0
- package/dist/compiler/config.js +10 -1
- package/dist/compiler/index.d.ts +1 -1
- package/dist/compiler/keyframes.d.ts +6 -0
- package/dist/compiler/keyframes.js +120 -0
- package/dist/compiler/libraries.d.ts +2 -0
- package/dist/compiler/libraries.js +3 -2
- package/dist/compiler/mapper.js +6 -1
- package/dist/compiler/selectors.d.ts +3 -0
- package/dist/compiler/selectors.js +38 -0
- package/dist/compiler/stylesheet.js +8 -0
- package/package.json +1 -1
- package/package.schema.json +8 -0
|
@@ -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,
|
|
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
|
|
55
|
-
|
|
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 {
|
|
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 {
|
|
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),
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
335
|
-
|
|
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
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
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(
|
|
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
|
|
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): {
|
package/dist/compiler/config.js
CHANGED
|
@@ -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([
|
|
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' ||
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -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,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.
|
package/dist/compiler/mapper.js
CHANGED
|
@@ -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 }) => ({
|
|
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 {};
|