@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.
- package/README.md +332 -76
- package/dist/compiler/block.d.ts +30 -0
- package/dist/compiler/block.js +86 -26
- package/dist/compiler/compiler.d.ts +16 -0
- package/dist/compiler/compiler.js +294 -25
- package/dist/compiler/config.d.ts +16 -0
- package/dist/compiler/config.js +61 -0
- package/dist/compiler/index.d.ts +6 -1
- package/dist/compiler/index.js +3 -0
- package/dist/compiler/keyframes.d.ts +6 -0
- package/dist/compiler/keyframes.js +120 -0
- package/dist/compiler/libraries.d.ts +16 -0
- package/dist/compiler/libraries.js +76 -0
- package/dist/compiler/mapper.d.ts +1 -0
- package/dist/compiler/mapper.js +165 -0
- package/dist/compiler/stylesheet.d.ts +15 -0
- package/dist/compiler/stylesheet.js +244 -0
- package/package.json +15 -3
- package/package.schema.json +117 -0
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
|
52
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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: [
|
|
219
|
-
: { declarations: [
|
|
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
|
+
}
|
package/dist/compiler/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/compiler/index.js
CHANGED
|
@@ -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[];
|