@pre.style/core 3.1.0-alpha.1 → 3.1.0-alpha.3

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,272 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { transform } from 'lightningcss';
3
+ import { key, overrideKey, PreStyleError, valueOf } from './block.js';
4
+ import { atRuleInfo } from './at-rules.js';
5
+ import { SLOT_PREFIX } from './interpolate.js';
6
+ import { print } from './print.js';
7
+ import { withClass } from './selectors.js';
8
+ // A variable a block's runtime values set. In parsed rules, a generated
9
+ // variable's name is this prefix and a hash of what it's for, and the compiler
10
+ // gives it a name when printing, like localized keyframes.
11
+ export const VARIABLE_PREFIX = '--\0pre.style-variable:';
12
+ const isSlot = (node) => {
13
+ const value = node;
14
+ return value?.type === 'var' && value.value.name.ident.startsWith(SLOT_PREFIX);
15
+ };
16
+ // A placeholder's slot, and its type from `type()`, which is in its name (in
17
+ // hex) so that blocks differing only by type don't share a parse.
18
+ const parseSlot = (node) => {
19
+ const [, slot, type] = /^(\d+)(?:-t([\da-f]+))?/.exec(node.value.name.ident.slice(SLOT_PREFIX.length));
20
+ return {
21
+ slot: Number(slot),
22
+ syntax: type === undefined ? null : Buffer.from(type, 'hex').toString(),
23
+ };
24
+ };
25
+ // Calls `visit` for each slot in a value, in the order they're written.
26
+ function eachSlot(value, visit) {
27
+ if (Array.isArray(value)) {
28
+ for (const item of value)
29
+ eachSlot(item, visit);
30
+ return;
31
+ }
32
+ if (value === null || typeof value !== 'object')
33
+ return;
34
+ if (isSlot(value))
35
+ visit(value);
36
+ for (const child of Object.values(value))
37
+ eachSlot(child, visit);
38
+ }
39
+ const IGNORED = (token) => token.type === 'token' && token.value?.type === 'white-space';
40
+ // The slot a declaration is entirely made of, for `--brand: ${color}` directly
41
+ // on the element, which can set `--brand` itself. Under at-rules or a selector
42
+ // variant, the generated class would have to be too, so those use a variable.
43
+ function directSlot(atom) {
44
+ if (atom.atRules.length > 0 || atom.variant !== null || atom.declaration.important)
45
+ return null;
46
+ const declaration = atom.declaration.value;
47
+ // Lightning CSS also uses `custom` for properties it doesn't parse, such as
48
+ // `content` with a `var()`.
49
+ if (declaration.property !== 'custom' || !atom.property.startsWith('--'))
50
+ return null;
51
+ const tokens = declaration.value.value.filter((t) => !IGNORED(t));
52
+ const [token] = tokens;
53
+ if (tokens.length !== 1 || !isSlot(token))
54
+ return null;
55
+ return !token.value.fallback || parseSlot(token).syntax !== null ? token : null;
56
+ }
57
+ const hash = (identity) => createHash('sha256').update(JSON.stringify(identity)).digest('base64url').slice(0, 16);
58
+ const SELF = '__pre-style-self__';
59
+ // A declaration's selector, for where to set its variables. Null when it's the
60
+ // element itself, as for `&:hover` and `.dark &`.
61
+ function selectorOf(atom) {
62
+ if (atom.variant === null)
63
+ return null;
64
+ const selectors = withClass(atom.variant.selectors, SELF);
65
+ const onElement = selectors.every((selector) => {
66
+ const compound = selector.slice(selector.findLastIndex((c) => c.type === 'combinator') + 1);
67
+ return (compound.some((c) => c.type === 'class' && c.name === SELF) &&
68
+ !compound.some((c) => c.type === 'pseudo-element'));
69
+ });
70
+ if (onElement)
71
+ return null;
72
+ const rule = {
73
+ type: 'style',
74
+ value: {
75
+ selectors,
76
+ // Empty rules aren't printed.
77
+ declarations: {
78
+ declarations: [
79
+ {
80
+ property: 'custom',
81
+ value: {
82
+ name: '--z',
83
+ value: [{ type: 'token', value: { type: 'ident', value: 'z' } }],
84
+ },
85
+ },
86
+ ],
87
+ },
88
+ rules: [],
89
+ loc: { source_index: 0, line: 0, column: 1 },
90
+ },
91
+ };
92
+ const { code } = print([rule], [], {}, false);
93
+ return code
94
+ .slice(0, code.indexOf('{'))
95
+ .trim()
96
+ .replace(/\s*\n\s*/g, ' ')
97
+ .split(`.${SELF}`);
98
+ }
99
+ const atomIdentity = (atom, index) => [
100
+ atom.atRules.map((segment) => segment.key),
101
+ atom.variant?.key ?? '',
102
+ atom.property,
103
+ index,
104
+ ];
105
+ // `@property --a { syntax: "*"; inherits: false; }`. Without it, an element
106
+ // with no value of its own takes its parent's instead of its default.
107
+ const registration = (name) => {
108
+ const rule = {
109
+ type: 'property',
110
+ value: {
111
+ name,
112
+ syntax: { type: 'universal' },
113
+ inherits: false,
114
+ initialValue: null,
115
+ loc: { source_index: 0, line: 0, column: 1 },
116
+ },
117
+ };
118
+ return {
119
+ key: key(rule),
120
+ rule,
121
+ hoist: atRuleInfo('property').hoist,
122
+ overrideKey: null,
123
+ position: null,
124
+ };
125
+ };
126
+ // Units and functions that depend on where they're used. Browsers drop a
127
+ // typed `@property` whose `initial-value` has them (tested in Chromium), and
128
+ // Lightning CSS doesn't report them.
129
+ const NOT_INDEPENDENT = /(?<![\w-])[+-]?[\d.]+(?:e[+-]?\d+)?(?:em|rem|ex|rex|ch|rch|lh|rlh|cap|rcap|ic|ric|cqw|cqh|cqi|cqb|cqmin|cqmax)\b|\b(?:env|attr)\(/i;
130
+ const QUOTED = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g;
131
+ // A slot's default, as CSS.
132
+ function defaultText(node) {
133
+ const rule = {
134
+ type: 'style',
135
+ value: {
136
+ selectors: [[{ type: 'class', name: 'z' }]],
137
+ declarations: {
138
+ declarations: [
139
+ { property: 'custom', value: { name: '--z', value: node.value.fallback ?? [] } },
140
+ ],
141
+ },
142
+ rules: [],
143
+ loc: { source_index: 0, line: 0, column: 1 },
144
+ },
145
+ };
146
+ const { code } = print([rule], [], {}, false);
147
+ return /--z:\s*(.*?);?\s*\}/s.exec(code)[1];
148
+ }
149
+ // `@property --a { syntax: "<length>"; inherits: false; initial-value: 1px; }`.
150
+ function typedRegistration(name, syntax, initial, position) {
151
+ const invalid = (reason, cause) => new PreStyleError(`\`${initial}\` can't be the default of \`type(${syntax})\`: ${reason}`, position, { cause });
152
+ if (NOT_INDEPENDENT.test(initial.replace(QUOTED, '')))
153
+ throw invalid("a typed variable's default can't depend on where it's used, such as `em`, `rem`, container units, `env()`, or `attr()`, or browsers drop its `@property` rule. Use an absolute value, or leave out `type()`.");
154
+ let parsed;
155
+ try {
156
+ transform({
157
+ filename: 'type.css',
158
+ code: Buffer.from(`@property --z{syntax:${JSON.stringify(syntax)};inherits:false;initial-value:${initial}}`),
159
+ visitor: {
160
+ StyleSheetExit(sheet) {
161
+ [parsed] = sheet.rules;
162
+ },
163
+ },
164
+ });
165
+ }
166
+ catch (error) {
167
+ throw invalid(`it isn't valid for that type (${error.message}).`, error);
168
+ }
169
+ const rule = { ...parsed, value: { ...valueOf(parsed), name } };
170
+ return {
171
+ key: key(rule),
172
+ rule,
173
+ hoist: atRuleInfo('property').hoist,
174
+ overrideKey: null,
175
+ position,
176
+ };
177
+ }
178
+ const SLOT_MESSAGE = "`${…}` can only be used in a declaration's value, because it becomes a `var()`, which isn't valid here.";
179
+ // Points a slot at its variable, and registers the variable. A typed
180
+ // variable's default moves to its `initial-value`, so each type and default
181
+ // has its own variable.
182
+ function useVariable(node, identity, slot, syntax, variables, added, position, selector = null) {
183
+ const initial = syntax === null ? null : defaultText(node);
184
+ const variable = hash([identity, syntax, initial]);
185
+ const name = VARIABLE_PREFIX + variable;
186
+ variables.push({ key: variable, property: null, slot, selector });
187
+ node.value.name.ident = name;
188
+ if (syntax !== null)
189
+ node.value.fallback = null;
190
+ if (added.has(variable))
191
+ return;
192
+ added.set(variable, syntax === null ? registration(name) : typedRegistration(name, syntax, initial, position));
193
+ }
194
+ // Replaces the slots in a global rule, such as a `@keyframes`, with variables
195
+ // identified by the rule and their place in it, so the same rule in any block
196
+ // shares them.
197
+ function globalVariables(global, variables, added) {
198
+ const rule = structuredClone(global.rule);
199
+ const slots = [];
200
+ eachSlot(rule, (node) => slots.push(node));
201
+ const parsed = slots.map(parseSlot);
202
+ slots.forEach((node, index) => {
203
+ const type = parsed[index].syntax === null ? '' : `:${parsed[index].syntax}`;
204
+ node.value.name.ident = `${SLOT_PREFIX}${index}${type}`;
205
+ });
206
+ const identity = key(rule);
207
+ slots.forEach((node, index) => {
208
+ const { slot, syntax } = parsed[index];
209
+ useVariable(node, [identity, index], slot, syntax, variables, added, global.position);
210
+ });
211
+ const json = key(rule);
212
+ // Anywhere else, such as a selector Sass built, it isn't a `var()` node.
213
+ if (json.includes(SLOT_PREFIX))
214
+ throw new PreStyleError(SLOT_MESSAGE, global.position);
215
+ return { ...global, key: json, rule };
216
+ }
217
+ // Turns a parsed block's placeholder variables into its real ones. Runs after
218
+ // overridden atoms are dropped, so their slots are left out. Variables are
219
+ // identified by what they're for, so `width: ${w}px` in any block shares one
220
+ // variable and class.
221
+ export function extractVariables(atoms, globals) {
222
+ const variables = [];
223
+ const added = new Map();
224
+ const kept = globals.map((global) => global.key.includes(SLOT_PREFIX) ? globalVariables(global, variables, added) : global);
225
+ const keptAtoms = [];
226
+ for (const atom of atoms) {
227
+ const prefix = atom.atRules.map((segment) => segment.key).join() + (atom.variant?.key ?? '');
228
+ // Sass can move a placeholder, so its position is checked again here.
229
+ if (prefix.includes(SLOT_PREFIX))
230
+ throw new PreStyleError(SLOT_MESSAGE, atom.position);
231
+ if (!atom.declaration.key.includes(SLOT_PREFIX)) {
232
+ keptAtoms.push(atom);
233
+ continue;
234
+ }
235
+ const direct = directSlot(atom);
236
+ if (direct !== null) {
237
+ const { slot, syntax } = parseSlot(direct);
238
+ variables.push({
239
+ key: `direct:${atom.property}`,
240
+ property: atom.property,
241
+ slot,
242
+ selector: null,
243
+ });
244
+ // A typed custom property registers itself, under its own name.
245
+ if (syntax !== null) {
246
+ const registered = typedRegistration(atom.property, syntax, defaultText(direct), atom.position);
247
+ added.set(`direct:${atom.property}:${registered.key}`, {
248
+ ...registered,
249
+ overrideKey: overrideKey(registered.rule),
250
+ });
251
+ }
252
+ continue;
253
+ }
254
+ const value = structuredClone(atom.declaration.value);
255
+ const selector = selectorOf(atom);
256
+ let index = 0;
257
+ eachSlot(value, (node) => {
258
+ const { slot, syntax } = parseSlot(node);
259
+ const identity = atomIdentity(atom, index++);
260
+ useVariable(node, identity, slot, syntax, variables, added, atom.position, selector);
261
+ });
262
+ keptAtoms.push({
263
+ ...atom,
264
+ declaration: {
265
+ ...atom.declaration,
266
+ key: key(value) + (atom.declaration.important ? '!' : ''),
267
+ value,
268
+ },
269
+ });
270
+ }
271
+ return { atoms: keptAtoms, globals: [...kept, ...added.values()], variables };
272
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export declare function css(_strings: TemplateStringsArray): string;
2
- export declare function scss(_strings: TemplateStringsArray): string;
1
+ export type RuntimeValue = string | number | null | undefined;
2
+ export declare function css(_strings: TemplateStringsArray, ..._values: RuntimeValue[]): string;
3
+ export declare function scss(_strings: TemplateStringsArray, ..._values: RuntimeValue[]): string;
3
4
  import { createCn } from './cn.ts';
4
5
  export declare const cn: (...inputs: import("./cn.ts").ClassValue[]) => string;
5
6
  export { createCn };
6
- export type { ClassDictionary, ClassValue, CnTable } from './cn.ts';
7
+ export type { ClassDictionary, ClassValue, CnLookup, CnTable } from './cn.ts';
package/dist/index.js CHANGED
@@ -4,10 +4,10 @@
4
4
  function notTransformed() {
5
5
  throw new Error('pre.style transform not applied. css and scss blocks must be compiled by a pre.style bundler plugin, such as @pre.style/vite.');
6
6
  }
7
- export function css(_strings) {
7
+ export function css(_strings, ..._values) {
8
8
  return notTransformed();
9
9
  }
10
- export function scss(_strings) {
10
+ export function scss(_strings, ..._values) {
11
11
  return notTransformed();
12
12
  }
13
13
  import { createCn } from './cn.js';
@@ -0,0 +1,2 @@
1
+ export declare const dynamic: (classes: string, values: readonly unknown[], ...variables: import("./runtime.ts").RuntimeVariable[]) => string;
2
+ export declare const dynamicLowercase: (classes: string, values: readonly unknown[], ...variables: import("./runtime.ts").RuntimeVariable[]) => string;
@@ -0,0 +1,9 @@
1
+ import { CASE_INSENSITIVE, CASE_SENSITIVE, create } from './runtime.js';
2
+ // `./runtime.ts` with full error messages, which the bundler plugin uses
3
+ // outside production builds.
4
+ const invalidValue = (value, strings) => {
5
+ const got = typeof value === 'number' ? String(value) : typeof value;
6
+ throw new TypeError(`pre.style: \`\${…}\` values must be strings or numbers, or null or undefined to use the default. Got ${got} in \`${strings.join('${…}')}\`. For a condition, write \`\${open ? width : null}\` rather than \`\${open && width}\`.`);
7
+ };
8
+ export const dynamic = /* @__PURE__ */ create(CASE_SENSITIVE, invalidValue);
9
+ export const dynamicLowercase = /* @__PURE__ */ create(CASE_INSENSITIVE, invalidValue);
@@ -0,0 +1,16 @@
1
+ import type { CnLookup } from './cn.ts';
2
+ export type RuntimeVariable = [
3
+ name: string,
4
+ strings: readonly string[],
5
+ indexes: readonly number[],
6
+ property?: string | 0,
7
+ selector?: readonly string[]
8
+ ];
9
+ export declare const CASE_SENSITIVE: string;
10
+ export declare const CASE_INSENSITIVE: string;
11
+ export type InvalidValue = (value: unknown, strings: readonly string[]) => never;
12
+ export declare const create: (alphabet: string, invalid: InvalidValue) => (classes: string, values: readonly unknown[], ...variables: RuntimeVariable[]) => string;
13
+ export declare const dynamic: (classes: string, values: readonly unknown[], ...variables: RuntimeVariable[]) => string;
14
+ export declare const dynamicLowercase: (classes: string, values: readonly unknown[], ...variables: RuntimeVariable[]) => string;
15
+ export type CnVariables = Record<string, string>;
16
+ export declare const generatedClasses: (variables: CnVariables) => CnLookup;
@@ -0,0 +1,99 @@
1
+ const DIGITS = '0123456789';
2
+ const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
3
+ export const CASE_SENSITIVE = DIGITS + LOWERCASE + LOWERCASE.toUpperCase();
4
+ export const CASE_INSENSITIVE = DIGITS + LOWERCASE;
5
+ const HASH_LENGTH = 4;
6
+ // A hash of a value that's the same on the server and in the browser, since
7
+ // the class names it's part of have to match for hydration. FNV-1a.
8
+ function hash(value, alphabet) {
9
+ let h = 0x811c9dc5;
10
+ for (let i = 0; i < value.length; i++) {
11
+ h ^= value.charCodeAt(i);
12
+ h = Math.imul(h, 0x01000193);
13
+ }
14
+ h >>>= 0;
15
+ let result = '';
16
+ for (let i = 0; i < HASH_LENGTH; i++) {
17
+ result += alphabet[h % alphabet.length];
18
+ h = Math.floor(h / alphabet.length);
19
+ }
20
+ return result;
21
+ }
22
+ // The variable's value, or null when any of its values is null or undefined,
23
+ // so its default applies. Anything else is almost always a bug, which falling
24
+ // back would hide.
25
+ function text(strings, indexes, values, invalid) {
26
+ let result = strings[0];
27
+ let missing = false;
28
+ for (let i = 0; i < indexes.length; i++) {
29
+ const value = values[indexes[i]];
30
+ if (value === null || value === undefined)
31
+ missing = true;
32
+ else if (typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value)))
33
+ result += value + strings[i + 1];
34
+ else
35
+ invalid(value, strings);
36
+ }
37
+ return missing ? null : result;
38
+ }
39
+ let sheet;
40
+ const inserted = new Set();
41
+ // Inserts a generated class's rule once. `setProperty` parses the value alone,
42
+ // so a value can't add rules or declarations, even if it comes from a user.
43
+ function insert(selector, className, property, value) {
44
+ if (inserted.has(className))
45
+ return;
46
+ inserted.add(className);
47
+ // An unknown selector is ignored, as it is in the CSS file. Browsers without
48
+ // constructable stylesheets, or where the list can't be changed (Chrome before
49
+ // 99, Firefox before 101, Safari before 16.4), keep the defaults rather than
50
+ // failing the render.
51
+ try {
52
+ if (sheet === undefined) {
53
+ sheet = new CSSStyleSheet();
54
+ document.adoptedStyleSheets.push(sheet);
55
+ }
56
+ const index = sheet.insertRule(`${selector}{}`, sheet.cssRules.length);
57
+ sheet.cssRules[index].style.setProperty(property, value);
58
+ }
59
+ catch { }
60
+ }
61
+ export const create = (alphabet, invalid) => (classes, values, ...variables) => {
62
+ let result = classes;
63
+ for (const [name, strings, indexes, property, selector] of variables) {
64
+ const value = text(strings, indexes, values, invalid);
65
+ if (value === null)
66
+ continue;
67
+ const className = `_${name}${hash(value, alphabet)}`;
68
+ // The server returns the same class strings, without the rules, so
69
+ // hydration matches and the page uses its defaults until this runs.
70
+ if (typeof document !== 'undefined')
71
+ insert(selector ? selector.join(`.${className}`) : `.${className}`, className, property || `--${name}`, value);
72
+ result += result === '' ? className : ` ${className}`;
73
+ }
74
+ return result;
75
+ };
76
+ // Production errors are numbered, to keep the runtime small. The site's
77
+ // `/errors` pages explain them, and `./runtime-dev.ts` has the full messages.
78
+ const invalidValue = (value) => {
79
+ throw new TypeError(`pre.style error 1 (${typeof value}): https://pre.style/errors/1`);
80
+ };
81
+ // What a block with `${…}` compiles to. It takes the block's class string, its
82
+ // expressions' values, so each runs once and in order, and its variables, and
83
+ // returns the class string with a generated class for each value.
84
+ export const dynamic = /* @__PURE__ */ create(CASE_SENSITIVE, invalidValue);
85
+ // For the `quirksMode` setting, where class selectors ignore case.
86
+ export const dynamicLowercase = /* @__PURE__ */ create(CASE_INSENSITIVE, invalidValue);
87
+ // A `cn` lookup that also recognizes generated classes, which aren't in its
88
+ // table: `_`, a variable's name, and four characters. Each sets its variable on
89
+ // the element. It's here rather than in `cn`, so apps without `${…}` don't ship
90
+ // it.
91
+ export const generatedClasses = (variables) => new (class extends Map {
92
+ get(className) {
93
+ const name = className.slice(1, -4);
94
+ return (super.get(className) ??
95
+ (className[0] === '_' && Object.hasOwn(variables, name)
96
+ ? ['', variables[name] || `--${name}`]
97
+ : undefined));
98
+ }
99
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pre.style/core",
3
- "version": "3.1.0-alpha.1",
3
+ "version": "3.1.0-alpha.3",
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",
@@ -37,6 +37,16 @@
37
37
  "types": "./dist/index.d.ts",
38
38
  "default": "./dist/index.js"
39
39
  },
40
+ "./runtime": {
41
+ "pre.style-source": "./src/runtime.ts",
42
+ "types": "./dist/runtime.d.ts",
43
+ "default": "./dist/runtime.js"
44
+ },
45
+ "./runtime/dev": {
46
+ "pre.style-source": "./src/runtime-dev.ts",
47
+ "types": "./dist/runtime-dev.d.ts",
48
+ "default": "./dist/runtime-dev.js"
49
+ },
40
50
  "./compiler": {
41
51
  "pre.style-source": "./src/compiler/index.ts",
42
52
  "types": "./dist/compiler/index.d.ts",
@@ -61,6 +61,10 @@
61
61
  "prependFiles": {
62
62
  "$ref": "#/definitions/prependFiles"
63
63
  },
64
+ "customPropertyLocalization": {
65
+ "description": "Give each custom property declared in the library's blocks and stylesheet files a generated name, used only in the block or file that declares it. Names it only reads, and names from prepended files, stay global. The app's setting doesn't apply to libraries.",
66
+ "type": "boolean"
67
+ },
64
68
  "keyframeLocalization": {
65
69
  "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
70
  "type": "boolean"
@@ -88,6 +92,10 @@
88
92
  "prependFiles": {
89
93
  "$ref": "#/definitions/prependFiles"
90
94
  },
95
+ "customPropertyLocalization": {
96
+ "description": "Give each custom property declared in the app's blocks and stylesheet files a generated name, used only in the block or file that declares it. Names it only reads, and names from prepended files, stay global. Libraries use their own setting.",
97
+ "type": "boolean"
98
+ },
91
99
  "keyframeLocalization": {
92
100
  "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
101
  "type": "boolean"