@pre.style/core 3.1.0-alpha.1 → 3.1.0-alpha.3
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 +395 -6
- package/dist/cn.d.ts +2 -1
- package/dist/cn.js +4 -2
- package/dist/compiler/block.d.ts +3 -0
- package/dist/compiler/block.js +17 -4
- package/dist/compiler/compiler.d.ts +36 -0
- package/dist/compiler/compiler.js +187 -44
- package/dist/compiler/config.d.ts +1 -0
- package/dist/compiler/config.js +4 -0
- package/dist/compiler/custom-properties.d.ts +5 -0
- package/dist/compiler/custom-properties.js +150 -0
- package/dist/compiler/index.d.ts +3 -2
- package/dist/compiler/index.js +1 -1
- package/dist/compiler/interpolate.d.ts +11 -0
- package/dist/compiler/interpolate.js +282 -0
- package/dist/compiler/libraries.d.ts +2 -0
- package/dist/compiler/libraries.js +9 -2
- package/dist/compiler/mapper.js +3 -1
- package/dist/compiler/selectors.d.ts +3 -0
- package/dist/compiler/selectors.js +38 -0
- package/dist/compiler/stylesheet.js +9 -0
- package/dist/compiler/variables.d.ts +13 -0
- package/dist/compiler/variables.js +272 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/runtime-dev.d.ts +2 -0
- package/dist/runtime-dev.js +9 -0
- package/dist/runtime.d.ts +16 -0
- package/dist/runtime.js +99 -0
- package/package.json +11 -1
- package/package.schema.json +8 -0
|
@@ -3,10 +3,13 @@ import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
|
|
|
3
3
|
import { transform } from 'lightningcss';
|
|
4
4
|
import SweatMap from 'sweatmap';
|
|
5
5
|
import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
|
|
6
|
+
import { interpolate } from './interpolate.js';
|
|
6
7
|
import { print } from './print.js';
|
|
7
|
-
import { lvhfaRank, withClass } from './selectors.js';
|
|
8
|
+
import { lvhfaRank, specificity, withClass } from './selectors.js';
|
|
8
9
|
import { KEYFRAMES_PREFIX } from './keyframes.js';
|
|
10
|
+
import { VARIABLE_PREFIX } from './variables.js';
|
|
9
11
|
import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
|
|
12
|
+
import { SHORTHANDS } from '../shorthands.js';
|
|
10
13
|
const RANGES = {
|
|
11
14
|
Digits: { start: '0030', end: '0039' },
|
|
12
15
|
Uppercase: { start: '0041', end: '005A' },
|
|
@@ -30,6 +33,16 @@ const QUIRKS_NAME_OPTIONS = {
|
|
|
30
33
|
cssSafe: true,
|
|
31
34
|
ranges: Object.fromEntries(Object.entries(RANGES).filter(([name]) => name !== 'Uppercase')),
|
|
32
35
|
};
|
|
36
|
+
// A block with `${…}`, from the text around its expressions and their source.
|
|
37
|
+
export function interpolatedBlock(block) {
|
|
38
|
+
const { strings, expressions, ...rest } = block;
|
|
39
|
+
try {
|
|
40
|
+
return { ...rest, ...interpolate(strings, expressions, block.lang) };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw error instanceof PreStyleError ? locate(error, { ...rest, code: '' }) : error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
33
46
|
export async function createCompiler(options = {}) {
|
|
34
47
|
let sass = null;
|
|
35
48
|
try {
|
|
@@ -54,19 +67,22 @@ export class Compiler {
|
|
|
54
67
|
#root;
|
|
55
68
|
// One name list for every class, one for prefixes (the at-rules and
|
|
56
69
|
// selector variant a class is under), which only the `cn` table uses, and
|
|
57
|
-
// one for localized keyframes.
|
|
70
|
+
// one each for localized keyframes and runtime variables.
|
|
58
71
|
#names;
|
|
59
|
-
// How often each class, prefix,
|
|
60
|
-
// scanned blocks.
|
|
72
|
+
// How often each class, prefix, localized keyframes, and variable is used
|
|
73
|
+
// across scanned blocks.
|
|
61
74
|
#counts = {
|
|
62
75
|
atoms: new Map(),
|
|
63
76
|
prefixes: new Map(),
|
|
64
77
|
keyframes: new Map(),
|
|
78
|
+
variables: new Map(),
|
|
65
79
|
};
|
|
66
80
|
#atoms = new Map();
|
|
67
81
|
#globals = new Map();
|
|
68
82
|
// The current global for each overridable at-rule.
|
|
69
83
|
#overrides = new Map();
|
|
84
|
+
// Each runtime variable's custom property, or null for `--<its name>`.
|
|
85
|
+
#variables = new Map();
|
|
70
86
|
#order = 0;
|
|
71
87
|
constructor(options, sass) {
|
|
72
88
|
this.#options = options;
|
|
@@ -77,6 +93,8 @@ export class Compiler {
|
|
|
77
93
|
// four characters only after about 227,000 names, so they're never
|
|
78
94
|
// `none` or a CSS-wide keyword.
|
|
79
95
|
keyframes: new SweatMap(NAME_OPTIONS),
|
|
96
|
+
// Part of the classes generated at runtime, so these follow class names.
|
|
97
|
+
variables: new SweatMap(options.quirksMode ? QUIRKS_NAME_OPTIONS : NAME_OPTIONS),
|
|
80
98
|
};
|
|
81
99
|
this.#layer = layerRule(options.layer);
|
|
82
100
|
const root = options.root ?? process.cwd();
|
|
@@ -89,6 +107,7 @@ export class Compiler {
|
|
|
89
107
|
dir: dir + sep,
|
|
90
108
|
prepend: readPrepend(dir, library.prependFiles),
|
|
91
109
|
keyframeLocalization: library.keyframeLocalization ?? false,
|
|
110
|
+
customPropertyLocalization: library.customPropertyLocalization ?? false,
|
|
92
111
|
};
|
|
93
112
|
})
|
|
94
113
|
.toSorted((a, b) => b.dir.length - a.dir.length);
|
|
@@ -110,7 +129,9 @@ export class Compiler {
|
|
|
110
129
|
this.#register(block, parsed);
|
|
111
130
|
this.#count(this.#resolveBlock(block, parsed, []));
|
|
112
131
|
for (const global of parsed.globals)
|
|
113
|
-
this.#
|
|
132
|
+
this.#countNames(global.key);
|
|
133
|
+
for (const variable of parsed.variables ?? [])
|
|
134
|
+
count(this.#counts.variables, variable.key);
|
|
114
135
|
}
|
|
115
136
|
// Pass 1 of naming for a stylesheet file: counts what each class uses.
|
|
116
137
|
scanStylesheet(sheet) {
|
|
@@ -118,12 +139,12 @@ export class Compiler {
|
|
|
118
139
|
for (const name of parsed.classes.keys())
|
|
119
140
|
this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
|
|
120
141
|
for (const global of parsed.globals)
|
|
121
|
-
this.#
|
|
142
|
+
this.#countNames(global.key);
|
|
122
143
|
}
|
|
123
144
|
// Pass 2 of naming: gives the most-used entries the shortest names. Ties
|
|
124
145
|
// are broken by key, so names don't depend on the order files are read.
|
|
125
146
|
assignNames() {
|
|
126
|
-
for (const list of ['atoms', 'prefixes', 'keyframes']) {
|
|
147
|
+
for (const list of ['atoms', 'prefixes', 'keyframes', 'variables']) {
|
|
127
148
|
const sorted = [...this.#counts[list]].toSorted(([a, countA], [b, countB]) => countB - countA || (a < b ? -1 : a > b ? 1 : 0));
|
|
128
149
|
for (const [key] of sorted)
|
|
129
150
|
this.#names[list].set(key);
|
|
@@ -136,6 +157,20 @@ export class Compiler {
|
|
|
136
157
|
this.#register(block, parsed);
|
|
137
158
|
return this.#classString(this.#resolveBlock(block, parsed, []));
|
|
138
159
|
}
|
|
160
|
+
// The variables a block sets at runtime, in the order they're written.
|
|
161
|
+
variables(block) {
|
|
162
|
+
const parsed = this.#parse(block);
|
|
163
|
+
this.#register(block, parsed);
|
|
164
|
+
return (parsed.variables ?? []).map((variable) => {
|
|
165
|
+
const name = this.#names.variables.set(variable.key);
|
|
166
|
+
return {
|
|
167
|
+
name,
|
|
168
|
+
property: variable.property ? this.#propertyName(variable.property) : `--${name}`,
|
|
169
|
+
slot: block.slots[variable.slot],
|
|
170
|
+
selector: variable.selector,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
139
174
|
// The files a block's class string depends on, through `composes … from`.
|
|
140
175
|
dependencies(block) {
|
|
141
176
|
const dependencies = [];
|
|
@@ -155,6 +190,34 @@ export class Compiler {
|
|
|
155
190
|
throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
|
|
156
191
|
}
|
|
157
192
|
}
|
|
193
|
+
// Where each of the given classes is used, in output order. Classes that
|
|
194
|
+
// aren't in the output are left out. Sources are only kept with `sourceMap`
|
|
195
|
+
// on, and only list what has been compiled so far.
|
|
196
|
+
usages(classNames) {
|
|
197
|
+
const wanted = new Set(classNames);
|
|
198
|
+
const usages = [];
|
|
199
|
+
let order = 0;
|
|
200
|
+
const walk = (group) => {
|
|
201
|
+
for (const record of sortAtoms(group.atoms)) {
|
|
202
|
+
order++;
|
|
203
|
+
if (record.className !== null && wanted.has(record.className))
|
|
204
|
+
usages.push(this.#usage(record, order));
|
|
205
|
+
}
|
|
206
|
+
for (const child of sortGroups([...group.children.values()]))
|
|
207
|
+
walk(child);
|
|
208
|
+
};
|
|
209
|
+
walk(this.#atomGroups());
|
|
210
|
+
return usages;
|
|
211
|
+
}
|
|
212
|
+
// Drops the places a file uses atoms, because it changed. They're added back
|
|
213
|
+
// as its blocks are compiled again. The atoms stay, like the rest of dev's
|
|
214
|
+
// output, which only grows.
|
|
215
|
+
forgetSources(file) {
|
|
216
|
+
for (const record of this.#atoms.values())
|
|
217
|
+
for (const [key, source] of record.sources)
|
|
218
|
+
if (source.file === file)
|
|
219
|
+
record.sources.delete(key);
|
|
220
|
+
}
|
|
158
221
|
// Each class of a stylesheet file, with its class string.
|
|
159
222
|
stylesheet(sheet) {
|
|
160
223
|
const parsed = this.#parseSheet(sheet);
|
|
@@ -211,18 +274,29 @@ export class Compiler {
|
|
|
211
274
|
const key = prefixKey(record.atom);
|
|
212
275
|
const prefix = key === '' ? '' : this.#names.prefixes.set(key);
|
|
213
276
|
const properties = (table[prefix] ??= {});
|
|
214
|
-
const
|
|
277
|
+
const property = this.#propertyName(record.atom.property);
|
|
215
278
|
const name = this.#nameOf(record);
|
|
216
279
|
properties[property] = properties[property] ? `${properties[property]} ${name}` : name;
|
|
217
280
|
}
|
|
218
281
|
return table;
|
|
219
282
|
}
|
|
283
|
+
// The variables runtime values set, for `cn` to recognize generated classes.
|
|
284
|
+
cnVariables() {
|
|
285
|
+
const variables = {};
|
|
286
|
+
for (const [key, property] of this.#variables)
|
|
287
|
+
variables[this.#names.variables.set(key)] = property ? this.#propertyName(property) : '';
|
|
288
|
+
return variables;
|
|
289
|
+
}
|
|
220
290
|
// A library's blocks and files get its prepended files and keyframes
|
|
221
291
|
// setting instead of the app's.
|
|
222
292
|
#library(file) {
|
|
223
293
|
const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
|
|
224
|
-
const { prepend, keyframeLocalization } = library === -1
|
|
225
|
-
? {
|
|
294
|
+
const { prepend, keyframeLocalization, customPropertyLocalization } = library === -1
|
|
295
|
+
? {
|
|
296
|
+
prepend: this.#prepend,
|
|
297
|
+
keyframeLocalization: this.#options.keyframeLocalization,
|
|
298
|
+
customPropertyLocalization: this.#options.customPropertyLocalization,
|
|
299
|
+
}
|
|
226
300
|
: this.#libraries[library];
|
|
227
301
|
return {
|
|
228
302
|
library,
|
|
@@ -230,6 +304,7 @@ export class Compiler {
|
|
|
230
304
|
...this.#parseOptions,
|
|
231
305
|
prepend,
|
|
232
306
|
keyframeLocalization: keyframeLocalization ?? false,
|
|
307
|
+
customPropertyLocalization: customPropertyLocalization ?? false,
|
|
233
308
|
},
|
|
234
309
|
};
|
|
235
310
|
}
|
|
@@ -348,7 +423,7 @@ export class Compiler {
|
|
|
348
423
|
const prefix = prefixKey(atom);
|
|
349
424
|
if (prefix !== '')
|
|
350
425
|
count(this.#counts.prefixes, prefix);
|
|
351
|
-
this.#
|
|
426
|
+
this.#countNames(atom.declaration.key);
|
|
352
427
|
}
|
|
353
428
|
for (const extra of resolved.extras)
|
|
354
429
|
if (extra.startsWith(MARKER_PREFIX))
|
|
@@ -360,12 +435,18 @@ export class Compiler {
|
|
|
360
435
|
names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
|
|
361
436
|
return [...new Set(names)].join(' ');
|
|
362
437
|
}
|
|
363
|
-
// Counts the localized keyframes a JSON key names.
|
|
364
|
-
#
|
|
365
|
-
if (
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
438
|
+
// Counts the localized keyframes and custom properties a JSON key names.
|
|
439
|
+
#countNames(json) {
|
|
440
|
+
if (json.includes(KEYFRAMES_KEY))
|
|
441
|
+
for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
|
|
442
|
+
count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
|
|
443
|
+
if (json.includes(VARIABLE_KEY))
|
|
444
|
+
for (const [, hash] of json.matchAll(VARIABLE_KEY_RE))
|
|
445
|
+
count(this.#counts.variables, hash);
|
|
446
|
+
}
|
|
447
|
+
// A property's name in the output, for a custom property with a generated one.
|
|
448
|
+
#propertyName(property) {
|
|
449
|
+
return property.startsWith(VARIABLE_PREFIX) ? this.#named(property) : property;
|
|
369
450
|
}
|
|
370
451
|
// Replaces markers in selectors, and localized keyframes names, with their
|
|
371
452
|
// names.
|
|
@@ -373,6 +454,8 @@ export class Compiler {
|
|
|
373
454
|
const visit = (node) => {
|
|
374
455
|
if (Array.isArray(node))
|
|
375
456
|
return node.map(visit);
|
|
457
|
+
if (typeof node === 'string' && node.startsWith(VARIABLE_PREFIX))
|
|
458
|
+
return `--${this.#names.variables.set(node.slice(VARIABLE_PREFIX.length))}`;
|
|
376
459
|
if (node === null || typeof node !== 'object')
|
|
377
460
|
return node;
|
|
378
461
|
const record = node;
|
|
@@ -389,16 +472,30 @@ export class Compiler {
|
|
|
389
472
|
return visit(value);
|
|
390
473
|
}
|
|
391
474
|
#register(block, parsed) {
|
|
475
|
+
for (const variable of parsed.variables ?? []) {
|
|
476
|
+
if (this.#variables.has(variable.key))
|
|
477
|
+
continue;
|
|
478
|
+
this.#variables.set(variable.key, variable.property);
|
|
479
|
+
// A custom property set directly adds no atom or rule, but `cn` needs it.
|
|
480
|
+
this.#order++;
|
|
481
|
+
}
|
|
392
482
|
for (const atom of parsed.atoms) {
|
|
393
483
|
const key = atomKey(atom);
|
|
394
|
-
|
|
395
|
-
|
|
484
|
+
const source = sourceOf(block, atom.position);
|
|
485
|
+
let record = this.#atoms.get(key);
|
|
486
|
+
if (record === undefined) {
|
|
487
|
+
record = {
|
|
396
488
|
atom,
|
|
397
489
|
className: null,
|
|
398
490
|
order: this.#order++,
|
|
399
|
-
source
|
|
400
|
-
|
|
491
|
+
source,
|
|
492
|
+
sources: new Map(),
|
|
493
|
+
printed: null,
|
|
494
|
+
};
|
|
495
|
+
this.#atoms.set(key, record);
|
|
401
496
|
}
|
|
497
|
+
if (source !== null && this.#options.sourceMap)
|
|
498
|
+
record.sources.set(`${source.file}:${source.line}:${source.column}`, source);
|
|
402
499
|
}
|
|
403
500
|
for (const global of parsed.globals) {
|
|
404
501
|
if (this.#globals.has(global.key))
|
|
@@ -420,8 +517,8 @@ export class Compiler {
|
|
|
420
517
|
#nameOf(record) {
|
|
421
518
|
return (record.className ??= this.#names.atoms.set(atomKey(record.atom)));
|
|
422
519
|
}
|
|
423
|
-
|
|
424
|
-
|
|
520
|
+
// Atoms grouped by their at-rules, nesting groups the same way.
|
|
521
|
+
#atomGroups() {
|
|
425
522
|
const root = { segment: null, order: 0, atoms: [], children: new Map() };
|
|
426
523
|
for (const record of this.#atoms.values()) {
|
|
427
524
|
let group = root;
|
|
@@ -435,45 +532,91 @@ export class Compiler {
|
|
|
435
532
|
}
|
|
436
533
|
group.atoms.push(record);
|
|
437
534
|
}
|
|
535
|
+
return root;
|
|
536
|
+
}
|
|
537
|
+
#atomSelectors(record) {
|
|
538
|
+
const { atom } = record;
|
|
539
|
+
const className = this.#nameOf(record);
|
|
540
|
+
const selectors = atom.variant
|
|
541
|
+
? withClass(atom.variant.selectors, className)
|
|
542
|
+
: [[{ type: 'class', name: className }]];
|
|
543
|
+
return atom.variant?.key.includes(MARKER_KEY) ? this.#named(selectors) : selectors;
|
|
544
|
+
}
|
|
545
|
+
#atomRule(record, loc) {
|
|
546
|
+
const { atom } = record;
|
|
547
|
+
const value = hasGenerated(atom.declaration.key)
|
|
548
|
+
? this.#named(atom.declaration.value)
|
|
549
|
+
: atom.declaration.value;
|
|
550
|
+
const declarations = atom.declaration.important
|
|
551
|
+
? { importantDeclarations: [value] }
|
|
552
|
+
: { declarations: [value] };
|
|
553
|
+
return {
|
|
554
|
+
type: 'style',
|
|
555
|
+
value: { selectors: this.#atomSelectors(record), declarations, rules: [], loc },
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
#atomRules(loc) {
|
|
438
559
|
const emit = (group) => {
|
|
439
560
|
const rules = sortAtoms(group.atoms).map((record) => {
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
? withClass(atom.variant.selectors, className)
|
|
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;
|
|
450
|
-
const declarations = atom.declaration.important
|
|
451
|
-
? { importantDeclarations: [value] }
|
|
452
|
-
: { declarations: [value] };
|
|
453
|
-
return {
|
|
454
|
-
type: 'style',
|
|
455
|
-
value: { selectors, declarations, rules: [], loc: loc(source) },
|
|
456
|
-
};
|
|
561
|
+
// The first place it's still used, since `forgetSources` drops places.
|
|
562
|
+
const [source = record.source] = record.sources.values();
|
|
563
|
+
return this.#atomRule(record, loc(source));
|
|
457
564
|
});
|
|
458
565
|
for (const child of sortGroups([...group.children.values()])) {
|
|
566
|
+
const segment = child.segment;
|
|
567
|
+
const named = hasGenerated(segment.key) ? this.#named(segment.rule) : segment.rule;
|
|
459
568
|
rules.push({
|
|
460
|
-
type:
|
|
461
|
-
value: { ...valueOf(
|
|
569
|
+
type: segment.rule.type,
|
|
570
|
+
value: { ...valueOf(named), rules: emit(child) },
|
|
462
571
|
});
|
|
463
572
|
}
|
|
464
573
|
return rules;
|
|
465
574
|
};
|
|
466
|
-
return emit(
|
|
575
|
+
return emit(this.#atomGroups());
|
|
576
|
+
}
|
|
577
|
+
#usage(record, order) {
|
|
578
|
+
const { atom } = record;
|
|
579
|
+
const loc = { source_index: 0, line: 0, column: 1 };
|
|
580
|
+
const rule = this.#atomRule(record, loc);
|
|
581
|
+
// Printed for reading, so without the user's `minify` and `targets`.
|
|
582
|
+
const { drafts, nonStandard, errorRecovery } = this.#parseOptions.lightningcss;
|
|
583
|
+
const text = (rules) => print([rules], [], { drafts, nonStandard, errorRecovery }, false).code;
|
|
584
|
+
const code = (record.printed ??= text(rule));
|
|
585
|
+
const open = code.indexOf(' {\n');
|
|
586
|
+
return {
|
|
587
|
+
className: record.className,
|
|
588
|
+
selector: code.slice(0, open).replaceAll('\n', ' '),
|
|
589
|
+
atRules: atom.atRules.map((segment) => {
|
|
590
|
+
const named = hasGenerated(segment.key) ? this.#named(segment.rule) : segment.rule;
|
|
591
|
+
const wrapped = { ...valueOf(named), rules: [rule] };
|
|
592
|
+
const printed = text({ type: segment.rule.type, value: wrapped });
|
|
593
|
+
return printed.slice(0, printed.indexOf(' {\n'));
|
|
594
|
+
}),
|
|
595
|
+
declaration: code
|
|
596
|
+
.slice(open + 3, code.lastIndexOf('\n}'))
|
|
597
|
+
.trim()
|
|
598
|
+
.replace(/;$/, ''),
|
|
599
|
+
property: this.#propertyName(atom.property),
|
|
600
|
+
covers: Object.hasOwn(SHORTHANDS, atom.property) ? SHORTHANDS[atom.property] : [],
|
|
601
|
+
important: atom.declaration.important,
|
|
602
|
+
specificity: specificity(this.#atomSelectors(record)),
|
|
603
|
+
order,
|
|
604
|
+
sources: [...record.sources.values()],
|
|
605
|
+
};
|
|
467
606
|
}
|
|
468
607
|
}
|
|
469
608
|
// Markers and localized keyframes names as they appear in JSON keys, which
|
|
470
609
|
// escape the `\0`.
|
|
471
610
|
const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
|
|
472
611
|
const KEYFRAMES_KEY = JSON.stringify(KEYFRAMES_PREFIX).slice(1, -1);
|
|
612
|
+
const VARIABLE_KEY = JSON.stringify(VARIABLE_PREFIX).slice(1, -1);
|
|
613
|
+
const VARIABLE_KEY_RE = new RegExp(`${RegExp.escape(VARIABLE_KEY)}([\\w-]+)`, 'g');
|
|
473
614
|
const KEYFRAMES_KEY_RE = new RegExp(`${RegExp.escape(KEYFRAMES_KEY)}([\\w-]+)`, 'g');
|
|
474
615
|
// Rules that can't be inside a layer.
|
|
475
616
|
const outside = (rule) => rule.type === 'import' || rule.type === 'namespace';
|
|
476
|
-
|
|
617
|
+
// Whether a declaration has generated names.
|
|
618
|
+
const hasGenerated = (json) => json.includes(KEYFRAMES_KEY) || json.includes(VARIABLE_KEY);
|
|
619
|
+
const hasPlaceholder = (json) => json.includes(MARKER_KEY) || hasGenerated(json);
|
|
477
620
|
// Parses the `layer` option into an `@layer` block rule.
|
|
478
621
|
function layerRule(layer) {
|
|
479
622
|
if (layer === undefined || layer === false)
|
|
@@ -7,6 +7,7 @@ export interface PackageConfig {
|
|
|
7
7
|
libraries?: Record<string, LibraryConfig | boolean>;
|
|
8
8
|
quirksMode?: boolean;
|
|
9
9
|
keyframeLocalization?: boolean;
|
|
10
|
+
customPropertyLocalization?: boolean;
|
|
10
11
|
}
|
|
11
12
|
export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
|
|
12
13
|
export declare function readConfig(root: string): {
|
package/dist/compiler/config.js
CHANGED
|
@@ -7,6 +7,7 @@ const KEYS = new Set([
|
|
|
7
7
|
'libraries',
|
|
8
8
|
'quirksMode',
|
|
9
9
|
'keyframeLocalization',
|
|
10
|
+
'customPropertyLocalization',
|
|
10
11
|
]);
|
|
11
12
|
const isStrings = (value) => typeof value === 'string' ||
|
|
12
13
|
(Array.isArray(value) && value.every((item) => typeof item === 'string'));
|
|
@@ -31,6 +32,9 @@ export function checkField(field, where, keys) {
|
|
|
31
32
|
fail('quirksMode', '`true` or `false`');
|
|
32
33
|
if (field.keyframeLocalization !== undefined && typeof field.keyframeLocalization !== 'boolean')
|
|
33
34
|
fail('keyframeLocalization', '`true` or `false`');
|
|
35
|
+
if (field.customPropertyLocalization !== undefined &&
|
|
36
|
+
typeof field.customPropertyLocalization !== 'boolean')
|
|
37
|
+
fail('customPropertyLocalization', '`true` or `false`');
|
|
34
38
|
if (field.libraries !== undefined &&
|
|
35
39
|
(field.libraries === null ||
|
|
36
40
|
typeof field.libraries !== 'object' ||
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Atom, GlobalRule, Lang, ParseOptions } from './block.ts';
|
|
2
|
+
export declare function localizeCustomProperties(atoms: Atom[], globals: GlobalRule[], scope: string, lang: Lang, options: ParseOptions): {
|
|
3
|
+
globals: GlobalRule[];
|
|
4
|
+
atoms: (atoms: Atom[]) => Atom[];
|
|
5
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { key, overrideKey, parseRules, valueOf, walk } from './block.js';
|
|
3
|
+
import { VARIABLE_PREFIX } from './variables.js';
|
|
4
|
+
// With `customPropertyLocalization`, a custom property that a block or
|
|
5
|
+
// stylesheet file declares gets a generated name, used only in that block or
|
|
6
|
+
// file. In parsed rules it's a variable name, which the compiler names when
|
|
7
|
+
// printing, in the same list as the variables runtime values set. Names it only
|
|
8
|
+
// reads, and names from prepended files, which are how values are shared, stay
|
|
9
|
+
// global.
|
|
10
|
+
// Properties whose values name custom properties. Other dashed names, such as
|
|
11
|
+
// `anchor-name: --x` or `anchor(--x top)`, aren't custom properties.
|
|
12
|
+
const NAMING = new Set(['will-change', 'transition', 'transition-property']);
|
|
13
|
+
// Calls `declared` with each custom property a value declares.
|
|
14
|
+
function eachDeclared(value, declared) {
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
for (const item of value)
|
|
17
|
+
eachDeclared(item, declared);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (value === null || typeof value !== 'object')
|
|
21
|
+
return;
|
|
22
|
+
const node = value;
|
|
23
|
+
if (node.property === 'custom' && node.value?.name?.startsWith('--'))
|
|
24
|
+
declared(node.value.name);
|
|
25
|
+
if (node.type === 'property' && typeof node.value?.name === 'string')
|
|
26
|
+
declared(node.value.name);
|
|
27
|
+
for (const child of Object.values(node))
|
|
28
|
+
eachDeclared(child, declared);
|
|
29
|
+
}
|
|
30
|
+
// Replaces custom property names in a value. Returns `value` itself when nothing
|
|
31
|
+
// changed.
|
|
32
|
+
function rename(value, local, naming = false) {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
const mapped = value.map((item) => rename(item, local, naming));
|
|
35
|
+
return (mapped.some((item, i) => item !== value[i]) ? mapped : value);
|
|
36
|
+
}
|
|
37
|
+
if (value === null || typeof value !== 'object')
|
|
38
|
+
return value;
|
|
39
|
+
const node = value;
|
|
40
|
+
// `var(--x)`.
|
|
41
|
+
const ident = node.ident;
|
|
42
|
+
// `transition-property`'s `{ property: '--x' }`.
|
|
43
|
+
const property = node.property;
|
|
44
|
+
// `will-change: --x`.
|
|
45
|
+
const dashed = naming && node.type === 'dashed-ident' ? node.value : undefined;
|
|
46
|
+
const isNaming = naming ||
|
|
47
|
+
(node.property === 'custom' && NAMING.has(node.value?.name)) ||
|
|
48
|
+
(node.property === 'unparsed' && NAMING.has(node.value?.propertyId?.property));
|
|
49
|
+
let result = node;
|
|
50
|
+
const set = (field, next) => {
|
|
51
|
+
if (result === node)
|
|
52
|
+
result = { ...node };
|
|
53
|
+
result[field] = next;
|
|
54
|
+
};
|
|
55
|
+
for (const [field, child] of Object.entries(node)) {
|
|
56
|
+
const next = rename(child, local, isNaming);
|
|
57
|
+
if (next !== child)
|
|
58
|
+
set(field, next);
|
|
59
|
+
}
|
|
60
|
+
if (typeof ident === 'string' && local.has(ident))
|
|
61
|
+
set('ident', local.get(ident));
|
|
62
|
+
if (typeof property === 'string' && local.has(property))
|
|
63
|
+
set('property', local.get(property));
|
|
64
|
+
if (typeof dashed === 'string' && local.has(dashed))
|
|
65
|
+
set('value', local.get(dashed));
|
|
66
|
+
// `--x: 1` and `@property --x`.
|
|
67
|
+
if ((node.property === 'custom' || node.type === 'property') && local.has(result.value?.name))
|
|
68
|
+
set('value', { ...result.value, name: local.get(result.value.name) });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
const prepended = new Map();
|
|
72
|
+
// The rules and custom properties prepended files output. They're parsed once
|
|
73
|
+
// more on their own, and only with this setting on, since within a block they
|
|
74
|
+
// can't be told apart from its own rules.
|
|
75
|
+
function prependedOf(lang, options) {
|
|
76
|
+
const code = options.prepend[lang];
|
|
77
|
+
const cacheKey = `${lang}\n${code}`;
|
|
78
|
+
let result = prepended.get(cacheKey);
|
|
79
|
+
if (result !== undefined)
|
|
80
|
+
return result;
|
|
81
|
+
result = { keys: new Set(), names: new Set() };
|
|
82
|
+
if (code.trim() !== '') {
|
|
83
|
+
const css = lang === 'scss' && options.sass !== null ? options.sass.compileString(code).css : code;
|
|
84
|
+
const parsed = { atoms: [], globals: [], composes: [] };
|
|
85
|
+
parseRules(css, options.lightningcss, () => null, (rules) => walk(rules, [], null, parsed, () => null));
|
|
86
|
+
for (const global of parsed.globals) {
|
|
87
|
+
result.keys.add(global.key);
|
|
88
|
+
eachDeclared(global.rule, (name) => result.names.add(name));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
prepended.set(cacheKey, result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
// Localizes the custom properties a scope (a block, or a whole stylesheet file)
|
|
95
|
+
// declares. `scope` identifies it, and has to be stable between builds.
|
|
96
|
+
export function localizeCustomProperties(atoms, globals, scope, lang, options) {
|
|
97
|
+
const shared = prependedOf(lang, options);
|
|
98
|
+
const local = new Map();
|
|
99
|
+
const declared = (name) => {
|
|
100
|
+
if (shared.names.has(name) || local.has(name))
|
|
101
|
+
return;
|
|
102
|
+
const hash = createHash('sha256').update(`${scope}\n${name}`).digest('base64url').slice(0, 16);
|
|
103
|
+
local.set(name, VARIABLE_PREFIX + hash);
|
|
104
|
+
};
|
|
105
|
+
for (const atom of atoms)
|
|
106
|
+
eachDeclared(atom.declaration.value, declared);
|
|
107
|
+
const own = (global) => !shared.keys.has(global.key);
|
|
108
|
+
for (const global of globals)
|
|
109
|
+
if (own(global))
|
|
110
|
+
eachDeclared(global.rule, declared);
|
|
111
|
+
if (local.size === 0)
|
|
112
|
+
return { globals, atoms: (list) => list };
|
|
113
|
+
return {
|
|
114
|
+
globals: globals.map((global) => {
|
|
115
|
+
if (!own(global))
|
|
116
|
+
return global;
|
|
117
|
+
const rule = rename(global.rule, local);
|
|
118
|
+
if (rule === global.rule)
|
|
119
|
+
return global;
|
|
120
|
+
return {
|
|
121
|
+
...global,
|
|
122
|
+
key: key(rule),
|
|
123
|
+
rule,
|
|
124
|
+
overrideKey: global.overrideKey === null ? null : overrideKey(rule),
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
atoms: (list) => list.map((atom) => {
|
|
128
|
+
const value = rename(atom.declaration.value, local);
|
|
129
|
+
const atRules = atom.atRules.map((segment) => {
|
|
130
|
+
const rule = rename(segment.rule, local);
|
|
131
|
+
return rule === segment.rule ? segment : { key: key(valueKey(rule)), rule };
|
|
132
|
+
});
|
|
133
|
+
const unchanged = value === atom.declaration.value && atRules.every((s, i) => s === atom.atRules[i]);
|
|
134
|
+
if (unchanged)
|
|
135
|
+
return atom;
|
|
136
|
+
return {
|
|
137
|
+
...atom,
|
|
138
|
+
atRules,
|
|
139
|
+
property: local.get(atom.property) ?? atom.property,
|
|
140
|
+
declaration: {
|
|
141
|
+
...atom.declaration,
|
|
142
|
+
key: key(value) + (atom.declaration.important ? '!' : ''),
|
|
143
|
+
value,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// The same key `walk` gives an at-rule.
|
|
150
|
+
const valueKey = (rule) => ({ type: rule.type, value: valueOf(rule) });
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { Compiler, createCompiler } from './compiler.ts';
|
|
2
|
-
export type { Block, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Stylesheet, } from './compiler.ts';
|
|
1
|
+
export { Compiler, createCompiler, interpolatedBlock } from './compiler.ts';
|
|
2
|
+
export type { Block, BlockVariable, 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
|
+
export type { Slot } from './interpolate.ts';
|
|
5
6
|
export type { Printed } from './print.ts';
|
|
6
7
|
export { checkField, readConfig } from './config.ts';
|
|
7
8
|
export type { PackageConfig } from './config.ts';
|
package/dist/compiler/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Compiler, createCompiler } from './compiler.js';
|
|
1
|
+
export { Compiler, createCompiler, interpolatedBlock } from './compiler.js';
|
|
2
2
|
export { PreStyleError } from './block.js';
|
|
3
3
|
export { checkField, readConfig } from './config.js';
|
|
4
4
|
export { findLibraries } from './libraries.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Lang } from './block.ts';
|
|
2
|
+
export interface Slot {
|
|
3
|
+
strings: string[];
|
|
4
|
+
expressions: number[];
|
|
5
|
+
}
|
|
6
|
+
export interface Interpolated {
|
|
7
|
+
code: string;
|
|
8
|
+
slots: Slot[];
|
|
9
|
+
}
|
|
10
|
+
export declare const SLOT_PREFIX = "--__pre-style-slot-";
|
|
11
|
+
export declare function interpolate(strings: string[], expressions: string[], lang: Lang): Interpolated;
|