@pre.style/core 3.0.0-alpha.0 → 3.1.0-alpha.0
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 +216 -33
- package/dist/compiler/block.d.ts +28 -0
- package/dist/compiler/block.js +82 -24
- package/dist/compiler/compiler.d.ts +13 -0
- package/dist/compiler/compiler.js +190 -16
- package/dist/compiler/config.d.ts +15 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/index.d.ts +6 -1
- package/dist/compiler/index.js +3 -0
- package/dist/compiler/libraries.d.ts +14 -0
- package/dist/compiler/libraries.js +75 -0
- package/dist/compiler/mapper.d.ts +1 -0
- package/dist/compiler/mapper.js +160 -0
- package/dist/compiler/stylesheet.d.ts +15 -0
- package/dist/compiler/stylesheet.js +236 -0
- package/package.json +15 -3
- package/package.schema.json +109 -0
|
@@ -1,9 +1,10 @@
|
|
|
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
3
|
import SweatMap from 'sweatmap';
|
|
4
|
-
import { parseBlock, PreStyleError, valueOf } from './block.js';
|
|
4
|
+
import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
|
|
5
5
|
import { print } from './print.js';
|
|
6
6
|
import { lvhfaRank, withClass } from './selectors.js';
|
|
7
|
+
import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
|
|
7
8
|
const RANGES = {
|
|
8
9
|
Digits: { start: '0030', end: '0039' },
|
|
9
10
|
Uppercase: { start: '0041', end: '005A' },
|
|
@@ -45,6 +46,8 @@ export class Compiler {
|
|
|
45
46
|
#prepend;
|
|
46
47
|
#libraries;
|
|
47
48
|
#blocks = new Map();
|
|
49
|
+
#stylesheets = new Map();
|
|
50
|
+
#root;
|
|
48
51
|
// One name list for every class, and one for prefixes (the at-rules and
|
|
49
52
|
// selector variant a class is under), which only the `cn` table uses.
|
|
50
53
|
#names;
|
|
@@ -62,6 +65,7 @@ export class Compiler {
|
|
|
62
65
|
prefixes: new SweatMap(NAME_OPTIONS),
|
|
63
66
|
};
|
|
64
67
|
const root = options.root ?? process.cwd();
|
|
68
|
+
this.#root = root;
|
|
65
69
|
this.#prepend = readPrepend(root, options.prependFiles);
|
|
66
70
|
this.#libraries = (options.libraries ?? [])
|
|
67
71
|
.map((library) => {
|
|
@@ -84,13 +88,14 @@ export class Compiler {
|
|
|
84
88
|
// block before `assignNames`.
|
|
85
89
|
scan(block) {
|
|
86
90
|
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
91
|
this.#register(block, parsed);
|
|
92
|
+
this.#count(this.#resolveBlock(block, parsed, []));
|
|
93
|
+
}
|
|
94
|
+
// Pass 1 of naming for a stylesheet file: counts what each class uses.
|
|
95
|
+
scanStylesheet(sheet) {
|
|
96
|
+
const parsed = this.#parseSheet(sheet);
|
|
97
|
+
for (const name of parsed.classes.keys())
|
|
98
|
+
this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
|
|
94
99
|
}
|
|
95
100
|
// Pass 2 of naming: gives the most-used entries the shortest names. Ties
|
|
96
101
|
// are broken by key, so names don't depend on the order files are read.
|
|
@@ -106,7 +111,38 @@ export class Compiler {
|
|
|
106
111
|
classString(block) {
|
|
107
112
|
const parsed = this.#parse(block);
|
|
108
113
|
this.#register(block, parsed);
|
|
109
|
-
return
|
|
114
|
+
return this.#classString(this.#resolveBlock(block, parsed, []));
|
|
115
|
+
}
|
|
116
|
+
// The files a block's class string depends on, through `composes … from`.
|
|
117
|
+
dependencies(block) {
|
|
118
|
+
const dependencies = [];
|
|
119
|
+
this.#resolveBlock(block, this.#parse(block), dependencies);
|
|
120
|
+
return [...new Set(dependencies)];
|
|
121
|
+
}
|
|
122
|
+
// The classes a stylesheet file exports, without compiling or keeping
|
|
123
|
+
// anything, for tools that only need the names (such as the TypeScript
|
|
124
|
+
// content mapper).
|
|
125
|
+
classNames(sheet) {
|
|
126
|
+
const { prepend } = this.#library(sheet.file);
|
|
127
|
+
try {
|
|
128
|
+
const parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), { ...this.#parseOptions, prepend });
|
|
129
|
+
return [...parsed.classes.keys()];
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Each class of a stylesheet file, with its class string.
|
|
136
|
+
stylesheet(sheet) {
|
|
137
|
+
const parsed = this.#parseSheet(sheet);
|
|
138
|
+
const dependencies = [...parsed.dependencies];
|
|
139
|
+
const exports = [];
|
|
140
|
+
for (const name of parsed.classes.keys())
|
|
141
|
+
exports.push([
|
|
142
|
+
name,
|
|
143
|
+
this.#classString(this.#resolveClass(sheet.file, parsed, name, [], dependencies)),
|
|
144
|
+
]);
|
|
145
|
+
return { exports, dependencies: [...new Set(dependencies)] };
|
|
110
146
|
}
|
|
111
147
|
css() {
|
|
112
148
|
const sources = [];
|
|
@@ -124,7 +160,7 @@ export class Compiler {
|
|
|
124
160
|
.filter((g) => g.global.hoist !== false)
|
|
125
161
|
.toSorted((a, b) => a.global.hoist - b.global.hoist);
|
|
126
162
|
const rest = globals.filter((g) => g.global.hoist === false);
|
|
127
|
-
const rules = [...hoisted, ...rest].map((g) => withLoc(g.global.rule, loc(g.source)));
|
|
163
|
+
const rules = [...hoisted, ...rest].map((g) => withLoc(g.global.key.includes(MARKER_KEY) ? this.#named(g.global.rule) : g.global.rule, loc(g.source)));
|
|
128
164
|
rules.push(...this.#atomRules(loc));
|
|
129
165
|
return print(rules, sources, this.#parseOptions.lightningcss, this.#options.sourceMap ?? false);
|
|
130
166
|
}
|
|
@@ -141,12 +177,14 @@ export class Compiler {
|
|
|
141
177
|
}
|
|
142
178
|
return table;
|
|
143
179
|
}
|
|
180
|
+
// A library's blocks and files get its prepended files instead of the
|
|
181
|
+
// app's.
|
|
182
|
+
#library(file) {
|
|
183
|
+
const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
|
|
184
|
+
return { library, prepend: library === -1 ? this.#prepend : this.#libraries[library].prepend };
|
|
185
|
+
}
|
|
144
186
|
#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;
|
|
187
|
+
const { library, prepend } = this.#library(block.file);
|
|
150
188
|
const cacheKey = `${library}\n${block.lang}\n${block.code}`;
|
|
151
189
|
let parsed = this.#blocks.get(cacheKey);
|
|
152
190
|
if (parsed === undefined) {
|
|
@@ -160,6 +198,136 @@ export class Compiler {
|
|
|
160
198
|
}
|
|
161
199
|
return parsed;
|
|
162
200
|
}
|
|
201
|
+
// Parses a stylesheet file and registers its classes and global rules, so
|
|
202
|
+
// the output includes them once the file is used.
|
|
203
|
+
#parseSheet(sheet) {
|
|
204
|
+
const { library, prepend } = this.#library(sheet.file);
|
|
205
|
+
const cacheKey = `${library}\n${sheet.lang}\n${sheet.file}\n${sheet.code}`;
|
|
206
|
+
let parsed = this.#stylesheets.get(cacheKey);
|
|
207
|
+
if (parsed === undefined) {
|
|
208
|
+
try {
|
|
209
|
+
parsed = parseStylesheet(sheet.code, sheet.lang, sheet.file, relative(this.#root, sheet.file), {
|
|
210
|
+
...this.#parseOptions,
|
|
211
|
+
prepend,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
|
|
216
|
+
}
|
|
217
|
+
this.#stylesheets.set(cacheKey, parsed);
|
|
218
|
+
}
|
|
219
|
+
const block = fileBlock(sheet.file);
|
|
220
|
+
for (const target of parsed.classes.values())
|
|
221
|
+
this.#register(block, { atoms: target.atoms, globals: [], composes: [] });
|
|
222
|
+
this.#register(block, { atoms: [], globals: parsed.globals, composes: [] });
|
|
223
|
+
return parsed;
|
|
224
|
+
}
|
|
225
|
+
// Reads a file named by `composes … from`.
|
|
226
|
+
#readSheet(file) {
|
|
227
|
+
let code;
|
|
228
|
+
try {
|
|
229
|
+
code = readFileSync(file, 'utf8');
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw new PreStyleError(`Can't read ${file}, named by \`composes\`.`, null, { cause: error });
|
|
233
|
+
}
|
|
234
|
+
return this.#parseSheet({ code, lang: file.endsWith('.scss') ? 'scss' : 'css', file });
|
|
235
|
+
}
|
|
236
|
+
// A class's atoms and other names, after its `composes`. Composed atoms
|
|
237
|
+
// come first, so the class's own declarations replace the ones they
|
|
238
|
+
// conflict with, as in `cn`. `stack` holds the classes being resolved, to
|
|
239
|
+
// find cycles. Files composed from are added to `dependencies`.
|
|
240
|
+
#resolveClass(file, parsed, name, stack, dependencies) {
|
|
241
|
+
const target = parsed.classes.get(name);
|
|
242
|
+
if (target === undefined)
|
|
243
|
+
throw new PreStyleError(`${file} has no class \`${name}\` to compose.`, null);
|
|
244
|
+
const id = `${file}\0${name}`;
|
|
245
|
+
if (stack.includes(id))
|
|
246
|
+
throw new PreStyleError(`\`composes\` cycle: ${[...stack, id].map((entry) => entry.replace('\0', ' .')).join(' -> ')}.`, null);
|
|
247
|
+
const composed = this.#resolveComposes(file, parsed, target.composes, [...stack, id], dependencies);
|
|
248
|
+
if (target.marker)
|
|
249
|
+
composed.extras.push(markerKey(relative(this.#root, file), name));
|
|
250
|
+
return {
|
|
251
|
+
atoms: composed.atoms.length === 0
|
|
252
|
+
? target.atoms
|
|
253
|
+
: dropOverridden([...composed.atoms, ...target.atoms]),
|
|
254
|
+
extras: composed.extras,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
#resolveBlock(block, parsed, dependencies) {
|
|
258
|
+
if (parsed.composes.length === 0)
|
|
259
|
+
return { atoms: parsed.atoms, extras: [] };
|
|
260
|
+
for (const c of parsed.composes) {
|
|
261
|
+
if (c.from.type === 'local')
|
|
262
|
+
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);
|
|
263
|
+
}
|
|
264
|
+
const file = block.file ?? resolvePath(this.#root, 'block.css');
|
|
265
|
+
let composed;
|
|
266
|
+
try {
|
|
267
|
+
composed = this.#resolveComposes(file, null, parsed.composes, [], dependencies);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
throw error instanceof PreStyleError ? locate(error, block) : error;
|
|
271
|
+
}
|
|
272
|
+
return { atoms: dropOverridden([...composed.atoms, ...parsed.atoms]), extras: composed.extras };
|
|
273
|
+
}
|
|
274
|
+
#resolveComposes(file, parsed, composes, stack, dependencies) {
|
|
275
|
+
const atoms = [];
|
|
276
|
+
const extras = [];
|
|
277
|
+
for (const c of composes) {
|
|
278
|
+
if (c.from.type === 'global') {
|
|
279
|
+
extras.push(...c.names);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let from = file;
|
|
283
|
+
let sheet = parsed;
|
|
284
|
+
if (c.from.type === 'file') {
|
|
285
|
+
from = resolvePath(dirname(file), c.from.path);
|
|
286
|
+
dependencies.push(from);
|
|
287
|
+
sheet = this.#readSheet(from);
|
|
288
|
+
dependencies.push(...sheet.dependencies);
|
|
289
|
+
}
|
|
290
|
+
for (const name of c.names) {
|
|
291
|
+
const resolved = this.#resolveClass(from, sheet, name, stack, dependencies);
|
|
292
|
+
atoms.push(...resolved.atoms);
|
|
293
|
+
extras.push(...resolved.extras);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { atoms, extras };
|
|
297
|
+
}
|
|
298
|
+
#count(resolved) {
|
|
299
|
+
for (const atom of resolved.atoms) {
|
|
300
|
+
count(this.#counts.atoms, atomKey(atom));
|
|
301
|
+
const prefix = prefixKey(atom);
|
|
302
|
+
if (prefix !== '')
|
|
303
|
+
count(this.#counts.prefixes, prefix);
|
|
304
|
+
}
|
|
305
|
+
for (const extra of resolved.extras)
|
|
306
|
+
if (extra.startsWith(MARKER_PREFIX))
|
|
307
|
+
count(this.#counts.atoms, extra);
|
|
308
|
+
}
|
|
309
|
+
#classString(resolved) {
|
|
310
|
+
const names = resolved.atoms.map((atom) => this.#nameOf(this.#atoms.get(atomKey(atom))));
|
|
311
|
+
for (const extra of resolved.extras)
|
|
312
|
+
names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
|
|
313
|
+
return [...new Set(names)].join(' ');
|
|
314
|
+
}
|
|
315
|
+
// Replaces markers in selectors with their names.
|
|
316
|
+
#named(value) {
|
|
317
|
+
const visit = (node) => {
|
|
318
|
+
if (Array.isArray(node))
|
|
319
|
+
return node.map(visit);
|
|
320
|
+
if (node === null || typeof node !== 'object')
|
|
321
|
+
return node;
|
|
322
|
+
const record = node;
|
|
323
|
+
if (record.type === 'class' &&
|
|
324
|
+
typeof record.name === 'string' &&
|
|
325
|
+
record.name.startsWith(MARKER_PREFIX))
|
|
326
|
+
return { ...record, name: this.#names.atoms.set(record.name) };
|
|
327
|
+
return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, visit(v)]));
|
|
328
|
+
};
|
|
329
|
+
return visit(value);
|
|
330
|
+
}
|
|
163
331
|
#register(block, parsed) {
|
|
164
332
|
for (const atom of parsed.atoms) {
|
|
165
333
|
const key = atomKey(atom);
|
|
@@ -211,9 +379,11 @@ export class Compiler {
|
|
|
211
379
|
const rules = sortAtoms(group.atoms).map((record) => {
|
|
212
380
|
const { atom, source } = record;
|
|
213
381
|
const className = this.#nameOf(record);
|
|
214
|
-
|
|
382
|
+
let selectors = atom.variant
|
|
215
383
|
? withClass(atom.variant.selectors, className)
|
|
216
384
|
: [[{ type: 'class', name: className }]];
|
|
385
|
+
if (atom.variant?.key.includes(MARKER_KEY))
|
|
386
|
+
selectors = this.#named(selectors);
|
|
217
387
|
const declarations = atom.declaration.important
|
|
218
388
|
? { importantDeclarations: [atom.declaration.value] }
|
|
219
389
|
: { declarations: [atom.declaration.value] };
|
|
@@ -233,6 +403,10 @@ export class Compiler {
|
|
|
233
403
|
return emit(root);
|
|
234
404
|
}
|
|
235
405
|
}
|
|
406
|
+
// Markers as they appear in JSON keys, which escape the `\0`.
|
|
407
|
+
const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
|
|
408
|
+
// A stylesheet file, for positions and source maps.
|
|
409
|
+
const fileBlock = (file) => ({ code: '', lang: 'css', file, line: 1, column: 1 });
|
|
236
410
|
// Reads prepended files, relative to `dir`, once.
|
|
237
411
|
function readPrepend(dir, files) {
|
|
238
412
|
const read = (list = []) => list.map((file) => readFileSync(resolvePath(dir, file), 'utf8')).join('\n');
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
10
|
+
export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
|
|
11
|
+
export declare function readConfig(root: string): {
|
|
12
|
+
dir: string;
|
|
13
|
+
config: PackageConfig;
|
|
14
|
+
warnings: string[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
const KEYS = new Set(['include', 'exclude', 'prependFiles', 'libraries', 'quirksMode']);
|
|
4
|
+
const isStrings = (value) => typeof value === 'string' ||
|
|
5
|
+
(Array.isArray(value) && value.every((item) => typeof item === 'string'));
|
|
6
|
+
const isPrependFiles = (value) => (Array.isArray(value) && isStrings(value)) ||
|
|
7
|
+
(value !== null &&
|
|
8
|
+
typeof value === 'object' &&
|
|
9
|
+
!Array.isArray(value) &&
|
|
10
|
+
Object.entries(value).every(([lang, files]) => (lang === 'css' || lang === 'scss') && Array.isArray(files) && isStrings(files)));
|
|
11
|
+
// Checks a `pre.style` field's shared keys, and returns warnings for keys
|
|
12
|
+
// it doesn't know. `where` names the field in errors.
|
|
13
|
+
export function checkField(field, where, keys) {
|
|
14
|
+
const fail = (key, expected) => {
|
|
15
|
+
throw new Error(`[pre.style] ${where}: \`${key}\` must be ${expected}.`);
|
|
16
|
+
};
|
|
17
|
+
if (field.include !== undefined && !isStrings(field.include))
|
|
18
|
+
fail('include', 'a glob or an array of globs');
|
|
19
|
+
if (field.exclude !== undefined && !isStrings(field.exclude))
|
|
20
|
+
fail('exclude', 'a glob or an array of globs');
|
|
21
|
+
if (field.prependFiles !== undefined && !isPrependFiles(field.prependFiles))
|
|
22
|
+
fail('prependFiles', 'an array of paths, or an object with `css` and `scss` arrays');
|
|
23
|
+
if (field.quirksMode !== undefined && typeof field.quirksMode !== 'boolean')
|
|
24
|
+
fail('quirksMode', '`true` or `false`');
|
|
25
|
+
if (field.libraries !== undefined &&
|
|
26
|
+
(field.libraries === null ||
|
|
27
|
+
typeof field.libraries !== 'object' ||
|
|
28
|
+
Array.isArray(field.libraries)))
|
|
29
|
+
fail('libraries', 'an object of package names');
|
|
30
|
+
return Object.keys(field)
|
|
31
|
+
.filter((key) => !keys.has(key))
|
|
32
|
+
.map((key) => `${where}: unknown key \`${key}\`.`);
|
|
33
|
+
}
|
|
34
|
+
// Reads the `pre.style` field from the package.json nearest `root`. `dir`
|
|
35
|
+
// is the package's directory, which the field's paths are relative to.
|
|
36
|
+
export function readConfig(root) {
|
|
37
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
38
|
+
const file = join(dir, 'package.json');
|
|
39
|
+
if (existsSync(file)) {
|
|
40
|
+
const field = JSON.parse(readFileSync(file, 'utf8'))['pre.style'];
|
|
41
|
+
if (field === undefined)
|
|
42
|
+
return { dir, config: {}, warnings: [] };
|
|
43
|
+
const where = `"pre.style" in ${file}`;
|
|
44
|
+
if (field === null || typeof field !== 'object' || Array.isArray(field))
|
|
45
|
+
throw new Error(`[pre.style] ${where} must be an object.`);
|
|
46
|
+
const warnings = checkField(field, where, KEYS);
|
|
47
|
+
return { dir, config: field, warnings };
|
|
48
|
+
}
|
|
49
|
+
if (dirname(dir) === dir)
|
|
50
|
+
return { dir: root, config: {}, warnings: [] };
|
|
51
|
+
}
|
|
52
|
+
}
|
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,14 @@
|
|
|
1
|
+
import type { PrependFiles } from './compiler.ts';
|
|
2
|
+
export interface LibraryConfig {
|
|
3
|
+
include?: string | string[];
|
|
4
|
+
exclude?: string | string[];
|
|
5
|
+
prependFiles?: PrependFiles;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedLibrary {
|
|
8
|
+
name: string;
|
|
9
|
+
dir: string;
|
|
10
|
+
include: string[];
|
|
11
|
+
exclude: string[];
|
|
12
|
+
prependFiles?: PrependFiles;
|
|
13
|
+
}
|
|
14
|
+
export declare function findLibraries(root: string, overrides?: Record<string, LibraryConfig | boolean>, warn?: (message: string) => void): ResolvedLibrary[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { checkField } from './config.js';
|
|
4
|
+
// Keys a library's field shares with the app's. `libraries` and
|
|
5
|
+
// `quirksMode` only apply to the app.
|
|
6
|
+
const SHARED_KEYS = new Set(['include', 'exclude', 'prependFiles']);
|
|
7
|
+
const APP_KEYS = new Set(['libraries', 'quirksMode']);
|
|
8
|
+
const DEFAULT_INCLUDE = ['**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts,astro}'];
|
|
9
|
+
const readPackage = (dir) => JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
10
|
+
// Finds an installed package the way Node does, by looking in each
|
|
11
|
+
// `node_modules` up from `from`.
|
|
12
|
+
function findPackage(name, from) {
|
|
13
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
14
|
+
const candidate = join(dir, 'node_modules', name);
|
|
15
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
16
|
+
return realpathSync(candidate);
|
|
17
|
+
if (dirname(dir) === dir)
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Finds the pre.style component libraries an app uses: its dependencies
|
|
22
|
+
// with a `pre.style` field, and theirs, recursively. `overrides` adds
|
|
23
|
+
// packages, changes their config, or (with `false`) leaves them out.
|
|
24
|
+
// `warn` receives problems that don't stop the build.
|
|
25
|
+
export function findLibraries(root, overrides = {}, warn = () => { }) {
|
|
26
|
+
const libraries = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const visit = (name, from) => {
|
|
29
|
+
const dir = findPackage(name, from);
|
|
30
|
+
if (dir === null || seen.has(dir))
|
|
31
|
+
return;
|
|
32
|
+
seen.add(dir);
|
|
33
|
+
const pkg = readPackage(dir);
|
|
34
|
+
const config = Object.hasOwn(overrides, name) ? overrides[name] : pkg['pre.style'];
|
|
35
|
+
if (!config)
|
|
36
|
+
return;
|
|
37
|
+
if (config !== true) {
|
|
38
|
+
const where = Object.hasOwn(overrides, name)
|
|
39
|
+
? `"libraries" entry for ${name}`
|
|
40
|
+
: `"pre.style" in ${join(dir, 'package.json')}`;
|
|
41
|
+
for (const key of Object.keys(config))
|
|
42
|
+
if (APP_KEYS.has(key))
|
|
43
|
+
warn(`${where}: \`${key}\` only applies to the app, so it's ignored for a library.`);
|
|
44
|
+
for (const warning of checkField(config, where, new Set([...SHARED_KEYS, ...APP_KEYS])))
|
|
45
|
+
warn(warning);
|
|
46
|
+
}
|
|
47
|
+
const { include, exclude, prependFiles } = config === true ? {} : config;
|
|
48
|
+
libraries.push({
|
|
49
|
+
name,
|
|
50
|
+
dir,
|
|
51
|
+
include: include === undefined ? DEFAULT_INCLUDE : [include].flat(),
|
|
52
|
+
exclude: exclude === undefined ? [] : [exclude].flat(),
|
|
53
|
+
prependFiles,
|
|
54
|
+
});
|
|
55
|
+
// A library's peer dependencies are installed by the app, and found the
|
|
56
|
+
// same way from the library.
|
|
57
|
+
for (const dependency of Object.keys({
|
|
58
|
+
...pkg.dependencies,
|
|
59
|
+
...pkg.peerDependencies,
|
|
60
|
+
...pkg.optionalDependencies,
|
|
61
|
+
})) {
|
|
62
|
+
visit(dependency, dir);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const app = existsSync(join(root, 'package.json')) ? readPackage(root) : {};
|
|
66
|
+
const names = Object.keys({
|
|
67
|
+
...app.dependencies,
|
|
68
|
+
...app.devDependencies,
|
|
69
|
+
...app.optionalDependencies,
|
|
70
|
+
...overrides,
|
|
71
|
+
});
|
|
72
|
+
for (const name of names)
|
|
73
|
+
visit(name, root);
|
|
74
|
+
return libraries;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// The TypeScript content mapper (TypeScript 7.1 and later). `tsc` and the
|
|
2
|
+
// editor run it as a separate process, through the `typescript.contentMapper`
|
|
3
|
+
// field in package.json, and send it each `.css` and `.scss` file in the
|
|
4
|
+
// program over JSON-RPC on stdio. It answers with a TypeScript view of the
|
|
5
|
+
// file that declares one `string` export per class, so named imports from
|
|
6
|
+
// stylesheet files are typed with their exact keys.
|
|
7
|
+
//
|
|
8
|
+
// Register it in tsconfig.json, and run `tsc --runExternalCode`:
|
|
9
|
+
//
|
|
10
|
+
// "contentMappers": [{ "package": "@pre.style/core", "extensions": [".css", ".scss"] }]
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
import { PreStyleError } from './block.js';
|
|
15
|
+
import { createCompiler } from './compiler.js';
|
|
16
|
+
import { readConfig } from './config.js';
|
|
17
|
+
import { findLibraries } from './libraries.js';
|
|
18
|
+
import { isIdentifier } from './stylesheet.js';
|
|
19
|
+
// Each open project's compiler, by its handle.
|
|
20
|
+
const projects = new Map();
|
|
21
|
+
// Diagnostic codes, for errors in a stylesheet file.
|
|
22
|
+
const INVALID_STYLESHEET = 9001;
|
|
23
|
+
const prependPaths = (dir, files) => (Array.isArray(files) ? files : [...(files?.css ?? []), ...(files?.scss ?? [])]).map((file) => resolve(dir, file));
|
|
24
|
+
async function openProject(params) {
|
|
25
|
+
// The project's package.json, nearest its tsconfig.json.
|
|
26
|
+
const root = params.configFileName ? dirname(params.configFileName) : process.cwd();
|
|
27
|
+
const app = readConfig(root);
|
|
28
|
+
const libraries = findLibraries(app.dir, app.config.libraries);
|
|
29
|
+
const compiler = createCompiler({
|
|
30
|
+
root: app.dir,
|
|
31
|
+
prependFiles: app.config.prependFiles,
|
|
32
|
+
libraries: libraries.map(({ dir, prependFiles }) => ({ dir, prependFiles })),
|
|
33
|
+
quirksMode: app.config.quirksMode,
|
|
34
|
+
warn: () => { },
|
|
35
|
+
});
|
|
36
|
+
projects.set(params.projectHandle, compiler);
|
|
37
|
+
await compiler;
|
|
38
|
+
// Changes to these files change which classes files export.
|
|
39
|
+
const watchedFiles = [
|
|
40
|
+
join(app.dir, 'package.json'),
|
|
41
|
+
...prependPaths(app.dir, app.config.prependFiles),
|
|
42
|
+
...libraries.flatMap(({ dir, prependFiles }) => [
|
|
43
|
+
join(dir, 'package.json'),
|
|
44
|
+
...prependPaths(dir, prependFiles),
|
|
45
|
+
]),
|
|
46
|
+
].filter((file) => existsSync(file));
|
|
47
|
+
const hash = createHash('sha256');
|
|
48
|
+
for (const file of watchedFiles)
|
|
49
|
+
hash.update(`${file}\0${readFileSync(file, 'utf8')}\0`);
|
|
50
|
+
return { configIdentity: hash.digest('hex'), watchedFiles };
|
|
51
|
+
}
|
|
52
|
+
// The offset of a line and column (line from 0, column from 1) in `text`.
|
|
53
|
+
function offsetOf(text, line, column) {
|
|
54
|
+
let offset = 0;
|
|
55
|
+
for (let i = 0; i < line; i++) {
|
|
56
|
+
const next = text.indexOf('\n', offset);
|
|
57
|
+
if (next === -1)
|
|
58
|
+
break;
|
|
59
|
+
offset = next + 1;
|
|
60
|
+
}
|
|
61
|
+
return Math.min(text.length, offset + column - 1);
|
|
62
|
+
}
|
|
63
|
+
async function transform(params) {
|
|
64
|
+
const compiler = await projects.get(params.projectHandle);
|
|
65
|
+
if (compiler === undefined)
|
|
66
|
+
throw new Error(`Unknown project ${params.projectHandle}.`);
|
|
67
|
+
const { fileName, content } = params;
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = compiler.classNames({
|
|
71
|
+
code: content,
|
|
72
|
+
lang: fileName.endsWith('.scss') ? 'scss' : 'css',
|
|
73
|
+
file: fileName,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!(error instanceof PreStyleError))
|
|
78
|
+
throw error;
|
|
79
|
+
const position = error.position;
|
|
80
|
+
return {
|
|
81
|
+
text: 'export {};\n',
|
|
82
|
+
extension: '.ts',
|
|
83
|
+
diagnostics: [
|
|
84
|
+
{
|
|
85
|
+
// The message without the file:line:col prefix, which the
|
|
86
|
+
// diagnostic shows itself.
|
|
87
|
+
messageText: error.message.replace(/^.*?:\d+:\d+: /, ''),
|
|
88
|
+
start: position ? offsetOf(content, position.line, position.column) : 0,
|
|
89
|
+
length: 0,
|
|
90
|
+
code: INVALID_STYLESHEET,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// One `string` export per class. Each name maps to where the class is
|
|
96
|
+
// first written in the file, so go-to-definition opens the stylesheet.
|
|
97
|
+
let text = '';
|
|
98
|
+
const mappings = [];
|
|
99
|
+
let local = 0;
|
|
100
|
+
for (const name of names) {
|
|
101
|
+
const written = new RegExp(`\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`).exec(content);
|
|
102
|
+
let start;
|
|
103
|
+
if (isIdentifier(name)) {
|
|
104
|
+
text += 'export declare const ';
|
|
105
|
+
start = text.length;
|
|
106
|
+
text += `${name}: string;\n`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const binding = `__pre_style_${local++}`;
|
|
110
|
+
text += `declare const ${binding}: string;\nexport { ${binding} as `;
|
|
111
|
+
start = text.length + 1;
|
|
112
|
+
text += `${JSON.stringify(name)} };\n`;
|
|
113
|
+
}
|
|
114
|
+
// Verbatim mapping: the same text in both, in UTF-16 code units.
|
|
115
|
+
if (written !== null)
|
|
116
|
+
mappings.push([start, name.length, written.index + 1, name.length, 0]);
|
|
117
|
+
}
|
|
118
|
+
return { text: text || 'export {};\n', extension: '.ts', mappings };
|
|
119
|
+
}
|
|
120
|
+
async function handle(request) {
|
|
121
|
+
switch (request.method) {
|
|
122
|
+
case 'initialize':
|
|
123
|
+
return { positionEncoding: 'utf-16', diagnosticSource: 'pre.style' };
|
|
124
|
+
case 'openProject':
|
|
125
|
+
return openProject(request.params);
|
|
126
|
+
case 'transform':
|
|
127
|
+
return transform(request.params);
|
|
128
|
+
case 'closeProject':
|
|
129
|
+
projects.delete(request.params.projectHandle);
|
|
130
|
+
return null;
|
|
131
|
+
default:
|
|
132
|
+
throw Object.assign(new Error(`Unknown method ${request.method}.`), { code: -32601 });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function send(message) {
|
|
136
|
+
const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...message }));
|
|
137
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
138
|
+
process.stdout.write(body);
|
|
139
|
+
}
|
|
140
|
+
// Messages are framed like the Language Server Protocol's: a
|
|
141
|
+
// `Content-Length` header, a blank line, then the JSON body.
|
|
142
|
+
let buffer = Buffer.alloc(0);
|
|
143
|
+
process.stdin.on('data', (chunk) => {
|
|
144
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
145
|
+
for (;;) {
|
|
146
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
147
|
+
if (headerEnd === -1)
|
|
148
|
+
return;
|
|
149
|
+
const length = Number(/Content-Length: *(\d+)/i.exec(buffer.subarray(0, headerEnd).toString())?.[1]);
|
|
150
|
+
const start = headerEnd + 4;
|
|
151
|
+
if (buffer.length < start + length)
|
|
152
|
+
return;
|
|
153
|
+
const request = JSON.parse(buffer.subarray(start, start + length).toString());
|
|
154
|
+
buffer = buffer.subarray(start + length);
|
|
155
|
+
// Notifications have no ID, and get no response.
|
|
156
|
+
if (request.id === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
handle(request).then((result) => send({ id: request.id, result }), (error) => send({ id: request.id, error: { code: error.code ?? -32603, message: error.message } }));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Atom, Compose, GlobalRule, Lang, ParseOptions } from './block.ts';
|
|
2
|
+
export declare const MARKER_PREFIX = "\0pre.style-marker:";
|
|
3
|
+
export declare const markerKey: (id: string, name: string) => string;
|
|
4
|
+
export declare const isIdentifier: (name: string) => boolean;
|
|
5
|
+
export interface StylesheetClass {
|
|
6
|
+
atoms: Atom[];
|
|
7
|
+
composes: Compose[];
|
|
8
|
+
marker: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ParsedStylesheet {
|
|
11
|
+
classes: Map<string, StylesheetClass>;
|
|
12
|
+
globals: GlobalRule[];
|
|
13
|
+
dependencies: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function parseStylesheet(code: string, lang: Lang, file: string, id: string, options: ParseOptions): ParsedStylesheet;
|