@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.
@@ -0,0 +1,236 @@
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 { compose, isElement } from './selectors.js';
5
+ // Classes used as context for another class (`.card:hover .title`), or in a
6
+ // compound with it (`.a.b`), are replaced with a marker: one generated name
7
+ // added to that class's string. In parsed selectors, a marker is a class
8
+ // named with this prefix, the stylesheet's ID, and the class, so markers
9
+ // from different files never share a key. The compiler gives it a name when
10
+ // printing.
11
+ export const MARKER_PREFIX = '\0pre.style-marker:';
12
+ export const markerKey = (id, name) => `${MARKER_PREFIX}${id}\0${name}`;
13
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
14
+ 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(' '));
15
+ // Whether a class can be exported as a `const` of the same name. Others
16
+ // (such as `primary-button`) are exported by string name.
17
+ export const isIdentifier = (name) => IDENTIFIER.test(name) && !RESERVED.has(name);
18
+ // Separates the CSS Sass writes for prepended code (and modules loaded with
19
+ // `@use`, which Sass writes first) from the file's own.
20
+ const SPLIT = '/*! __pre-style-split__ */';
21
+ // Leading `@use` and `@forward` rules, which Sass requires before any other
22
+ // rule, so they're moved above the prepended code.
23
+ const LEADING_MODULE_RULE = /^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/|@(?:use|forward)\b[^;]*;)/;
24
+ // A style rule, or (with `global`) another rule that isn't an atomic
25
+ // at-rule.
26
+ const none = () => null;
27
+ // Parses a stylesheet file. Each class becomes a block: its rules, with the
28
+ // class replaced by `&`, wherever they appear in the file. Rules that don't
29
+ // target a class, and the prepended code's rules, are global. `id` names the
30
+ // file in marker keys.
31
+ export function parseStylesheet(code, lang, file, id, options) {
32
+ if (lang === 'scss' && options.sass === null) {
33
+ throw new PreStyleError('Found an scss file, but Sass is not installed. Install the `sass` package to use scss files.', null);
34
+ }
35
+ let head = options.prepend[lang];
36
+ let body = code;
37
+ let dependencies = [];
38
+ if (lang === 'scss') {
39
+ const modules = [];
40
+ let rest = code;
41
+ let offset = 0;
42
+ for (let match; (match = LEADING_MODULE_RULE.exec(rest));) {
43
+ if (match[0].startsWith('@'))
44
+ modules.push(match[0]);
45
+ offset += match[0].length;
46
+ rest = rest.slice(match[0].length);
47
+ }
48
+ // Blanked, not removed, so Sass reports the file's own line numbers.
49
+ const kept = code.slice(0, offset).replace(/[^\n]/g, ' ') + rest;
50
+ const prefix = `${modules.join('\n')}\n${head}\n${SPLIT}\n`;
51
+ const firstLine = prefix.split('\n').length - 1;
52
+ let compiled;
53
+ try {
54
+ compiled = options.sass.compileString(prefix + kept, {
55
+ url: pathToFileURL(file),
56
+ logger: { warn: options.warn, debug: options.warn },
57
+ });
58
+ }
59
+ catch (error) {
60
+ const span = error.span?.start;
61
+ throw new PreStyleError(`Sass failed to compile the file: ${error.message}`, span && span.line >= firstLine
62
+ ? { line: span.line - firstLine, column: span.column + 1 }
63
+ : null, { cause: error });
64
+ }
65
+ const at = compiled.css.indexOf(SPLIT);
66
+ head = at === -1 ? '' : compiled.css.slice(0, at);
67
+ body = at === -1 ? compiled.css : compiled.css.slice(at + SPLIT.length);
68
+ dependencies = (compiled.loadedUrls ?? [])
69
+ .filter((url) => url.protocol === 'file:')
70
+ .map((url) => fileURLToPath(url))
71
+ .filter((path) => path !== file);
72
+ }
73
+ // Sass output positions don't match the file, so scss atoms have none.
74
+ // Lightning CSS lines start from 0 and columns from 1, like positions.
75
+ const position = (loc) => lang === 'css' && loc ? { line: loc.line, column: loc.column } : null;
76
+ // The prepended code's rules are all global.
77
+ const prepended = { atoms: [], globals: [], composes: [] };
78
+ if (head.trim() !== '')
79
+ parseRules(head, options.lightningcss, none, (rules) => walk(rules, [], null, prepended, none));
80
+ const entries = [];
81
+ parseRules(body, options.lightningcss, position, (rules) => flatten(rules, [], null, entries, position));
82
+ // Every class that owns a rule, or is in a compound with the class that
83
+ // does, is exported. Other classes (such as `.dark`) are global names.
84
+ const classes = new Map();
85
+ for (const entry of entries) {
86
+ if (entry.global)
87
+ continue;
88
+ for (const selector of entry.selectors) {
89
+ const owner = ownerOf(selector);
90
+ if (owner === null)
91
+ continue;
92
+ for (const name of owner.compound)
93
+ if (!classes.has(name))
94
+ classes.set(name, { atoms: [], composes: [], marker: false });
95
+ }
96
+ }
97
+ // Replaces exported classes, other than the one at `skip`, with markers.
98
+ const withMarkers = (selector, skip) => selector.map((component, i) => {
99
+ if (i === skip)
100
+ return { type: 'nesting' };
101
+ return replaceClasses(component, (name) => {
102
+ const target = classes.get(name);
103
+ if (target === undefined)
104
+ return null;
105
+ target.marker = true;
106
+ return markerKey(id, name);
107
+ });
108
+ });
109
+ const parsed = { classes, globals: [...prepended.globals], dependencies };
110
+ for (const entry of entries) {
111
+ if (entry.global) {
112
+ addGlobal(entry.global, entry.atRules, parsed.globals, position);
113
+ continue;
114
+ }
115
+ const owned = new Map();
116
+ const other = [];
117
+ for (const selector of entry.selectors) {
118
+ const owner = ownerOf(selector);
119
+ if (owner === null)
120
+ other.push(withMarkers(selector, -1));
121
+ else {
122
+ const list = owned.get(owner.name) ?? [];
123
+ list.push(withMarkers(selector, owner.index));
124
+ owned.set(owner.name, list);
125
+ }
126
+ }
127
+ for (const [name, selectors] of owned) {
128
+ const target = classes.get(name);
129
+ // `composes` only works in a rule for the class alone, outside
130
+ // nested selectors and at-rules.
131
+ const composes = !entry.nested && entry.atRules.length === 0 && isElement(selectors)
132
+ ? target.composes
133
+ : null;
134
+ addDeclarations(entry.declarations, entry.atRules, selectors, target.atoms, composes, entry.position);
135
+ }
136
+ const { declarations = [], importantDeclarations = [] } = entry.declarations;
137
+ if (other.length > 0 && declarations.length + importantDeclarations.length > 0) {
138
+ const rule = {
139
+ type: 'style',
140
+ value: { selectors: other, declarations: entry.declarations, rules: [], loc: entry.loc },
141
+ };
142
+ addGlobal(rule, entry.atRules, parsed.globals, () => entry.position);
143
+ }
144
+ }
145
+ for (const target of classes.values())
146
+ target.atoms = dropOverridden(target.atoms);
147
+ return parsed;
148
+ }
149
+ // Flattens rules into entries, in order: style rules with their full
150
+ // selectors, resolving nesting, and other rules that aren't atomic at-rules,
151
+ // which are global.
152
+ function flatten(rules, atRules, parent, entries, position) {
153
+ for (const rule of rules) {
154
+ if (rule.type === 'style') {
155
+ const { selectors, declarations, rules: children = [], loc } = rule.value;
156
+ const full = parent === null ? selectors : compose(parent, selectors);
157
+ entries.push({
158
+ selectors: full,
159
+ declarations: declarations ?? {},
160
+ atRules,
161
+ nested: parent !== null,
162
+ loc,
163
+ position: position(loc),
164
+ });
165
+ flatten(children, atRules, full, entries, position);
166
+ continue;
167
+ }
168
+ if (rule.type === 'nested-declarations' && parent !== null) {
169
+ entries.push({
170
+ selectors: parent,
171
+ declarations: rule.value.declarations ?? {},
172
+ atRules,
173
+ nested: true,
174
+ loc: rule.value.loc,
175
+ position: position(rule.value.loc),
176
+ });
177
+ continue;
178
+ }
179
+ if (atRuleInfo(rule.type).atomic) {
180
+ const { rules: children, ...value } = valueOf(rule);
181
+ const segment = {
182
+ key: key({ type: rule.type, value }),
183
+ rule: { type: rule.type, value },
184
+ };
185
+ flatten(children, [...atRules, segment], parent, entries, position);
186
+ continue;
187
+ }
188
+ entries.push({
189
+ global: rule,
190
+ selectors: [],
191
+ declarations: {},
192
+ atRules,
193
+ nested: parent !== null,
194
+ loc: undefined,
195
+ position: null,
196
+ });
197
+ }
198
+ }
199
+ // The class a selector's declarations belong to: the first class in the
200
+ // rightmost compound that has one, so `.card .title` belongs to `title` and
201
+ // `.card > div` to `card`. `compound` lists every class in that compound.
202
+ // Null when the selector has no class outside pseudo-class arguments.
203
+ function ownerOf(selector) {
204
+ let end = selector.length;
205
+ for (let start = selector.length - 1; start >= -1; start--) {
206
+ if (start !== -1 && selector[start].type !== 'combinator')
207
+ continue;
208
+ const compound = [];
209
+ for (let i = start + 1; i < end; i++) {
210
+ const c = selector[i];
211
+ if (c.type === 'class')
212
+ compound.push({ name: c.name, index: i });
213
+ }
214
+ if (compound.length > 0)
215
+ return { ...compound[0], compound: compound.map(({ name }) => name) };
216
+ end = start;
217
+ }
218
+ return null;
219
+ }
220
+ // Replaces class names in a selector component, including inside
221
+ // pseudo-class arguments. `replace` returns the new name, or null to keep it.
222
+ function replaceClasses(component, replace) {
223
+ const visit = (value) => {
224
+ if (Array.isArray(value))
225
+ return value.map(visit);
226
+ if (value === null || typeof value !== 'object')
227
+ return value;
228
+ const node = value;
229
+ if (node.type === 'class' && typeof node.name === 'string') {
230
+ const name = replace(node.name);
231
+ return name === null ? node : { ...node, name };
232
+ }
233
+ return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, visit(v)]));
234
+ };
235
+ return visit(component);
236
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pre.style/core",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.1.0-alpha.0",
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,109 @@
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
+ }
65
+ }
66
+ },
67
+ "anyOf": [
68
+ {
69
+ "type": "boolean",
70
+ "const": true,
71
+ "description": "A component library compiled with the default settings."
72
+ },
73
+ {
74
+ "type": "object",
75
+ "properties": {
76
+ "include": {
77
+ "$ref": "#/definitions/globs",
78
+ "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."
79
+ },
80
+ "exclude": {
81
+ "$ref": "#/definitions/globs",
82
+ "description": "Globs removed from `include`."
83
+ },
84
+ "prependFiles": {
85
+ "$ref": "#/definitions/prependFiles"
86
+ },
87
+ "libraries": {
88
+ "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.",
89
+ "type": "object",
90
+ "additionalProperties": {
91
+ "anyOf": [
92
+ {
93
+ "type": "boolean"
94
+ },
95
+ {
96
+ "$ref": "#/definitions/library"
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ "quirksMode": {
102
+ "description": "App only. You probably don't want this: lowercase class names only, for pages in quirks mode (no doctype). Names get longer.",
103
+ "type": "boolean"
104
+ }
105
+ },
106
+ "additionalProperties": false
107
+ }
108
+ ]
109
+ }