@teacss/core 0.4.7 → 0.5.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 CHANGED
@@ -1,20 +1,10 @@
1
1
  # @teacss/core
2
2
 
3
- **The framework-neutral TeaCSS engine.**
3
+ The framework-neutral TeaCSS parser and generator.
4
4
 
5
- ## Purpose
6
-
7
- `@teacss/core` parses colon-syntax tokens, resolves the shared `@` condition
8
- axis, matches rules, and emits layered CSS. It owns mechanism only; presets own
9
- the vocabulary.
10
-
11
- Preset, integration, and tooling authors use this package directly. It does
12
- not discover filesystem configuration, manage build-tool module lifecycles, or
13
- provide runtime class composition; higher-level packages own those boundaries.
14
- A negated condition stays unmatched when its owning resolver cannot express a
15
- negative form; it is never silently emitted as the positive condition.
16
-
17
- ## Usage
5
+ `@teacss/core` parses colon-syntax tokens, resolves trailing conditions,
6
+ matches preset rules, and emits layered CSS. It owns mechanism; presets own
7
+ utility vocabulary.
18
8
 
19
9
  ```sh
20
10
  bun add @teacss/core @teacss/preset-standard
@@ -24,129 +14,366 @@ bun add @teacss/core @teacss/preset-standard
24
14
  import { createGenerator } from "@teacss/core";
25
15
  import { presetStandard } from "@teacss/preset-standard";
26
16
 
27
- const generator = await createGenerator({
28
- presets: [presetStandard()],
29
- });
30
-
17
+ const generator = await createGenerator({ presets: [presetStandard()] });
31
18
  const { css } = await generator.generate("p:4 bg-color:red-500@hover");
32
19
  ```
33
20
 
34
- Large direct-input builds can cap concurrent token parsing to reduce peak memory:
35
-
36
- ```ts
37
- const { css } = await generator.generate(tokens, {
38
- tokenConcurrency: 256,
39
- });
40
- ```
41
-
42
- The limit must be a positive integer. It is opt-in because custom asynchronous
43
- rules may intentionally coordinate work across tokens; omitting it preserves the
44
- existing unbounded scheduling behavior. Matched tokens and emitted CSS remain in
45
- the same deterministic order as default scheduling; matched-token iteration
46
- continues to follow input order. CSS string tie-breakers use
47
- locale-independent UTF-16 code-unit order.
48
-
49
- Most apps should install `teacss`. Use `@teacss/core` directly when building
50
- presets, generators, or tooling.
51
-
52
- Class-list integrations can use `splitClassTokens()` to split whitespace while
53
- preserving spaces inside attached `[]` literal regions. It intentionally keeps
54
- quotes, semicolons, and group syntax as ordinary token content; source
55
- extraction remains a separate, broader boundary. The default source extractor
56
- also recognizes valid unquoted HTML class values such as `class=p:4` and
57
- distributes grouped conditions in HTML values such as
58
- `class={p:4;m:2}@hover`. Braced attributes in JSX/TSX remain host expression
59
- containers rather than HTML class groups. Known JavaScript, TypeScript, JSX,
60
- TSX, and MDX file IDs stay on that host-source path, so invalid unquoted JSX
61
- attributes are not reinterpreted as HTML. File-aware grouping treats
62
- `.ts`/`.mts`/`.cts` as non-JSX host source and `.jsx`/`.tsx`/`.mdx` with JSX
63
- raw-text semantics, avoiding ambiguous angle assertions and text delimiters.
64
- Only `.tsx` applies TSX generic-arrow disambiguation; JSX, MDX, and mixed
65
- documents retain their own or conservative auto-detection semantics.
66
-
67
- Use `expandGroups()` when scanning host source, where valid TeaCSS groups may
68
- sit inside JavaScript strings or arrays. Use `expandClassGroups()` only after a
69
- class list has been isolated; it expands top-level groups while preserving
70
- group-like braces inside attached `[]` values and `()` CSS functions.
71
-
72
- Generic runtime class merging is intentionally separate from the generator:
73
-
74
- ```sh
75
- bun add @teacss/classes
76
- ```
77
-
78
- ```ts
79
- import { createMerger } from "@teacss/classes";
80
- ```
81
-
82
- ## Theme merging
83
-
84
- Plain theme objects from presets and user configuration are deep-merged. A
85
- non-plain theme, such as a class instance, is treated as one atomic value so its
86
- prototype and private state remain valid: a later non-plain theme replaces an
87
- earlier one, and an empty plain overlay leaves it unchanged. To replace it with
88
- a plain theme, use a top-level `{ $reset: true, ... }` value. To combine
89
- class-backed state with another theme, use `extendTheme` and perform the
90
- class-aware merge there. A non-empty plain overlay on a non-plain theme is
91
- rejected instead of manufacturing an invalid class instance.
92
-
93
- ## Transactional configuration
94
-
95
- Runtime integrations that reload configuration can stage work without mutating
96
- the live generator:
97
-
98
- ```ts
99
- const prepared = await generator.prepareConfig(nextConfig);
100
- const candidates = await prepared.generator.applyExtractors(source, id);
101
-
102
- if (validationPassed) generator.commitConfig(prepared);
103
- ```
104
-
105
- `prepared.generator` is isolated but shares the candidate resolved config that
106
- will be committed. `commitConfig()` returns `false` when a newer preparation has
107
- superseded the candidate or the candidate generator was reconfigured. The
108
- prepared snapshot is frozen, while its resolved config remains mutable for
109
- staged transformer work. A successful commit keeps the live generator identity
110
- and notifies every `config` observer; preparation and failed validation emit
111
- nothing. Observer failures are reported as diagnostics after the commit and do
112
- not roll the active configuration back.
113
-
114
- ## Preflight context
115
-
116
- Preset preflights receive the generator and theme through their `getCSS(context)`
117
- callback. `context.generated` additionally exposes the utilities produced by the
118
- current `generate()` call after postprocessing, including cache hits and excluding
119
- preflights. Demand-driven preflights can inspect this read-only list without
120
- sharing mutable state across generation calls.
121
-
122
- ## Parent at-rule ordering
123
-
124
- Tooling that re-serializes parsed utilities outside `generate()` can order
125
- parent blocks exactly as generation does:
21
+ Most applications should install `teacss` and a build adapter instead. Use Core
22
+ directly for presets, generators, and tooling.
23
+
24
+ Rule declaration tuples use `[property, value, operators?]`. The optional third
25
+ slot is retained for custom processors; Core does not interpret or execute it.
26
+ Only the property and value are serialized into CSS.
27
+
28
+ ## Key APIs
29
+
30
+ | API | Purpose |
31
+ | ---------------------- | ---------------------------------------------------------------- |
32
+ | `createGenerator` | Resolves presets and creates a CSS generator. |
33
+ | `splitClassTokens` | Splits class whitespace while preserving attached `[]` literals. |
34
+ | `expandGroups` | Expands TeaCSS groups while scanning host source. |
35
+ | `expandClassGroups` | Expands groups in an already isolated class list. |
36
+ | `compareParentAtRules` | Applies the generator's deterministic parent-rule order. |
37
+ | `comparableWidth` | Normalizes comparable media-query widths. |
38
+
39
+ Width comparison includes `svi`, `svb`, `lvi`, `lvb`, `dvi`, `dvb`, `rex`,
40
+ `rch`, `rcap`, and `ric`. These relative units compare within their own scale,
41
+ not against pixels or other relative units. Numeric media ordering and
42
+ `@screen at-*` range selection share this comparison.
43
+ Values that overflow JavaScript's finite numeric range, including during pixel
44
+ conversion, return `null` and do not participate in numeric width comparison.
45
+ This does not reject or rewrite the authored CSS value.
46
+ Width parsing accepts only CSS whitespace (space, tab, LF, CR, and form feed).
47
+ Unicode spaces such as NBSP remain part of the input, so `768px` followed by
48
+ NBSP is not silently treated as a valid pixel width or selected as a range bound.
49
+ Negated media queries stay outside numeric width ordering, including CSS-escaped
50
+ `not` keywords. Keyword checks use complete names, not fragments such as
51
+ `not-feature`; comments and strings do not contribute negation. Escapes are
52
+ decoded only for identity checks, without rewriting the emitted query.
53
+ Composed at-rule parents preserve escaped punctuation in their preludes.
54
+ An escaped bracket, parenthesis, or quote cannot swallow the next parent;
55
+ both ordinary generation and `constructCSS()` emit properly nested blocks.
56
+
57
+ The default extractor understands quoted and valid unquoted HTML classes. It
58
+ keeps JavaScript, TypeScript, JSX, TSX, and MDX on their host-language paths so
59
+ host expressions are not mistaken for TeaCSS groups.
60
+
61
+ Group expansion preserves CSS escapes in conditions, including escaped `;`,
62
+ commas, and brackets. They remain part of each member's condition instead of
63
+ ending a group, splitting its members, or swallowing following classes.
64
+ The default extractor also protects escaped quotes, backticks, semicolons, and
65
+ braces in condition suffixes during its final token split. Ordinary host quotes
66
+ and whitespace remain source boundaries; escaped punctuation cannot silently
67
+ turn a condition into a shorter selector.
68
+ Quoted attributes on structural conditions, such as `probe@_li[title="x"]`
69
+ and `probe@.active[title=""]`, also stay intact during scanning and group
70
+ expansion. Ordinary host arrays and decorator indexes remain extractable.
71
+ Structural selector conditions preserve NBSP-like Unicode spaces before group
72
+ expansion and attribute masking, including escaped forms and unquoted HTML
73
+ class values. Ordinary source whitespace and U+2028/U+2029 line separators
74
+ remain boundaries; named conditions do not gain Unicode-space continuation.
75
+
76
+ The compatibility helpers `parseVariantGroup()` and `expandVariantGroup()`
77
+ accept a `MagicStringLike` buffer. They check all target ranges against the
78
+ original source before writing, so a pre-existing overlapping edit or unreadable
79
+ range rejects without partially expanding earlier groups. Edits outside the
80
+ groups are preserved. This does not roll back failures from a custom `overwrite()`.
81
+ Compatibility group helpers snapshot only own separator and prefix entries,
82
+ rechecking ownership after earlier getters run. Deleted slots cannot introduce
83
+ inherited separators into matching or inherited prefixes into collapsing.
84
+
85
+ `getPath(id)` removes the first `?` and everything after it, including any line
86
+ terminators in the query. The path before it is preserved verbatim.
87
+
88
+ `applyExtractors()` returns the supplied accumulator. Passing a `CountableSet`
89
+ preserves its counting methods in the inferred return type; omitting the
90
+ accumulator or passing `undefined` creates and returns a plain `Set`.
91
+ It commits only changes made by successful extraction. Unchanged
92
+ `CountableSet` members and counts are left alone, including zero, infinite, and NaN counts
93
+ and caller changes made while asynchronous extraction is pending.
94
+ Explicit zero-count members returned or set by extractors are retained when the
95
+ combined count is zero. Deletions and concurrent decrements that exhaust a
96
+ positive source count still remove the member.
97
+
98
+ Programmatic `safelist` entries and callback results accept `{a;b}@condition`
99
+ groups, like explicit token lists. Expanded members respect the blocklist and
100
+ do not increase existing source occurrence counts. Pass `safelist: false` to
101
+ `generate()` to omit the configured safelist.
102
+
103
+ Large direct-input builds may set a positive `tokenConcurrency` limit:
126
104
 
127
105
  ```ts
128
- import { compareParentAtRules } from "@teacss/core";
129
-
130
- const ordered = [...blocks.keys()].sort((a, b) => compareParentAtRules(generator.parentOrders, a, b));
106
+ await generator.generate(tokens, { tokenConcurrency: 256 });
131
107
  ```
132
108
 
133
- An explicit `parentOrders` entry wins. Otherwise plain `@media` width queries
134
- cascade mobile-first — `min-width` ascending, `max-width` descending, all
135
- case-insensitive — and every other parent keeps lexicographic order ahead of
136
- them. `@container (min-width: ...)`, `@media not (min-width: ...)`, and a width
137
- that is not a length at all — `calc()`, `var()`, a malformed value — are not
138
- width queries, so none of them is placed by a width.
139
-
140
- Two widths compare only on a shared scale. Every unit with a fixed pixel ratio
141
- shares one — `px`, `rem`, `em` (both against the initial font size, so 16px),
142
- and the absolute `in` / `pt` / `pc` / `cm` / `mm` / `q` — so `48rem` and `768px`
143
- are one width and `96pt` cascades ahead of `640px`. A unit whose pixel value
144
- depends on the viewport or the font, such as `vw` or `ch`, gets its own scale:
145
- `9vw` orders ahead of `10vw`, but nothing here can say where either sits
146
- relative to `640px`, so the scales are kept apart rather than interleaved on a
147
- guess. `comparableWidth` is exported for a consumer that has to reduce the same
148
- widths itself.
149
-
150
- ## Status
109
+ Omitting the option preserves unbounded scheduling. Output order remains
110
+ deterministic.
111
+
112
+ `generate()` infers `matched` as a `Map` when `extendedInfo: true` is required
113
+ by the supplied options type, and as a `Set` when known to be disabled or omitted. Runtime
114
+ booleans and optional `GenerateOptions<true>` return `Set | Map`; narrow with
115
+ `matched instanceof Map` before reading extended token information.
116
+
117
+ With `outputToCssLayers: { allLayers: true }`, the native layer-order declaration
118
+ includes both configured layers (even unused ones) and layers used by rules or
119
+ preflights. Custom layers retain their sorted cascade position, including when
120
+ `getLayers()` selects only part of the result. Aliases and `null` opt-outs apply
121
+ to this declaration as well as the layer blocks.
122
+
123
+ Adapters holding externally extracted tokens may pass
124
+ `isInputCurrent: () => sourceRevision === extractedRevision`. Core captures this
125
+ optional callback once per `generate()` call and checks it repeatedly across
126
+ generation boundaries, including config retries and diagnostic publication.
127
+ It must be synchronous and side-effect-free: `false` rejects with
128
+ `Error("[@teacss/core] Generation input is no longer current.")`; callback errors
129
+ propagate, and invalid callback types or non-boolean results reject with
130
+ `TypeError`. An accidentally asynchronous check's rejection is observed; it does
131
+ not become an additional unhandled rejection. Re-extract before starting another
132
+ call. Omitting it preserves existing behavior. This is not cancellation of
133
+ running hooks or rollback of diagnostics already delivered while the input was current.
134
+
135
+ Selector merging isolates vendor-prefixed pseudo selectors, including those
136
+ following escaped backslashes or comments after the colon, so unsupported
137
+ pseudos cannot invalidate ordinary selectors. Escaped vendor-name characters
138
+ receive the same protection; escaped standard pseudo names can still merge.
139
+ Pseudo-like text inside escaped class names, comments, and attribute strings
140
+ can still merge.
141
+
142
+ Structural subject and target conditions reject empty, CSS-whitespace-only, or
143
+ comment-only attribute selectors such as `@.active[]` and `@_li[/**/]`.
144
+ Those tokens remain unmatched instead of invalidating merged rules. Empty
145
+ attribute values such as `[title=""]` remain supported.
146
+ Pseudo-element names must start CSS identifiers: `@::5`, `@::-5`, and `@::-`
147
+ remain unmatched, including on subject and target conditions. Names are not
148
+ restricted to a preset vocabulary; non-ASCII and escaped starts are supported.
149
+ Escaped legacy names such as `:bef\ore` keep their pseudo-element role and
150
+ per-token limit. Functional `::part()` / `::slotted()` chains compare decoded
151
+ names too, while emitted selectors preserve the authored spelling. Identifier
152
+ hex escapes consume at most six digits and one optional CSS whitespace terminator
153
+ (CRLF counts as one); extra top-level whitespace remains invalid.
154
+ Comments may separate a class dot or pseudo-class colon from its name, and
155
+ the two pseudo-element colons: `./**/active`, `:/**/hover`, `:/**/:before`.
156
+ They cannot split a hash token (`#/**/id`), an identifier, or the name and opening
157
+ parenthesis of a function (`:not/**/(...)`). Part states follow the same rules.
158
+ Functional chain arguments preserve non-ASCII identifiers, including NBSP.
159
+ Between chained pseudo-elements, only comments or permitted part states are
160
+ allowed; neither CSS whitespace nor non-ASCII space-like characters are erased.
161
+ This chain check also applies to resolver-authored suffixes and pseudo-elements.
162
+ Structural identifiers preserve non-ASCII characters such as NBSP; JavaScript
163
+ whitespace is not interchangeable with CSS whitespace during selector validation.
164
+ Quoted structural strings reject raw LF, CR, and form-feed characters, while
165
+ preserving CSS escapes and backslash line continuations (including CRLF).
166
+ Outside quoted strings and comments, a backslash followed by LF, CR, or form-feed
167
+ is not a valid selector escape and leaves the structural condition unmatched.
168
+
169
+ The experimental `scope` generation option accepts a selector or selector list:
170
+ `{ scope: ".app-a, .app-b" }` scopes each generated selector to both roots,
171
+ preserving each scope arm's specificity. It does not style the roots themselves.
172
+ Rule metadata `noScope` bypasses scoping; raw CSS and preflights are not scoped.
173
+ An empty string disables scoping. Empty or comment-only list arms reject
174
+ `generate()` with a `TypeError`, even when there is no generated CSS; this
175
+ guards against accidental unscoped output, not all invalid selector syntax.
176
+ Selector-list cleanup preserves escaped whitespace, hexadecimal escape
177
+ terminators, and non-ASCII identifier characters such as NBSP.
178
+ Replacing an interior `$$` scope placeholder retains a separating space,
179
+ including after escaped commas or braces. Scoped and unscoped output therefore
180
+ preserve the authored descendant relationship instead of joining compounds.
181
+ Adjacent `$$` placeholders are all replaced, even when they share whitespace.
182
+ Each inserts the same scope independently; disabling scope removes them all.
183
+
184
+ ## Configuration lifecycle
185
+
186
+ Nested preset resolution tracks both factory/promise wrappers and resolved source
187
+ objects along each ancestor path. Fresh wrappers cannot hide a cycle; sibling
188
+ branches may still reuse the same preset. `resolvePreset()` resolves one preset
189
+ without traversing its children.
190
+
191
+ Prefer `await createGenerator()`. The deprecated constructor initializes lazily:
192
+ concurrent calls share one attempt; after it fails, a later call can retry.
193
+ The failed calls still reject, and no automatic background retry is scheduled.
194
+ Initialization publishes config only after rule snapshots succeed. A snapshot
195
+ failure leaves initialization retryable; a replacement committed during snapshot
196
+ reads stays active without a duplicate config event.
197
+ An explicit replacement via `setConfig()` or `prepareConfig()` does not first
198
+ initialize the unrelated constructor config, so invalid initial configs can be
199
+ replaced without rerunning their failing hooks.
200
+ `setConfig()` without a replacement stays a no-op, even before initialization.
201
+
202
+ `prepareConfig(nextConfig)` creates an isolated candidate generator.
203
+ `commitConfig(prepared)` switches the live generator only when that candidate
204
+ is still current. A successful commit preserves generator identity and reports
205
+ observer failures as diagnostics.
206
+ If the diagnostic sink itself throws, that logging failure is ignored: the
207
+ committed configuration stays successful and async observers leave no unhandled
208
+ rejection. This does not suppress `configResolved()` failures before commit.
209
+ Commit validation rechecks candidate identity after reading mutable configuration;
210
+ if a getter reconfigures the candidate, the commit is rejected without touching
211
+ the live configuration or dispatching its observers.
212
+
213
+ `configResolved(config)` hooks may be synchronous or asynchronous. Core awaits
214
+ them sequentially in resolved preset order, then awaits the user hook, and
215
+ rebuilds rule indexes afterward. Return values are ignored; modify `config`
216
+ directly. A throw or rejection aborts resolution, so failed `setConfig()` calls
217
+ leave the live configuration unchanged. Config-event observers remain separate.
218
+
219
+ Content pipeline filters merge across presets without expanding the source list
220
+ into function arguments, so large compositions do not hit Node's argument limit.
221
+ Filter order, deduplication, and regular-expression identity are preserved.
222
+
223
+ Themes are deep-merged only while they are plain objects. A non-plain theme is
224
+ atomic; replace it, reset to a plain object with `{ $reset: true }`, or combine
225
+ it through `extendTheme`.
226
+ New plain-object branches also consume nested `$reset` markers, including when
227
+ replacing a scalar; the caller's patch is left unchanged.
228
+ `mergeDeep()` copies array-patch containers and retains only their own items,
229
+ including new fields and replacements of non-array values. Passing `true` as the
230
+ third argument concatenates only when both values are arrays.
231
+
232
+ `toArray()` treats optional `undefined` input as an empty array in its return
233
+ type, preserving array mutability and explicit `undefined` elements within arrays.
234
+ It captures array length once for both density checks and sparse compaction;
235
+ items appended by getters are not added to the current compacted result.
236
+
237
+ Shortcut grouping computes maximum rule and sort priorities incrementally,
238
+ without depending on the JavaScript engine's function-argument limit.
239
+
240
+ Preset preflights receive the generator, theme, and the current call's generated
241
+ utilities through `context.generated`.
242
+ With `preflights: false`, the emission stage skips reading the preflight list;
243
+ configuration resolution and prepared-config validation still run normally.
244
+
245
+ `isThenable()` checks objects and functions for a callable `then`, without invoking
246
+ it. Primitive values are rejected without reading their prototypes; errors from
247
+ object getters still propagate.
248
+
249
+ Rule callbacks receive `currentSelector` and `variantMatch` for their local parse
250
+ branch, including shortcut leaves; `rawSelector` remains the outer styled token.
251
+ `parseUtil()` does not rewrite its caller's matching context. Its rule and
252
+ `constructCSS()` callbacks use the same captured configuration even if an async
253
+ variant replaces the generator configuration in between.
254
+ Direct `parseUtil()` calls retry both results and failures made obsolete by
255
+ in-place edits to the current prepared config. Unchanged-config failures still
256
+ reject with their original value; contexts for replaced configs stay bound.
257
+ Rule activation checks both config identity and runtime revision, so an obsolete
258
+ async result cannot repopulate `activatedRules` after another call refreshes
259
+ prepared-config indexes.
260
+ Late branches from an obsolete parse also skip rule-detail recording, so they
261
+ cannot append retired rules to the shared `context.rules` after a retry.
262
+ Direct parsing with a retained older configuration still records its own details.
263
+ Retained contexts for an older configuration use a separate condition cache:
264
+ neither resolved nor unrecognized conditions can leak between old and live
265
+ configurations. Live configuration revisions still invalidate their cache.
266
+ Copied contexts exposed by `parseToken()`, extended-info data, and preflight
267
+ utilities retain that configuration binding when reused with Core methods.
268
+ Dynamic-rule prefix enumeration and dispatch buckets append own array entries,
269
+ so inherited numeric setters cannot discard prefixes or rules. Keyed and
270
+ fallback rule order remains identical to the ordinary scan.
271
+ Condition results, delimiter stacks, and pseudo-element chains use the same
272
+ own-entry writes, preserving both resolved and unresolved conditions.
273
+ Group expansion/collapse and unmatched-token suggestions likewise preserve
274
+ collected items and edit-distance rows through inherited numeric setters.
275
+ Unmatched-token indexing captures the static rule map once for both contents
276
+ and cache identity, ignoring absent or deleted static entries. Dynamic rule and
277
+ metadata-prefix traversal reads only own array items, so inherited entries do
278
+ not create diagnostic vocabulary or trigger matcher probes.
279
+ Event dispatch reads its event store once and snapshots only own listener slots,
280
+ rechecking each slot after earlier getters run. Subscription changes made by
281
+ listeners apply to subsequent dispatches, not the current snapshot.
282
+ Generator configuration notifications use the same single-store, own-slot
283
+ snapshot while retaining their separate observer-error isolation.
284
+ If reading that snapshot fails, the committed configuration remains successful:
285
+ the error is reported, that notification batch is skipped, and later notifications
286
+ can proceed after the listener storage is repaired.
287
+ Unsubscription uses the same own-slot snapshot before filtering, so deletions
288
+ by getters cannot retain inherited listeners. A failed snapshot leaves listener
289
+ storage unchanged and permits retrying the unsubscribe function.
290
+ `uniq()`, `uniqueBy()`, and `BetterMap.flatMap()` likewise check slot ownership
291
+ at each read, so getters and comparison callbacks cannot expose inherited values.
292
+ These traversals capture the initial array length; `toArray()` still preserves
293
+ the identity of dense arrays.
294
+ Array patches and recursive `$reset` cleanup also recheck ownership while reading,
295
+ so getter-driven deletions cannot promote inherited items into merged config.
296
+ CSS entry normalization checks ownership as entries are filtered. CSS value-list
297
+ normalization snapshots own items once before classification and conversion,
298
+ avoiding repeated top-level getters and retaining entry-tuple metadata.
299
+ `clone()` uses the same captured array length for allocation and copying, retaining
300
+ own `undefined` slots when source entries disappear during the reverse traversal.
301
+ Extractor, preprocessor, variant, and postprocessor traversal rechecks own slots
302
+ after earlier callbacks, including async waits. Extractor-returned arrays use
303
+ the same own-slot reads, so deleted entries cannot introduce inherited tokens.
304
+ Variant branch results, handler snapshots, postprocessor result arrays, and
305
+ preflight grouping also recheck ownership after earlier getters run, preserving
306
+ authored ordering without promoting inherited branches, selectors, or preflights.
307
+ Generation input arrays, safelist result arrays, sorted layer results, and
308
+ `getLayers()` include/exclude lists use the same own-slot reads, so getters cannot
309
+ expose inherited tokens or alter layer selection through deleted entries.
310
+ Condition parsing, resolver traversal, and selector assembly likewise recheck
311
+ own array entries after earlier getters or callbacks, excluding inherited
312
+ conditions and resolver hooks exposed by deletions.
313
+ `symbols.variants` snapshots only own handlers in callback inputs, callback
314
+ results, and array values. Shortcut expansion snapshots each own result slot
315
+ once before separating tokens from inline declarations.
316
+
317
+ Copied rule metadata uses own descriptor fields and prototype-free descriptors,
318
+ so ambient descriptor properties cannot replace accessors or redirect layers.
319
+ `withLayer()` rechecks metadata-slot ownership after reading its getter. A deleted
320
+ slot is recreated as an own property without invoking inherited setters; existing
321
+ readonly metadata slots still reject writes.
322
+ Metadata prefix/autocomplete arrays and copied context handler arrays also
323
+ recheck item ownership while reading, excluding inherited items exposed by getters.
324
+ Configuration shortcut parsing, preset and rule prefix copies, and rule-list
325
+ cloning use own-slot reads too, including rule-index rebuilds after config hooks.
326
+ Rule and shortcut tuple cloning validates each required slot immediately before
327
+ reading it. A matcher getter cannot expose an inherited body; already captured
328
+ matcher values remain valid if their getter removes its own source slot.
329
+ Top-level and child preset lists use own-slot flattening for both list and
330
+ group entries. `mergeConfigs()` rechecks input ownership after reading each
331
+ configuration, so earlier getters cannot introduce inherited presets or configs.
332
+ Merged configuration lists, autocomplete templates/extractors and shorthand
333
+ alternatives, content sources, and pipeline filters snapshot only own items
334
+ before flattening or joining, excluding inherited values exposed during reads.
335
+ Shorthand dictionary keys are rechecked before each read. Nested autocomplete
336
+ and CLI normalization uses captured values even if later getters delete their
337
+ source properties, preserving singleton entries when configurations are merged.
338
+
339
+ Variants and postprocessors may edit their declaration tuples in place without
340
+ rewriting source rule bodies. Core isolates those inputs, including inline
341
+ shortcut declarations and the body passed to `constructCSS`.
342
+ `normalizeVariant()` preserves the input variant's theme type for both function
343
+ and object forms; normalization does not loosen typed theme requirements.
344
+ For multi-result variants, each branch's matcher is checked against `blocklist`
345
+ before rules or shortcuts run. Allowed branches still emit CSS; an entirely
346
+ blocked result stays excluded from unmatched diagnostics.
347
+ Regular-expression matching invokes custom `exec` callbacks with the matcher
348
+ as receiver, without reading the function's `call` property. This also applies
349
+ to global, sticky, and frozen matchers, preserving their existing index handling.
350
+ Extractor `extract` and preflight `getCSS` hooks likewise ignore an own `call`
351
+ property, retain their owner as receiver, and await results or propagate errors.
352
+ Variant `match`, `body`, `selector`, and `handle` callbacks also ignore an own
353
+ `call` property while retaining their variant or original handler receiver.
354
+ Only `match` supports asynchronous results; CSS handler callbacks remain synchronous.
355
+ `sortLayers`, `outputToCssLayers.cssLayerName`, and synchronous/asynchronous
356
+ rule iterator methods follow the same receiver-preserving invocation contract.
357
+ Functional `symbols.variants` values receive an independent handler array per
358
+ rule output, so adding, removing, or reordering handlers does not change sibling
359
+ outputs. Handler objects themselves retain their identity and prototype hooks.
360
+ Handler fields, including both parent-tuple slots, are captured before applying
361
+ callbacks; callback edits cannot change the current application’s parent or order.
362
+
363
+ Shortcut prefix matching ignores ambient `Object.prototype.prefix` values.
364
+ Explicit metadata prefixes, including metadata prototype accessors in prepared
365
+ configurations, remain supported for static and dynamic shortcuts.
366
+ Direct shortcut expansion checks edits to the current prepared configuration
367
+ before lookup, including renamed matchers and mutated prefix arrays.
368
+ External lists passed to `expandConfiguredExpansions` are indexed from their
369
+ current contents on each lookup, without replacing the cached config index.
370
+ Contexts retained by extension hooks resume normal prepared-config checks after
371
+ their generation parsing finishes, including when it fails.
372
+ Missing-utility warnings inside shortcuts wait until that shortcut finishes
373
+ stringification, and are discarded if its config becomes obsolete. Failed
374
+ stringification does not consume warning deduplication for a later retry.
375
+
376
+ This package does not load filesystem configuration, manage build-tool
377
+ lifecycle, or provide runtime class composition.
151
378
 
152
379
  Pre-1.0. Public engine APIs may change before the stable release.