@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
|
@@ -0,0 +1,76 @@
|
|
|
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', 'keyframeLocalization']);
|
|
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, keyframeLocalization } = 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
|
+
keyframeLocalization,
|
|
55
|
+
});
|
|
56
|
+
// A library's peer dependencies are installed by the app, and found the
|
|
57
|
+
// same way from the library.
|
|
58
|
+
for (const dependency of Object.keys({
|
|
59
|
+
...pkg.dependencies,
|
|
60
|
+
...pkg.peerDependencies,
|
|
61
|
+
...pkg.optionalDependencies,
|
|
62
|
+
})) {
|
|
63
|
+
visit(dependency, dir);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const app = existsSync(join(root, 'package.json')) ? readPackage(root) : {};
|
|
67
|
+
const names = Object.keys({
|
|
68
|
+
...app.dependencies,
|
|
69
|
+
...app.devDependencies,
|
|
70
|
+
...app.optionalDependencies,
|
|
71
|
+
...overrides,
|
|
72
|
+
});
|
|
73
|
+
for (const name of names)
|
|
74
|
+
visit(name, root);
|
|
75
|
+
return libraries;
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
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, keyframeLocalization }) => ({
|
|
33
|
+
dir,
|
|
34
|
+
prependFiles,
|
|
35
|
+
keyframeLocalization,
|
|
36
|
+
})),
|
|
37
|
+
quirksMode: app.config.quirksMode,
|
|
38
|
+
keyframeLocalization: app.config.keyframeLocalization,
|
|
39
|
+
warn: () => { },
|
|
40
|
+
});
|
|
41
|
+
projects.set(params.projectHandle, compiler);
|
|
42
|
+
await compiler;
|
|
43
|
+
// Changes to these files change which classes files export.
|
|
44
|
+
const watchedFiles = [
|
|
45
|
+
join(app.dir, 'package.json'),
|
|
46
|
+
...prependPaths(app.dir, app.config.prependFiles),
|
|
47
|
+
...libraries.flatMap(({ dir, prependFiles }) => [
|
|
48
|
+
join(dir, 'package.json'),
|
|
49
|
+
...prependPaths(dir, prependFiles),
|
|
50
|
+
]),
|
|
51
|
+
].filter((file) => existsSync(file));
|
|
52
|
+
const hash = createHash('sha256');
|
|
53
|
+
for (const file of watchedFiles)
|
|
54
|
+
hash.update(`${file}\0${readFileSync(file, 'utf8')}\0`);
|
|
55
|
+
return { configIdentity: hash.digest('hex'), watchedFiles };
|
|
56
|
+
}
|
|
57
|
+
// The offset of a line and column (line from 0, column from 1) in `text`.
|
|
58
|
+
function offsetOf(text, line, column) {
|
|
59
|
+
let offset = 0;
|
|
60
|
+
for (let i = 0; i < line; i++) {
|
|
61
|
+
const next = text.indexOf('\n', offset);
|
|
62
|
+
if (next === -1)
|
|
63
|
+
break;
|
|
64
|
+
offset = next + 1;
|
|
65
|
+
}
|
|
66
|
+
return Math.min(text.length, offset + column - 1);
|
|
67
|
+
}
|
|
68
|
+
async function transform(params) {
|
|
69
|
+
const compiler = await projects.get(params.projectHandle);
|
|
70
|
+
if (compiler === undefined)
|
|
71
|
+
throw new Error(`Unknown project ${params.projectHandle}.`);
|
|
72
|
+
const { fileName, content } = params;
|
|
73
|
+
let names;
|
|
74
|
+
try {
|
|
75
|
+
names = compiler.classNames({
|
|
76
|
+
code: content,
|
|
77
|
+
lang: fileName.endsWith('.scss') ? 'scss' : 'css',
|
|
78
|
+
file: fileName,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!(error instanceof PreStyleError))
|
|
83
|
+
throw error;
|
|
84
|
+
const position = error.position;
|
|
85
|
+
return {
|
|
86
|
+
text: 'export {};\n',
|
|
87
|
+
extension: '.ts',
|
|
88
|
+
diagnostics: [
|
|
89
|
+
{
|
|
90
|
+
// The message without the file:line:col prefix, which the
|
|
91
|
+
// diagnostic shows itself.
|
|
92
|
+
messageText: error.message.replace(/^.*?:\d+:\d+: /, ''),
|
|
93
|
+
start: position ? offsetOf(content, position.line, position.column) : 0,
|
|
94
|
+
length: 0,
|
|
95
|
+
code: INVALID_STYLESHEET,
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// One `string` export per class. Each name maps to where the class is
|
|
101
|
+
// first written in the file, so go-to-definition opens the stylesheet.
|
|
102
|
+
let text = '';
|
|
103
|
+
const mappings = [];
|
|
104
|
+
let local = 0;
|
|
105
|
+
for (const name of names) {
|
|
106
|
+
const written = new RegExp(`\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`).exec(content);
|
|
107
|
+
let start;
|
|
108
|
+
if (isIdentifier(name)) {
|
|
109
|
+
text += 'export declare const ';
|
|
110
|
+
start = text.length;
|
|
111
|
+
text += `${name}: string;\n`;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const binding = `__pre_style_${local++}`;
|
|
115
|
+
text += `declare const ${binding}: string;\nexport { ${binding} as `;
|
|
116
|
+
start = text.length + 1;
|
|
117
|
+
text += `${JSON.stringify(name)} };\n`;
|
|
118
|
+
}
|
|
119
|
+
// Verbatim mapping: the same text in both, in UTF-16 code units.
|
|
120
|
+
if (written !== null)
|
|
121
|
+
mappings.push([start, name.length, written.index + 1, name.length, 0]);
|
|
122
|
+
}
|
|
123
|
+
return { text: text || 'export {};\n', extension: '.ts', mappings };
|
|
124
|
+
}
|
|
125
|
+
async function handle(request) {
|
|
126
|
+
switch (request.method) {
|
|
127
|
+
case 'initialize':
|
|
128
|
+
return { positionEncoding: 'utf-16', diagnosticSource: 'pre.style' };
|
|
129
|
+
case 'openProject':
|
|
130
|
+
return openProject(request.params);
|
|
131
|
+
case 'transform':
|
|
132
|
+
return transform(request.params);
|
|
133
|
+
case 'closeProject':
|
|
134
|
+
projects.delete(request.params.projectHandle);
|
|
135
|
+
return null;
|
|
136
|
+
default:
|
|
137
|
+
throw Object.assign(new Error(`Unknown method ${request.method}.`), { code: -32601 });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function send(message) {
|
|
141
|
+
const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...message }));
|
|
142
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
143
|
+
process.stdout.write(body);
|
|
144
|
+
}
|
|
145
|
+
// Messages are framed like the Language Server Protocol's: a
|
|
146
|
+
// `Content-Length` header, a blank line, then the JSON body.
|
|
147
|
+
let buffer = Buffer.alloc(0);
|
|
148
|
+
process.stdin.on('data', (chunk) => {
|
|
149
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
150
|
+
for (;;) {
|
|
151
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
152
|
+
if (headerEnd === -1)
|
|
153
|
+
return;
|
|
154
|
+
const length = Number(/Content-Length: *(\d+)/i.exec(buffer.subarray(0, headerEnd).toString())?.[1]);
|
|
155
|
+
const start = headerEnd + 4;
|
|
156
|
+
if (buffer.length < start + length)
|
|
157
|
+
return;
|
|
158
|
+
const request = JSON.parse(buffer.subarray(start, start + length).toString());
|
|
159
|
+
buffer = buffer.subarray(start + length);
|
|
160
|
+
// Notifications have no ID, and get no response.
|
|
161
|
+
if (request.id === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
handle(request).then((result) => send({ id: request.id, result }), (error) => send({ id: request.id, error: { code: error.code ?? -32603, message: error.message } }));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
@@ -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;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
2
|
+
import { atRuleInfo } from './at-rules.js';
|
|
3
|
+
import { addDeclarations, addGlobal, dropOverridden, key, parseRules, PreStyleError, valueOf, walk, } from './block.js';
|
|
4
|
+
import { localizeKeyframes } from './keyframes.js';
|
|
5
|
+
import { compose, isElement } from './selectors.js';
|
|
6
|
+
// Classes used as context for another class (`.card:hover .title`), or in a
|
|
7
|
+
// compound with it (`.a.b`), are replaced with a marker: one generated name
|
|
8
|
+
// added to that class's string. In parsed selectors, a marker is a class
|
|
9
|
+
// named with this prefix, the stylesheet's ID, and the class, so markers
|
|
10
|
+
// from different files never share a key. The compiler gives it a name when
|
|
11
|
+
// printing.
|
|
12
|
+
export const MARKER_PREFIX = '\0pre.style-marker:';
|
|
13
|
+
export const markerKey = (id, name) => `${MARKER_PREFIX}${id}\0${name}`;
|
|
14
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
15
|
+
const RESERVED = new Set('arguments await break case catch class const continue debugger default delete do else enum eval export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield'.split(' '));
|
|
16
|
+
// Whether a class can be exported as a `const` of the same name. Others
|
|
17
|
+
// (such as `primary-button`) are exported by string name.
|
|
18
|
+
export const isIdentifier = (name) => IDENTIFIER.test(name) && !RESERVED.has(name);
|
|
19
|
+
// Separates the CSS Sass writes for prepended code (and modules loaded with
|
|
20
|
+
// `@use`, which Sass writes first) from the file's own.
|
|
21
|
+
const SPLIT = '/*! __pre-style-split__ */';
|
|
22
|
+
// Leading `@use` and `@forward` rules, which Sass requires before any other
|
|
23
|
+
// rule, so they're moved above the prepended code.
|
|
24
|
+
const LEADING_MODULE_RULE = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/|@(?:use|forward)\b[^;]*;)/;
|
|
25
|
+
// A style rule, or (with `global`) another rule that isn't an atomic
|
|
26
|
+
// at-rule.
|
|
27
|
+
const none = () => null;
|
|
28
|
+
// Parses a stylesheet file. Each class becomes a block: its rules, with the
|
|
29
|
+
// class replaced by `&`, wherever they appear in the file. Rules that don't
|
|
30
|
+
// target a class, and the prepended code's rules, are global. `id` names the
|
|
31
|
+
// file in marker keys.
|
|
32
|
+
export function parseStylesheet(code, lang, file, id, options) {
|
|
33
|
+
if (lang === 'scss' && options.sass === null) {
|
|
34
|
+
throw new PreStyleError('Found an scss file, but Sass is not installed. Install the `sass` package to use scss files.', null);
|
|
35
|
+
}
|
|
36
|
+
let head = options.prepend[lang];
|
|
37
|
+
let body = code;
|
|
38
|
+
let dependencies = [];
|
|
39
|
+
if (lang === 'scss') {
|
|
40
|
+
const modules = [];
|
|
41
|
+
let rest = code;
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (let match; (match = LEADING_MODULE_RULE.exec(rest));) {
|
|
44
|
+
if (match[0].startsWith('@'))
|
|
45
|
+
modules.push(match[0]);
|
|
46
|
+
offset += match[0].length;
|
|
47
|
+
rest = rest.slice(match[0].length);
|
|
48
|
+
}
|
|
49
|
+
// Blanked, not removed, so Sass reports the file's own line numbers.
|
|
50
|
+
const kept = code.slice(0, offset).replace(/[^\n]/g, ' ') + rest;
|
|
51
|
+
const prefix = `${modules.join('\n')}\n${head}\n${SPLIT}\n`;
|
|
52
|
+
const firstLine = prefix.split('\n').length - 1;
|
|
53
|
+
let compiled;
|
|
54
|
+
try {
|
|
55
|
+
compiled = options.sass.compileString(prefix + kept, {
|
|
56
|
+
url: pathToFileURL(file),
|
|
57
|
+
logger: { warn: options.warn, debug: options.warn },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const span = error.span?.start;
|
|
62
|
+
throw new PreStyleError(`Sass failed to compile the file: ${error.message}`, span && span.line >= firstLine
|
|
63
|
+
? { line: span.line - firstLine, column: span.column + 1 }
|
|
64
|
+
: null, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
const at = compiled.css.indexOf(SPLIT);
|
|
67
|
+
head = at === -1 ? '' : compiled.css.slice(0, at);
|
|
68
|
+
body = at === -1 ? compiled.css : compiled.css.slice(at + SPLIT.length);
|
|
69
|
+
dependencies = (compiled.loadedUrls ?? [])
|
|
70
|
+
.filter((url) => url.protocol === 'file:')
|
|
71
|
+
.map((url) => fileURLToPath(url))
|
|
72
|
+
.filter((path) => path !== file);
|
|
73
|
+
}
|
|
74
|
+
// Sass output positions don't match the file, so scss atoms have none.
|
|
75
|
+
// Lightning CSS lines start from 0 and columns from 1, like positions.
|
|
76
|
+
const position = (loc) => lang === 'css' && loc ? { line: loc.line, column: loc.column } : null;
|
|
77
|
+
// The prepended code's rules are all global.
|
|
78
|
+
const prepended = { atoms: [], globals: [], composes: [] };
|
|
79
|
+
if (head.trim() !== '')
|
|
80
|
+
parseRules(head, options.lightningcss, none, (rules) => walk(rules, [], null, prepended, none));
|
|
81
|
+
const entries = [];
|
|
82
|
+
parseRules(body, options.lightningcss, position, (rules) => flatten(rules, [], null, entries, position));
|
|
83
|
+
// Every class that owns a rule, or is in a compound with the class that
|
|
84
|
+
// does, is exported. Other classes (such as `.dark`) are global names.
|
|
85
|
+
const classes = new Map();
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.global)
|
|
88
|
+
continue;
|
|
89
|
+
for (const selector of entry.selectors) {
|
|
90
|
+
const owner = ownerOf(selector);
|
|
91
|
+
if (owner === null)
|
|
92
|
+
continue;
|
|
93
|
+
for (const name of owner.compound)
|
|
94
|
+
if (!classes.has(name))
|
|
95
|
+
classes.set(name, { atoms: [], composes: [], marker: false });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Replaces exported classes, other than the one at `skip`, with markers.
|
|
99
|
+
const withMarkers = (selector, skip) => selector.map((component, i) => {
|
|
100
|
+
if (i === skip)
|
|
101
|
+
return { type: 'nesting' };
|
|
102
|
+
return replaceClasses(component, (name) => {
|
|
103
|
+
const target = classes.get(name);
|
|
104
|
+
if (target === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
target.marker = true;
|
|
107
|
+
return markerKey(id, name);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const parsed = { classes, globals: [...prepended.globals], dependencies };
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (entry.global) {
|
|
113
|
+
addGlobal(entry.global, entry.atRules, parsed.globals, position);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const owned = new Map();
|
|
117
|
+
const other = [];
|
|
118
|
+
for (const selector of entry.selectors) {
|
|
119
|
+
const owner = ownerOf(selector);
|
|
120
|
+
if (owner === null)
|
|
121
|
+
other.push(withMarkers(selector, -1));
|
|
122
|
+
else {
|
|
123
|
+
const list = owned.get(owner.name) ?? [];
|
|
124
|
+
list.push(withMarkers(selector, owner.index));
|
|
125
|
+
owned.set(owner.name, list);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const [name, selectors] of owned) {
|
|
129
|
+
const target = classes.get(name);
|
|
130
|
+
// `composes` only works in a rule for the class alone, outside
|
|
131
|
+
// nested selectors and at-rules.
|
|
132
|
+
const composes = !entry.nested && entry.atRules.length === 0 && isElement(selectors)
|
|
133
|
+
? target.composes
|
|
134
|
+
: null;
|
|
135
|
+
addDeclarations(entry.declarations, entry.atRules, selectors, target.atoms, composes, entry.position);
|
|
136
|
+
}
|
|
137
|
+
const { declarations = [], importantDeclarations = [] } = entry.declarations;
|
|
138
|
+
if (other.length > 0 && declarations.length + importantDeclarations.length > 0) {
|
|
139
|
+
const rule = {
|
|
140
|
+
type: 'style',
|
|
141
|
+
value: { selectors: other, declarations: entry.declarations, rules: [], loc: entry.loc },
|
|
142
|
+
};
|
|
143
|
+
addGlobal(rule, entry.atRules, parsed.globals, () => entry.position);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const target of classes.values())
|
|
147
|
+
target.atoms = dropOverridden(target.atoms);
|
|
148
|
+
// Keyframes are local to the whole file, as in CSS Modules.
|
|
149
|
+
if (options.keyframeLocalization) {
|
|
150
|
+
const local = localizeKeyframes(parsed.globals);
|
|
151
|
+
parsed.globals = local.globals;
|
|
152
|
+
for (const target of classes.values())
|
|
153
|
+
target.atoms = local.atoms(target.atoms);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
// Flattens rules into entries, in order: style rules with their full
|
|
158
|
+
// selectors, resolving nesting, and other rules that aren't atomic at-rules,
|
|
159
|
+
// which are global.
|
|
160
|
+
function flatten(rules, atRules, parent, entries, position) {
|
|
161
|
+
for (const rule of rules) {
|
|
162
|
+
if (rule.type === 'style') {
|
|
163
|
+
const { selectors, declarations, rules: children = [], loc } = rule.value;
|
|
164
|
+
const full = parent === null ? selectors : compose(parent, selectors);
|
|
165
|
+
entries.push({
|
|
166
|
+
selectors: full,
|
|
167
|
+
declarations: declarations ?? {},
|
|
168
|
+
atRules,
|
|
169
|
+
nested: parent !== null,
|
|
170
|
+
loc,
|
|
171
|
+
position: position(loc),
|
|
172
|
+
});
|
|
173
|
+
flatten(children, atRules, full, entries, position);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (rule.type === 'nested-declarations' && parent !== null) {
|
|
177
|
+
entries.push({
|
|
178
|
+
selectors: parent,
|
|
179
|
+
declarations: rule.value.declarations ?? {},
|
|
180
|
+
atRules,
|
|
181
|
+
nested: true,
|
|
182
|
+
loc: rule.value.loc,
|
|
183
|
+
position: position(rule.value.loc),
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (atRuleInfo(rule.type).atomic) {
|
|
188
|
+
const { rules: children, ...value } = valueOf(rule);
|
|
189
|
+
const segment = {
|
|
190
|
+
key: key({ type: rule.type, value }),
|
|
191
|
+
rule: { type: rule.type, value },
|
|
192
|
+
};
|
|
193
|
+
flatten(children, [...atRules, segment], parent, entries, position);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
entries.push({
|
|
197
|
+
global: rule,
|
|
198
|
+
selectors: [],
|
|
199
|
+
declarations: {},
|
|
200
|
+
atRules,
|
|
201
|
+
nested: parent !== null,
|
|
202
|
+
loc: undefined,
|
|
203
|
+
position: null,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// The class a selector's declarations belong to: the first class in the
|
|
208
|
+
// rightmost compound that has one, so `.card .title` belongs to `title` and
|
|
209
|
+
// `.card > div` to `card`. `compound` lists every class in that compound.
|
|
210
|
+
// Null when the selector has no class outside pseudo-class arguments.
|
|
211
|
+
function ownerOf(selector) {
|
|
212
|
+
let end = selector.length;
|
|
213
|
+
for (let start = selector.length - 1; start >= -1; start--) {
|
|
214
|
+
if (start !== -1 && selector[start].type !== 'combinator')
|
|
215
|
+
continue;
|
|
216
|
+
const compound = [];
|
|
217
|
+
for (let i = start + 1; i < end; i++) {
|
|
218
|
+
const c = selector[i];
|
|
219
|
+
if (c.type === 'class')
|
|
220
|
+
compound.push({ name: c.name, index: i });
|
|
221
|
+
}
|
|
222
|
+
if (compound.length > 0)
|
|
223
|
+
return { ...compound[0], compound: compound.map(({ name }) => name) };
|
|
224
|
+
end = start;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
// Replaces class names in a selector component, including inside
|
|
229
|
+
// pseudo-class arguments. `replace` returns the new name, or null to keep it.
|
|
230
|
+
function replaceClasses(component, replace) {
|
|
231
|
+
const visit = (value) => {
|
|
232
|
+
if (Array.isArray(value))
|
|
233
|
+
return value.map(visit);
|
|
234
|
+
if (value === null || typeof value !== 'object')
|
|
235
|
+
return value;
|
|
236
|
+
const node = value;
|
|
237
|
+
if (node.type === 'class' && typeof node.name === 'string') {
|
|
238
|
+
const name = replace(node.name);
|
|
239
|
+
return name === null ? node : { ...node, name };
|
|
240
|
+
}
|
|
241
|
+
return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, visit(v)]));
|
|
242
|
+
};
|
|
243
|
+
return visit(component);
|
|
244
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pre.style/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0-alpha.1",
|
|
4
4
|
"description": "Zero-runtime CSS-in-JS that compiles css and scss blocks into atomic CSS classes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atomic-css",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"directory": "packages/core"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"package.schema.json"
|
|
31
32
|
],
|
|
32
33
|
"type": "module",
|
|
33
34
|
"exports": {
|
|
@@ -40,7 +41,8 @@
|
|
|
40
41
|
"pre.style-source": "./src/compiler/index.ts",
|
|
41
42
|
"types": "./dist/compiler/index.d.ts",
|
|
42
43
|
"default": "./dist/compiler/index.js"
|
|
43
|
-
}
|
|
44
|
+
},
|
|
45
|
+
"./package.schema.json": "./package.schema.json"
|
|
44
46
|
},
|
|
45
47
|
"publishConfig": {
|
|
46
48
|
"access": "public",
|
|
@@ -72,6 +74,16 @@
|
|
|
72
74
|
"optional": true
|
|
73
75
|
}
|
|
74
76
|
},
|
|
77
|
+
"typescript": {
|
|
78
|
+
"contentMapper": {
|
|
79
|
+
"compilerOptions": [],
|
|
80
|
+
"dynamicConfig": true,
|
|
81
|
+
"exec": [
|
|
82
|
+
"node",
|
|
83
|
+
"dist/compiler/mapper.js"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
},
|
|
75
87
|
"engines": {
|
|
76
88
|
"node": ">=24"
|
|
77
89
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "pre.style package.json field",
|
|
4
|
+
"description": "The \"pre.style\" field in package.json. In an app, it holds the options every tool that reads the project's styles shares. In a component library, it marks the package as a library for apps to compile.",
|
|
5
|
+
"definitions": {
|
|
6
|
+
"globs": {
|
|
7
|
+
"anyOf": [
|
|
8
|
+
{
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "array",
|
|
13
|
+
"items": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"prependFiles": {
|
|
20
|
+
"description": "Files added to the start of every block and stylesheet file, for custom properties, Sass variables, and mixins. An array applies to both languages; `css` and `scss` apply to one. Relative to the package.",
|
|
21
|
+
"anyOf": [
|
|
22
|
+
{
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"css": {
|
|
32
|
+
"description": "Files added to the start of every css block and .css stylesheet file.",
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scss": {
|
|
39
|
+
"description": "Files added to the start of every scss block and .scss stylesheet file.",
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"library": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"include": {
|
|
54
|
+
"$ref": "#/definitions/globs",
|
|
55
|
+
"description": "Globs of the library's files with blocks, relative to the package. Defaults to every JS, TS, and Astro file."
|
|
56
|
+
},
|
|
57
|
+
"exclude": {
|
|
58
|
+
"$ref": "#/definitions/globs",
|
|
59
|
+
"description": "Globs removed from `include`."
|
|
60
|
+
},
|
|
61
|
+
"prependFiles": {
|
|
62
|
+
"$ref": "#/definitions/prependFiles"
|
|
63
|
+
},
|
|
64
|
+
"keyframeLocalization": {
|
|
65
|
+
"description": "Give each @keyframes in the library's blocks and stylesheet files a generated name, used only in the block or file that defines it, like styled-components' `keyframes`. The app's setting doesn't apply to libraries.",
|
|
66
|
+
"type": "boolean"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"anyOf": [
|
|
72
|
+
{
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"const": true,
|
|
75
|
+
"description": "A component library compiled with the default settings."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"include": {
|
|
81
|
+
"$ref": "#/definitions/globs",
|
|
82
|
+
"description": "Globs of every source file with blocks, relative to the package. Scanned before a build to assign names. Defaults to every JS, TS, HTML, and Astro file."
|
|
83
|
+
},
|
|
84
|
+
"exclude": {
|
|
85
|
+
"$ref": "#/definitions/globs",
|
|
86
|
+
"description": "Globs removed from `include`."
|
|
87
|
+
},
|
|
88
|
+
"prependFiles": {
|
|
89
|
+
"$ref": "#/definitions/prependFiles"
|
|
90
|
+
},
|
|
91
|
+
"keyframeLocalization": {
|
|
92
|
+
"description": "Give each @keyframes in the app's blocks and stylesheet files a generated name, used only in the block or file that defines it (including keyframes from prepended files), like styled-components' `keyframes`. Libraries use their own setting.",
|
|
93
|
+
"type": "boolean"
|
|
94
|
+
},
|
|
95
|
+
"libraries": {
|
|
96
|
+
"description": "App only. Component libraries to compile, by package name, in addition to dependencies with a \"pre.style\" field. A config replaces the package's field, `true` uses the defaults, and `false` leaves the package out.",
|
|
97
|
+
"type": "object",
|
|
98
|
+
"additionalProperties": {
|
|
99
|
+
"anyOf": [
|
|
100
|
+
{
|
|
101
|
+
"type": "boolean"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"$ref": "#/definitions/library"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"quirksMode": {
|
|
110
|
+
"description": "App only. You probably don't want this: lowercase class names only, for pages in quirks mode (no doctype). Names get longer.",
|
|
111
|
+
"type": "boolean"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"additionalProperties": false
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|