@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.
@@ -31,9 +31,22 @@ export interface GlobalRule {
31
31
  overrideKey: string | null;
32
32
  position: Position | null;
33
33
  }
34
+ export interface Compose {
35
+ names: string[];
36
+ from: {
37
+ type: 'local';
38
+ } | {
39
+ type: 'global';
40
+ } | {
41
+ type: 'file';
42
+ path: string;
43
+ };
44
+ position: Position | null;
45
+ }
34
46
  export interface ParsedBlock {
35
47
  atoms: Atom[];
36
48
  globals: GlobalRule[];
49
+ composes: Compose[];
37
50
  }
38
51
  export declare class PreStyleError extends Error {
39
52
  readonly position: Position | null;
@@ -41,12 +54,14 @@ export declare class PreStyleError extends Error {
41
54
  }
42
55
  export interface SassCompiler {
43
56
  compileString(source: string, options?: {
57
+ url?: URL;
44
58
  logger?: {
45
59
  warn?(message: string): void;
46
60
  debug?(message: string): void;
47
61
  };
48
62
  }): {
49
63
  css: string;
64
+ loadedUrls?: URL[];
50
65
  };
51
66
  }
52
67
  export interface ParseOptions {
@@ -54,9 +69,24 @@ export interface ParseOptions {
54
69
  prepend: Record<Lang, string>;
55
70
  sass: SassCompiler | null;
56
71
  warn: (message: string) => void;
72
+ keyframeLocalization: boolean;
57
73
  }
74
+ export declare const key: (value: unknown) => string;
58
75
  export declare function parseBlock(code: string, lang: Lang, options: ParseOptions): ParsedBlock;
76
+ export declare function parseRules(css: string, lightningcss: LightningOptions, position: ToPosition, collect: (rules: Rule[]) => void): void;
77
+ export declare function dropOverridden(atoms: Atom[]): Atom[];
78
+ export type ToPosition = (loc: {
79
+ line: number;
80
+ column: number;
81
+ } | undefined) => Position | null;
59
82
  export declare const valueOf: (rule: Rule) => Record<string, any>;
83
+ export declare function walk(rules: Rule[], atRules: AtRuleSegment[], variant: SelectorList | null, parsed: ParsedBlock, position: ToPosition): void;
84
+ export declare function addDeclarations(block: {
85
+ declarations?: Declaration[];
86
+ importantDeclarations?: Declaration[];
87
+ } | undefined, atRules: AtRuleSegment[], selectors: SelectorList, atoms: Atom[], composes: Compose[] | null, position: Position | null): void;
88
+ export declare function addGlobal(rule: Rule, atRules: AtRuleSegment[], globals: GlobalRule[], position: ToPosition): void;
89
+ export declare function overrideKey(rule: Rule): string;
60
90
  export declare function propertyOf(declaration: Declaration): {
61
91
  property: string;
62
92
  shorthand: boolean;
@@ -1,6 +1,7 @@
1
1
  import { composeVisitors, transform } from 'lightningcss';
2
2
  import { atRuleInfo } from './at-rules.js';
3
3
  import { expand } from './expand.js';
4
+ import { localizeKeyframes } from './keyframes.js';
4
5
  import { compose, fromPlaceholder, isElement, PLACEHOLDER, repair } from './selectors.js';
5
6
  import { splitBlock } from './split.js';
6
7
  import { SHORTHANDS } from '../shorthands.js';
@@ -14,7 +15,7 @@ export class PreStyleError extends Error {
14
15
  }
15
16
  // JSON keys used to deduplicate. Locations are left out, so identical rules
16
17
  // from different places share a key.
17
- const key = (value) => JSON.stringify(value, (k, v) => (k === 'loc' ? undefined : v));
18
+ export const key = (value) => JSON.stringify(value, (k, v) => (k === 'loc' ? undefined : v));
18
19
  const HOIST_ORDER = { charset: 0, layer: 1, import: 2, namespace: 3 };
19
20
  const hoistOrder = (rule) => HOIST_ORDER[/^@([\w-]+)/.exec(rule)[1].toLowerCase()] ?? Infinity;
20
21
  // Joins rules onto one line.
@@ -78,38 +79,46 @@ export function parseBlock(code, lang, options) {
78
79
  });
79
80
  }
80
81
  }
81
- const parsed = { atoms: [], globals: [] };
82
+ const parsed = { atoms: [], globals: [], composes: [] };
82
83
  // Sass output positions don't match the block, so scss atoms have none.
83
84
  const position = (loc) => lang === 'css' && loc ? toBlock(loc.line, loc.column) : null;
84
- // Collected on exit, after the user's visitor has changed the rules.
85
- const collect = {
85
+ parseRules(css, options.lightningcss, position, (rules) => walk(rules, [], null, parsed, position));
86
+ const atoms = dropOverridden(parsed.atoms);
87
+ if (!options.keyframeLocalization)
88
+ return { atoms, globals: parsed.globals, composes: parsed.composes };
89
+ const local = localizeKeyframes(parsed.globals);
90
+ return { atoms: local.atoms(atoms), globals: local.globals, composes: parsed.composes };
91
+ }
92
+ // Parses CSS with Lightning CSS, and passes its rules to `collect` once the
93
+ // user's visitor has changed them. `position` converts error locations.
94
+ export function parseRules(css, lightningcss, position, collect) {
95
+ const exit = {
86
96
  StyleSheetExit(sheet) {
87
- walk(sheet.rules, [], null, parsed, position);
97
+ collect(sheet.rules);
88
98
  },
89
99
  };
90
- const { visitor, ...rest } = options.lightningcss;
100
+ const { visitor, ...rest } = lightningcss;
91
101
  try {
92
102
  transform({
93
103
  ...rest,
94
104
  filename: 'block.css',
95
105
  code: Buffer.from(css),
96
- visitor: visitor ? composeVisitors([visitor, collect]) : collect,
106
+ visitor: visitor ? composeVisitors([visitor, exit]) : exit,
97
107
  });
98
108
  }
99
109
  catch (error) {
110
+ if (error instanceof PreStyleError)
111
+ throw error;
100
112
  const loc = error.loc;
101
113
  // Lightning CSS reports error lines from 1.
102
- throw new PreStyleError(`Invalid CSS: ${error.message}`, lang === 'css' && loc ? toBlock(loc.line - 1, loc.column) : null, {
103
- cause: error,
104
- });
114
+ throw new PreStyleError(`Invalid CSS: ${error.message}`, loc ? position({ line: loc.line - 1, column: loc.column }) : null, { cause: error });
105
115
  }
106
- return { atoms: dropOverridden(parsed.atoms), globals: parsed.globals };
107
116
  }
108
117
  // Within a block, a later declaration overrides an earlier one that sets the
109
118
  // same property under the same at-rules and selector variant, as in CSS.
110
119
  // `!important` declarations are collected after normal ones in each rule,
111
120
  // so they win. Identical atoms are kept once.
112
- function dropOverridden(atoms) {
121
+ export function dropOverridden(atoms) {
113
122
  const seen = new Set();
114
123
  const kept = [];
115
124
  for (let i = atoms.length - 1; i >= 0; i--) {
@@ -129,14 +138,15 @@ function dropOverridden(atoms) {
129
138
  export const valueOf = (rule) => 'value' in rule && rule.value !== null ? rule.value : {};
130
139
  const ruleLoc = (rule) => valueOf(rule).loc;
131
140
  // Walks the rules. `variant` is the element's selector variant when inside a
132
- // rule that targets the element, and null outside one.
133
- function walk(rules, atRules, variant, parsed, position) {
141
+ // rule that targets the element, and null outside one. Without the
142
+ // placeholder, every rule is global.
143
+ export function walk(rules, atRules, variant, parsed, position) {
134
144
  for (const rule of rules) {
135
145
  if (rule.type === 'style') {
136
146
  const { selectors, declarations, rules: children = [], loc } = rule.value;
137
147
  if (variant !== null) {
138
148
  const composed = compose(variant, selectors);
139
- addDeclarations(declarations, atRules, composed, parsed, position(loc));
149
+ addDeclarations(declarations, atRules, composed, parsed.atoms, null, position(loc));
140
150
  walk(children, atRules, composed, parsed, position);
141
151
  continue;
142
152
  }
@@ -151,15 +161,17 @@ function walk(rules, atRules, variant, parsed, position) {
151
161
  other.push(selector);
152
162
  }
153
163
  if (other.length > 0)
154
- addGlobal({ ...rule, value: { ...rule.value, selectors: other } }, atRules, parsed, position);
164
+ addGlobal({ ...rule, value: { ...rule.value, selectors: other } }, atRules, parsed.globals, position);
155
165
  if (element.length > 0) {
156
- addDeclarations(declarations, atRules, element, parsed, position(loc));
166
+ // `composes` only works directly in the block, outside at-rules.
167
+ const composes = atRules.length === 0 && isElement(element) ? parsed.composes : null;
168
+ addDeclarations(declarations, atRules, element, parsed.atoms, composes, position(loc));
157
169
  walk(children, atRules, element, parsed, position);
158
170
  }
159
171
  continue;
160
172
  }
161
173
  if (rule.type === 'nested-declarations' && variant !== null) {
162
- addDeclarations(rule.value.declarations, atRules, variant, parsed, position(rule.value.loc));
174
+ addDeclarations(rule.value.declarations, atRules, variant, parsed.atoms, null, position(rule.value.loc));
163
175
  continue;
164
176
  }
165
177
  if (atRuleInfo(rule.type).atomic) {
@@ -171,20 +183,40 @@ function walk(rules, atRules, variant, parsed, position) {
171
183
  walk(children, [...atRules, segment], variant, parsed, position);
172
184
  continue;
173
185
  }
174
- addGlobal(rule, atRules, parsed, position);
186
+ addGlobal(rule, atRules, parsed.globals, position);
175
187
  }
176
188
  }
177
- function addDeclarations(block, atRules, selectors, parsed, position) {
189
+ const COMPOSES_PLACEMENT = "`composes` must come before the rule's other declarations, and can't be used inside nested selectors or at-rules.";
190
+ const isComposes = (declaration) => declaration.property === 'unparsed' && declaration.value.propertyId.property === 'composes';
191
+ // Adds a rule's declarations as atoms. `composes` declarations are added to
192
+ // `composes` when it's given, and are an error where it isn't (inside
193
+ // nested selectors and at-rules).
194
+ export function addDeclarations(block, atRules, selectors, atoms, composes, position) {
178
195
  let variant = null;
179
196
  if (!isElement(selectors)) {
180
197
  const repaired = repair(selectors);
181
198
  variant = { key: key(repaired), selectors: repaired };
182
199
  }
200
+ if (block?.importantDeclarations?.some(isComposes))
201
+ throw new PreStyleError("`composes` can't be `!important`.", position);
202
+ let normal = block?.declarations ?? [];
203
+ // As in CSS Modules, `composes` comes before the rule's other declarations.
204
+ const leading = normal.findIndex((d) => !isComposes(d));
205
+ const own = leading === -1 ? normal.length : leading;
206
+ if (normal.slice(own).some(isComposes))
207
+ throw new PreStyleError(COMPOSES_PLACEMENT, position);
208
+ if (own > 0) {
209
+ if (composes === null)
210
+ throw new PreStyleError(COMPOSES_PLACEMENT, position);
211
+ for (const d of normal.slice(0, own))
212
+ composes.push(parseComposes(d, position));
213
+ normal = normal.slice(own);
214
+ }
183
215
  const add = (declarations = [], important) => {
184
216
  for (const declaration of declarations) {
185
217
  for (const value of expand(declaration)) {
186
218
  const { property, shorthand } = propertyOf(value);
187
- parsed.atoms.push({
219
+ atoms.push({
188
220
  atRules,
189
221
  variant,
190
222
  declaration: { key: key(value) + (important ? '!' : ''), value, important },
@@ -195,11 +227,38 @@ function addDeclarations(block, atRules, selectors, parsed, position) {
195
227
  }
196
228
  }
197
229
  };
198
- add(block?.declarations, false);
230
+ add(normal, false);
199
231
  add(block?.importantDeclarations, true);
200
232
  }
233
+ const ident = (t) => t?.type === 'token' && t.value.type === 'ident' ? t.value.value : null;
234
+ // Reads `composes: a b`, `composes: a from './file.css'`, or
235
+ // `composes: a from global`.
236
+ function parseComposes(declaration, position) {
237
+ const tokens = declaration.value.value.filter((t) => !(t.type === 'token' && t.value.type === 'white-space'));
238
+ const invalid = () => new PreStyleError('Invalid `composes`. Use `composes: a b;`, `composes: a from "./file.css";`, or `composes: a from global;`.', position);
239
+ const names = [];
240
+ let i = 0;
241
+ for (; i < tokens.length; i++) {
242
+ const name = ident(tokens[i]);
243
+ if (name === null || name === 'from')
244
+ break;
245
+ names.push(name);
246
+ }
247
+ if (names.length === 0)
248
+ throw invalid();
249
+ if (i === tokens.length)
250
+ return { names, from: { type: 'local' }, position };
251
+ if (ident(tokens[i]) !== 'from' || tokens.length !== i + 2)
252
+ throw invalid();
253
+ const source = tokens[i + 1];
254
+ if (ident(source) === 'global')
255
+ return { names, from: { type: 'global' }, position };
256
+ if (source.type === 'token' && source.value.type === 'string')
257
+ return { names, from: { type: 'file', path: source.value.value }, position };
258
+ throw invalid();
259
+ }
201
260
  // Global rules inside at-rules keep their at-rules around them.
202
- function addGlobal(rule, atRules, parsed, position) {
261
+ export function addGlobal(rule, atRules, globals, position) {
203
262
  let wrapped = rule;
204
263
  for (const segment of atRules.toReversed()) {
205
264
  wrapped = {
@@ -209,7 +268,7 @@ function addGlobal(rule, atRules, parsed, position) {
209
268
  }
210
269
  const info = atRules.length === 0 ? atRuleInfo(rule.type) : atRuleInfo('');
211
270
  wrapped = repair(wrapped);
212
- parsed.globals.push({
271
+ globals.push({
213
272
  key: key(wrapped),
214
273
  rule: wrapped,
215
274
  hoist: info.hoist,
@@ -218,11 +277,12 @@ function addGlobal(rule, atRules, parsed, position) {
218
277
  });
219
278
  }
220
279
  // The identity of an overridable at-rule: its type and name.
221
- function overrideKey(rule) {
280
+ export function overrideKey(rule) {
222
281
  const value = valueOf(rule);
223
282
  switch (rule.type) {
224
283
  case 'keyframes':
225
- return `keyframes ${key(value.name)}`;
284
+ // `@-webkit-keyframes x` doesn't replace `@keyframes x`.
285
+ return `keyframes ${key(value.vendorPrefix ?? [])} ${key(value.name)}`;
226
286
  case 'font-face': {
227
287
  // A font face is identified by its family, style, weight, and stretch.
228
288
  const descriptors = Object.fromEntries(value.properties
@@ -8,6 +8,7 @@ export type PrependFiles = string[] | {
8
8
  export interface Library {
9
9
  dir: string;
10
10
  prependFiles?: PrependFiles;
11
+ keyframeLocalization?: boolean;
11
12
  }
12
13
  export interface CompilerOptions {
13
14
  prependFiles?: PrependFiles;
@@ -17,6 +18,8 @@ export interface CompilerOptions {
17
18
  root?: string;
18
19
  warn?: (message: string) => void;
19
20
  quirksMode?: boolean;
21
+ keyframeLocalization?: boolean;
22
+ layer?: false | string;
20
23
  }
21
24
  export interface Block {
22
25
  code: string;
@@ -25,14 +28,27 @@ export interface Block {
25
28
  line?: number;
26
29
  column?: number;
27
30
  }
31
+ export interface Stylesheet {
32
+ code: string;
33
+ lang: Lang;
34
+ file: string;
35
+ }
36
+ export interface CompiledStylesheet {
37
+ exports: [name: string, classString: string][];
38
+ dependencies: string[];
39
+ }
28
40
  export declare function createCompiler(options?: CompilerOptions): Promise<Compiler>;
29
41
  export declare class Compiler {
30
42
  #private;
31
43
  constructor(options: CompilerOptions, sass: SassCompiler | null);
32
44
  get revision(): number;
33
45
  scan(block: Block): void;
46
+ scanStylesheet(sheet: Stylesheet): void;
34
47
  assignNames(): void;
35
48
  classString(block: Block): string;
49
+ dependencies(block: Block): string[];
50
+ classNames(sheet: Stylesheet): string[];
51
+ stylesheet(sheet: Stylesheet): CompiledStylesheet;
36
52
  css(): Printed;
37
53
  cnTable(): CnTable;
38
54
  }