@tailwind-merge/next 0.0.0-dev.50b1d1e9f69ac68604be38dabea94cccdc6b070a

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,2815 @@
1
+ import fs, { statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { readFile, readdir, realpath, stat } from "node:fs/promises";
5
+ import { getDefaultConfig, validators } from "tailwind-merge";
6
+ import { createClassGroupLookup, createClassGroupUtils, createParseClassName } from "tailwind-merge/unstable-do-not-import";
7
+ import { Features, __unstable__loadDesignSystem, compile, loadModule } from "@tailwindcss/node";
8
+ import { parse } from "postcss";
9
+ import enhancedResolve from "enhanced-resolve";
10
+ import { createHash } from "node:crypto";
11
+ import { clearRequireCache } from "@tailwindcss/node/require-cache";
12
+ //#region ../configurator/src/css-statements.ts
13
+ /**
14
+ * Reads active CSS statements and block headers for build-time inspection without interpreting their contents. Comments cannot introduce directives, and quoted text and function arguments cannot end a statement or open a block. Sharing this between source scanning and root discovery keeps both integrations' view of active directives consistent.
15
+ * Unlike declaration analysis of compiled CSS, discovery must tolerate unfinished edits so generation can select the entrypoint and report its error through the normal recovery path.
16
+ */
17
+ function* cssStatements(css) {
18
+ let statement = "";
19
+ let quote = "";
20
+ let parenDepth = 0;
21
+ for (let index = 0; index < css.length; index++) {
22
+ const character = css[index];
23
+ if (character === "\\") {
24
+ statement += css.slice(index, index + 2);
25
+ index += 1;
26
+ continue;
27
+ }
28
+ if (quote) {
29
+ statement += character;
30
+ if (character === quote) quote = "";
31
+ continue;
32
+ }
33
+ if (character === "/" && css[index + 1] === "*") {
34
+ const end = css.indexOf("*/", index + 2);
35
+ index = end === -1 ? css.length : end + 1;
36
+ statement += " ";
37
+ continue;
38
+ }
39
+ if (character === "\"" || character === "'") quote = character;
40
+ else if (character === "(") parenDepth += 1;
41
+ else if (character === ")") parenDepth -= 1;
42
+ else if (parenDepth === 0 && (character === ";" || character === "{" || character === "}")) {
43
+ if (statement.trim() !== "") yield statement.trim();
44
+ statement = "";
45
+ continue;
46
+ }
47
+ statement += character;
48
+ }
49
+ if (statement.trim() !== "") yield statement.trim();
50
+ }
51
+ /**
52
+ * Splits at a separator that sits outside any parentheses, brackets, braces, and quotes (Tailwind's `segment`), so `a(b,c),d` splits into two parts at the top-level comma and quoted separators stay put.
53
+ */
54
+ function segment(input, separator) {
55
+ const parts = [];
56
+ const stack = [];
57
+ let last = 0;
58
+ for (let index = 0; index < input.length; index++) {
59
+ const character = input[index];
60
+ if (stack.length === 0 && character === separator) {
61
+ parts.push(input.slice(last, index));
62
+ last = index + 1;
63
+ continue;
64
+ }
65
+ switch (character) {
66
+ case "\\":
67
+ index += 1;
68
+ break;
69
+ case "\"":
70
+ case "'":
71
+ while (++index < input.length) if (input[index] === "\\") index += 1;
72
+ else if (input[index] === character) break;
73
+ break;
74
+ case "(":
75
+ stack.push(")");
76
+ break;
77
+ case "[":
78
+ stack.push("]");
79
+ break;
80
+ case "{":
81
+ stack.push("}");
82
+ break;
83
+ case ")":
84
+ case "]":
85
+ case "}": if (stack.length > 0 && stack[stack.length - 1] === character) stack.pop();
86
+ }
87
+ }
88
+ parts.push(input.slice(last));
89
+ return parts;
90
+ }
91
+ //#endregion
92
+ //#region ../configurator/src/declarations.ts
93
+ /** Annotates compiled CSS with the target and conditions needed for coverage checks. PostCSS owns CSS syntax (including escaped identifiers, importance, and nested value blocks); this walker only interprets style scopes. `candidate` is the class the CSS was compiled for, so its own selector can be told apart from ancestor classes a variant adds (`.in-dark\:block` inside `:where(.dark *) .in-dark\:block`); without it the first class selector is taken. */
94
+ function parseDeclarations(css, candidate) {
95
+ const entries = [];
96
+ visit(parse(css).nodes);
97
+ return entries;
98
+ function visit(nodes, frame) {
99
+ for (const node of nodes) if (node.type === "decl") {
100
+ if (frame && !frame.skip) entries.push({
101
+ context: frame.context,
102
+ conditional: frame.conditional,
103
+ scope: frame.scope,
104
+ property: node.prop,
105
+ important: node.important ?? false,
106
+ value: node.value
107
+ });
108
+ } else if (node.type === "rule" || node.type === "atrule") {
109
+ const header = node.type === "rule" ? node.selector : `@${node.name}${node.params ? ` ${node.params}` : ""}`;
110
+ visit(node.nodes ?? [], frameForHeader(header, frame, candidate));
111
+ }
112
+ }
113
+ }
114
+ function frameForHeader(header, parent, candidate) {
115
+ const parentContext = parent?.context ?? "";
116
+ const parentConditional = parent?.conditional ?? false;
117
+ const parentScope = parent?.scope ?? [];
118
+ const parentSkip = parent?.skip ?? false;
119
+ if (header.startsWith("@")) {
120
+ const isNonStyle = /^@(?:property|(?:-[\w]+-)?keyframes)(?:\s|$)/i.test(header);
121
+ return {
122
+ context: parentContext,
123
+ conditional: true,
124
+ scope: [...parentScope, header],
125
+ skip: parentSkip || isNonStyle
126
+ };
127
+ }
128
+ const { contextFragment, conditional, relativeSelector } = analyzeSelector(header, candidate);
129
+ return {
130
+ context: parentContext === "" || contextFragment === "" ? parentContext + contextFragment : `${parentContext} ${contextFragment}`,
131
+ conditional: parentConditional || conditional,
132
+ scope: [...parentScope, relativeSelector],
133
+ skip: parentSkip
134
+ };
135
+ }
136
+ /** The class selector to anchor on: the one spelling the candidate, or the first one when the candidate is unknown or absent (a selector Tailwind built without the class itself). */
137
+ function findClassAnchor(subject, candidate) {
138
+ const classSelectors = [...subject.matchAll(/\.(?:[\w-]|\\(?:[\da-f]{1,6}\s?|.))+/gi)];
139
+ if (candidate !== void 0) {
140
+ const own = classSelectors.find((match) => unescapeIdentifier(match[0].slice(1)) === candidate);
141
+ if (own) return own;
142
+ }
143
+ return classSelectors[0] ?? null;
144
+ }
145
+ /** Reverses CSS identifier escaping: `\:` → `:`, `\32 xl` → `2xl`. */
146
+ function unescapeIdentifier(identifier) {
147
+ return identifier.replace(/\\(?:([\da-f]{1,6})\s?|(.))/gi, (_, hex, char) => hex !== void 0 ? String.fromCodePoint(Number.parseInt(hex, 16)) : char);
148
+ }
149
+ /** Single-colon selectors that are pseudo-elements by CSS's legacy compatibility rule; everything else single-colon is a pseudo-class. */
150
+ const LEGACY_PSEUDO_ELEMENTS = /* @__PURE__ */ new Set([
151
+ "before",
152
+ "after",
153
+ "first-line",
154
+ "first-letter"
155
+ ]);
156
+ /**
157
+ * Determines what a selector does to the render target relative to the class's base element. The subject anchor is `&` (nested rules) or the class selector itself (top-level rules, possibly wrapped in `:where(...)`). Pseudo-elements and combinator tails after the anchor change the target; pseudo-classes and ancestor prefixes only add conditions.
158
+ */
159
+ function analyzeSelector(selector, candidate) {
160
+ let subject = segment(selector, ",")[0].trim();
161
+ const wrapper = /^:(?:where|is)\((.*)\)$/.exec(subject);
162
+ if (wrapper) subject = wrapper[1].trim();
163
+ const anchorMatch = /&/.exec(subject) ?? findClassAnchor(subject, candidate);
164
+ if (!anchorMatch) return {
165
+ contextFragment: subject.replace(/\s+/g, " "),
166
+ conditional: false,
167
+ relativeSelector: selector
168
+ };
169
+ let conditional = anchorMatch.index > 0 || segment(selector, ",").length > 1;
170
+ let contextFragment = "";
171
+ let rest = subject.slice(anchorMatch.index + anchorMatch[0].length);
172
+ while (rest !== "") {
173
+ const pseudo = /^::?([\w-]+)(\((?:[^()]|\([^()]*\))*\))?/.exec(rest);
174
+ if (pseudo) {
175
+ if (pseudo[0].startsWith("::") || LEGACY_PSEUDO_ELEMENTS.has(pseudo[1])) contextFragment += `::${pseudo[1]}`;
176
+ else conditional = true;
177
+ rest = rest.slice(pseudo[0].length);
178
+ continue;
179
+ }
180
+ const compound = /^(?:\.(?:[\w-]|\\(?:[\da-f]{1,6}\s?|.))+|\[[^\]]*\])/i.exec(rest);
181
+ if (compound) {
182
+ conditional = true;
183
+ rest = rest.slice(compound[0].length);
184
+ continue;
185
+ }
186
+ contextFragment += (contextFragment === "" ? "" : " ") + rest.trim().replace(/\s+/g, " ");
187
+ break;
188
+ }
189
+ return {
190
+ contextFragment,
191
+ conditional,
192
+ relativeSelector: selector.replace(anchorMatch[0], "&")
193
+ };
194
+ }
195
+ //#endregion
196
+ //#region ../configurator/src/resolvers.ts
197
+ /**
198
+ * Resolves stylesheet requests with Tailwind's package fields and conditions after optional bundler resolution. An alias-expanded request may still need an extension or package lookup; finish that lookup here so failures report missing targets under the alias rather than the original specifier. Tracking at this boundary records the file's CSS role, including .pcss and extensionless imports, without mistaking JavaScript dependencies for stylesheets.
199
+ * Each caller gets an uncached filesystem view so regeneration sees imports created or repaired after an earlier resolution attempt.
200
+ * Failed resolution reports attempted missing paths separately from stylesheet roles, so integrations can watch for their creation without treating resolver metadata such as package.json as CSS.
201
+ */
202
+ function createStylesheetResolver(customResolver, onStylesheet, onMissingDependency) {
203
+ return createResolver([{
204
+ extensions: [".css"],
205
+ mainFields: ["style"],
206
+ conditionNames: ["style"]
207
+ }], customResolver, onStylesheet, onMissingDependency);
208
+ }
209
+ /** Resolves JavaScript configs/plugins with Tailwind's import-then-require fallback. Report the module before loading can fail, and missing targets before rejecting, so integrations can recover from repairs and creation. An uncached filesystem also avoids retaining a missing result across the first retry. */
210
+ function createModuleResolver(customResolver, onDependency) {
211
+ const extensions = [
212
+ ".js",
213
+ ".json",
214
+ ".node",
215
+ ".ts"
216
+ ];
217
+ return createResolver([{
218
+ extensions,
219
+ conditionNames: ["node", "import"]
220
+ }, {
221
+ extensions,
222
+ conditionNames: ["node", "require"]
223
+ }], customResolver, onDependency, onDependency);
224
+ }
225
+ /**
226
+ * Retains local transitive dependencies after module execution fails. Tailwind reports its own import walk only after a successful load, leaving a broken child (or a missing grandchild) invisible to watchers. Recognize its literal-import forms without a JavaScript parser so syntax errors cannot stop the walk. Matches only add watches: they never execute code or decide how Tailwind loads a module.
227
+ * This recovery-only walk uses fresh resolution, reports missing alternatives, follows cycles once, and leaves the original loader responsible for errors. Bare package imports stay outside the local graph, as in Tailwind's dependency collector.
228
+ */
229
+ async function trackModuleDependencies(file, onDependency) {
230
+ const visited = /* @__PURE__ */ new Set();
231
+ const jsExtensions = [
232
+ ".js",
233
+ ".cjs",
234
+ ".mjs",
235
+ ".ts",
236
+ ".cts",
237
+ ".mts",
238
+ ".jsx",
239
+ ".tsx",
240
+ ".json",
241
+ ".node"
242
+ ];
243
+ const tsExtensions = [
244
+ ".ts",
245
+ ".cts",
246
+ ".mts",
247
+ ".tsx",
248
+ ".js",
249
+ ".cjs",
250
+ ".mjs",
251
+ ".jsx",
252
+ ".json",
253
+ ".node"
254
+ ];
255
+ const resolveJs = createResolver([{
256
+ extensions: jsExtensions,
257
+ conditionNames: [
258
+ "node",
259
+ "import",
260
+ "require"
261
+ ]
262
+ }], void 0, void 0, onDependency);
263
+ const resolveTs = createResolver([{
264
+ extensions: tsExtensions,
265
+ conditionNames: [
266
+ "node",
267
+ "import",
268
+ "require"
269
+ ]
270
+ }], void 0, void 0, onDependency);
271
+ await visit(file);
272
+ async function visit(file) {
273
+ if (visited.has(file)) return;
274
+ visited.add(file);
275
+ onDependency(file);
276
+ const extension = path.extname(file);
277
+ if (extension === ".json" || extension === ".node") return;
278
+ const source = await readFile(file, "utf8").catch(() => null);
279
+ if (source === null) return;
280
+ const imports = /* @__PURE__ */ new Set();
281
+ for (const pattern of MODULE_IMPORT_PATTERNS) for (const match of source.matchAll(pattern)) if (match[1].startsWith(".")) imports.add(match[1]);
282
+ const resolve = [
283
+ ".js",
284
+ ".cjs",
285
+ ".mjs"
286
+ ].includes(extension) ? resolveJs : resolveTs;
287
+ await Promise.all([...imports].map(async (id) => {
288
+ const dependency = await resolve(id, path.dirname(file)).catch(() => void 0);
289
+ if (dependency) await visit(dependency);
290
+ }));
291
+ }
292
+ }
293
+ /** Shares fresh filesystem access and dependency reporting across stylesheet and module resolution. Keep failed alternatives private until every condition set fails; a successful fallback must not turn missing package metadata into watched dependencies or stylesheet inputs. */
294
+ function createResolver(alternatives, customResolver, onResolved, onMissingDependency) {
295
+ const resolvers = alternatives.map((options) => enhancedResolve.ResolverFactory.createResolver({
296
+ ...options,
297
+ fileSystem: fs,
298
+ useSyncFileSystemCalls: true,
299
+ modules: ["node_modules", ...process.env.NODE_PATH?.split(path.delimiter).filter(Boolean) ?? []]
300
+ }));
301
+ return async (id, base) => {
302
+ const request = await customResolver?.(id, base) || id;
303
+ const missingDependencies = onMissingDependency ? /* @__PURE__ */ new Set() : void 0;
304
+ let resolutionError;
305
+ for (const resolver of resolvers) {
306
+ let file;
307
+ try {
308
+ file = await new Promise((resolve, reject) => {
309
+ resolver.resolve({}, base, request, { missingDependencies }, (error, result) => {
310
+ if (error || !result) reject(error ?? /* @__PURE__ */ new Error(`Could not resolve '${id}' from '${base}'`));
311
+ else resolve(result);
312
+ });
313
+ });
314
+ } catch (error) {
315
+ resolutionError = error;
316
+ continue;
317
+ }
318
+ onResolved?.(file);
319
+ return file;
320
+ }
321
+ for (const file of missingDependencies ?? []) onMissingDependency?.(file);
322
+ if (onMissingDependency && (request.startsWith(".") || path.isAbsolute(request))) {
323
+ const directory = await existingParentDirectory(path.resolve(base, request));
324
+ if (directory) onMissingDependency(directory);
325
+ }
326
+ throw resolutionError;
327
+ };
328
+ }
329
+ /** A missing nested directory cannot be watched yet; its closest existing ancestor observes creation of the remaining path. Only failed local requests use this recovery watch, which drops out after successful resolution. */
330
+ async function existingParentDirectory(file) {
331
+ let directory = path.dirname(file);
332
+ while (!(await stat(directory).catch(() => null))?.isDirectory()) {
333
+ const parent = path.dirname(directory);
334
+ if (parent === directory) return;
335
+ directory = parent;
336
+ }
337
+ return directory;
338
+ }
339
+ /** Recognize local static imports, re-exports, dynamic imports, and requires even in incomplete source. Do not greedily span later statements' `from` clauses; extra matches in comments or strings only broaden failure-recovery watches. */
340
+ const MODULE_IMPORT_PATTERNS = [/\b(?:import|export)\s+(?:[^'";]*?\s+from\s*)?['"]([^'"]+)['"]/g, /\b(?:require|import)\s*\(\s*['"`]([^'"`]+)['"`]/g];
341
+ //#endregion
342
+ //#region ../configurator/src/design-system.ts
343
+ /**
344
+ * Loads project and vanilla systems through the same installed compiler and CSS-resolution base. The vanilla system is the reference for diffing and classifying theme-created classes; comparisons are only meaningful with an identical compiler.
345
+ */
346
+ async function loadDesignSystems({ css, base, integration }) {
347
+ const project = await loadDesignSystem(css, base, integration);
348
+ const vanilla = await loadDesignSystem(`@import 'tailwindcss'${project.important ? " important" : ""};`, base, integration);
349
+ return {
350
+ project: memoizeClassList(project),
351
+ vanilla: memoizeClassList(vanilla)
352
+ };
353
+ }
354
+ /** The node wrapper currently drops resolver hooks when loading a design system. Use its own Tailwind engine with explicit loaders for bundler integrations, without changing globals or resolving a different compiler from the project. */
355
+ async function loadDesignSystem(css, base, integration) {
356
+ if (!integration) return __unstable__loadDesignSystem(css, { base });
357
+ const engine = await (tailwindEngine ??= loadTailwindEngine());
358
+ const resolveStylesheet = createStylesheetResolver(integration.resolveCss, integration.onDependency, integration.onDependency);
359
+ const resolveModule = createModuleResolver(integration.resolveJs, integration.onDependency);
360
+ return engine.__unstable__loadDesignSystem(css, {
361
+ base,
362
+ async loadStylesheet(id, from) {
363
+ const file = await resolveStylesheet(id, from);
364
+ return {
365
+ path: file,
366
+ base: path.dirname(file),
367
+ content: await readFile(file, "utf8")
368
+ };
369
+ },
370
+ async loadModule(id, from) {
371
+ const file = await resolveModule(id, from);
372
+ if (integration.onDependency) await trackModuleDependencies(file, integration.onDependency);
373
+ return loadModule(`./${path.relative(from, file)}`, from, integration.onDependency ?? (() => {}), async () => file);
374
+ }
375
+ });
376
+ }
377
+ let tailwindEngine;
378
+ /** Resolves and imports the compiler once per process. A failed load is not kept: the next generation retries, e.g. once an install has finished relinking node_modules, instead of every later generation in a dev server rethrowing the stale error. */
379
+ function loadTailwindEngine() {
380
+ const engine = loadModule("tailwindcss", path.dirname(fileURLToPath(import.meta.resolve("@tailwindcss/node"))), () => {}).then(({ path: file }) => import(pathToFileURL(file).href));
381
+ engine.catch(() => {
382
+ tailwindEngine = void 0;
383
+ });
384
+ return engine;
385
+ }
386
+ /**
387
+ * Caches `getClassList()` on a loaded design system. Tailwind rebuilds the list on every call, and the custom-utility, augmentation, and collision passes share it. Repeated rebuilds previously dominated generation time on themes with many custom utilities. A loaded design system never changes, so caching is safe; consumers replace the object for a new theme.
388
+ */
389
+ function memoizeClassList(designSystem) {
390
+ const original = designSystem.getClassList.bind(designSystem);
391
+ let classList;
392
+ designSystem.getClassList = () => classList ??= original();
393
+ return designSystem;
394
+ }
395
+ /**
396
+ * Compiles a class through Tailwind and returns its declarations, or null when the class produces no CSS. Callers pass unprefixed class names (the form `getClassList()` uses everywhere); the candidate is prefixed automatically when the theme defines a prefix, because Tailwind only compiles `tw:p-2`-style candidates there — without this, every declaration-based pass silently saw prefixed themes as producing no CSS. `@property` registrations and `@keyframes` bodies are skipped: composable utilities share the former without conflicting, and the latter describe animation frames, not what the class sets on an element. Values matter to the conflict oracle: two classes re-declaring the same property with identical text are idempotent together and carry their real state elsewhere (usually in custom properties).
397
+ */
398
+ function declaredDeclarations(designSystem, className) {
399
+ let cache = declarationsCache.get(designSystem);
400
+ if (!cache) {
401
+ cache = /* @__PURE__ */ new Map();
402
+ declarationsCache.set(designSystem, cache);
403
+ }
404
+ let declarations = cache.get(className);
405
+ if (declarations === void 0) {
406
+ const candidate = toCandidate(designSystem, className);
407
+ const css = designSystem.candidatesToCss([candidate])[0] ?? null;
408
+ declarations = css === null ? null : parseDeclarations(css, candidate);
409
+ cache.set(className, declarations);
410
+ }
411
+ return declarations;
412
+ }
413
+ /**
414
+ * The context-qualified property names of a class — the signature used for class-group classification, where values and conditions don't matter but render targets do: `color` on the element and `color` on `::before` are different things to set.
415
+ */
416
+ function declaredProperties(designSystem, className) {
417
+ const declarations = declaredDeclarations(designSystem, className);
418
+ return declarations === null ? null : new Set(declarations.map((entry) => qualifiedProperty(entry)));
419
+ }
420
+ /** Key joining render target and property name, e.g. `'::after border-color'`; base-context entries stay the bare property name. */
421
+ function qualifiedProperty(entry) {
422
+ return entry.context === "" ? entry.property : `${entry.context} ${entry.property}`;
423
+ }
424
+ /** Shared scaffolding must apply under the same guards on the same target; equal property/value text alone cannot prove that. Header order is kept conservatively rather than attempting CSS condition equivalence. */
425
+ function sameDeclarationScope(first, second) {
426
+ return first.context === second.context && first.scope.length === second.scope.length && first.scope.every((header, index) => header === second.scope[index]);
427
+ }
428
+ /**
429
+ * Whether each class name compiles to CSS, checked in one `candidatesToCss` batch — the batched boolean form of the prefix-aware compilation `declaredDeclarations` does per class. Use one of the two over raw `candidatesToCss` so the answer holds for prefixed themes.
430
+ */
431
+ function classesCompile(designSystem, classNames) {
432
+ const candidates = classNames.map((className) => toCandidate(designSystem, className));
433
+ return designSystem.candidatesToCss(candidates).map((compiledCss) => compiledCss !== null);
434
+ }
435
+ /** The candidate string Tailwind compiles for a class name: prefixed under a theme prefix (`tw:p-2`), the name itself otherwise. Class lists and everything downstream of them stay unprefixed, so the prefix exists only at this compilation boundary. */
436
+ function toCandidate(designSystem, className) {
437
+ const prefix = designSystem.theme.prefix;
438
+ return prefix === null ? className : `${prefix}:${className}`;
439
+ }
440
+ const declarationsCache = /* @__PURE__ */ new WeakMap();
441
+ /** Proper-subset check over property names, used to recognize classes whose declarations span multiple groups' signatures. */
442
+ function haveProperSubset(subset, superset) {
443
+ return subset.size < superset.size && [...subset].every((property) => superset.has(property));
444
+ }
445
+ /** Set equality over property names — the strict form of "these classes set the same things". */
446
+ function havePropertiesEqual(first, second) {
447
+ return first.size === second.size && [...first].every((property) => second.has(property));
448
+ }
449
+ //#endregion
450
+ //#region ../configurator/src/augment.ts
451
+ /**
452
+ * Finds new and reinterpreted project classes and determines their owning groups empirically, without a maintained namespace table.
453
+ *
454
+ * Mechanism: diff the project's class list against the vanilla one. Every new class is matched against candidate groups derived from its vanilla siblings (classes sharing the first name segment, e.g. `text-…`), where each candidate group is represented by the declared-property signature of one exemplar class. A unique signature match assigns the group — `text-primary` declares `color` like `text-red-500` does, not `font-size` like `text-xl` — which handles Tailwind's undocumented compat sub-namespaces (`--text-color-*`, `--background-color-*`) and namespaces tailwind-merge has no theme key for (`--z-index-*`, `--border-width-*`) with one rule. Ambiguous or unmatched classes are reported, never guessed.
455
+ * Existing names are also checked when their group claim or membership in Tailwind's grouped suggestions changes; the deduplicated class list alone cannot reveal an added interpretation.
456
+ */
457
+ function buildAugmentations({ project, vanilla, projectClassGroupId, vanillaClassGroupId, groupPrefixKeys, customGroupIds, preservedCustomClasses }) {
458
+ const vanillaClassNames = vanilla.getClassList().map(([className]) => className);
459
+ const vanillaClassNameSet = new Set(vanillaClassNames);
460
+ const projectClassNames = project.getClassList().map(([className]) => className);
461
+ const newClassNames = projectClassNames.filter((className) => !vanillaClassNameSet.has(className));
462
+ const projectClassNameSet = new Set(projectClassNames);
463
+ const exemplarsByFirstSegment = collectExemplars(vanillaClassNames, vanillaClassGroupId);
464
+ const changedSuggestionNames = changedUtilitySuggestions(project, vanilla);
465
+ const assignments = /* @__PURE__ */ new Map();
466
+ const collisions = [];
467
+ const unassigned = [];
468
+ const handledNames = /* @__PURE__ */ new Set();
469
+ for (const className of newClassNames) {
470
+ const registrationName = className.startsWith("-") ? className.slice(1) : className;
471
+ if (handledNames.has(registrationName)) continue;
472
+ if (preservedCustomClasses.has(registrationName)) {
473
+ handledNames.add(registrationName);
474
+ unassigned.push({
475
+ className: registrationName,
476
+ reason: "Custom utility effects cannot be distinguished safely by runtime lookup"
477
+ });
478
+ continue;
479
+ }
480
+ const claimingGroupId = projectClassGroupId(registrationName);
481
+ if (claimingGroupId !== void 0 && customGroupIds.has(claimingGroupId)) continue;
482
+ const properties = declaredProperties(project, className);
483
+ if (properties === null || properties.size === 0) continue;
484
+ handledNames.add(registrationName);
485
+ const targetGroupId = classifyByProperties(registrationName, properties, exemplarsByFirstSegment, vanilla);
486
+ if (typeof targetGroupId !== "string") {
487
+ if (targetGroupId.isJanus && claimingGroupId !== void 0) {
488
+ collisions.push({
489
+ className: registrationName,
490
+ claimingGroupId,
491
+ ownerGroupId: vanillaClassGroupId(registrationName) ?? claimingGroupId,
492
+ resolution: "neutralize"
493
+ });
494
+ continue;
495
+ }
496
+ if (claimingGroupId === void 0) unassigned.push({
497
+ className: registrationName,
498
+ reason: targetGroupId.reason
499
+ });
500
+ continue;
501
+ }
502
+ if (claimingGroupId === targetGroupId) continue;
503
+ if (claimingGroupId !== void 0) collisions.push({
504
+ className: registrationName,
505
+ claimingGroupId,
506
+ ownerGroupId: targetGroupId,
507
+ resolution: "restore"
508
+ });
509
+ const groupClassNames = assignments.get(targetGroupId);
510
+ if (groupClassNames) groupClassNames.push(registrationName);
511
+ else assignments.set(targetGroupId, [registrationName]);
512
+ for (const aliasName of aliasSpellings(registrationName, targetGroupId, groupPrefixKeys)) {
513
+ if (handledNames.has(aliasName) || projectClassGroupId(aliasName) === targetGroupId) continue;
514
+ const aliasDeclarations = declaredDeclarations(project, aliasName);
515
+ const classDeclarations = declaredDeclarations(project, registrationName);
516
+ if (aliasDeclarations !== null && classDeclarations !== null && declarationsEqual(aliasDeclarations, classDeclarations)) {
517
+ handledNames.add(aliasName);
518
+ assignments.get(targetGroupId).push(aliasName);
519
+ }
520
+ }
521
+ }
522
+ for (const className of vanillaClassNames) {
523
+ const registrationName = className.startsWith("-") ? className.slice(1) : className;
524
+ const projectGroupId = projectClassGroupId(className);
525
+ const vanillaGroupId = vanillaClassGroupId(className);
526
+ if (projectGroupId === vanillaGroupId && !changedSuggestionNames.has(registrationName) || projectGroupId === void 0 && !projectClassNameSet.has(className) || vanillaGroupId === void 0 || projectGroupId !== void 0 && customGroupIds.has(projectGroupId)) continue;
527
+ if (handledNames.has(registrationName)) continue;
528
+ const properties = declaredProperties(project, className);
529
+ if (properties === null || properties.size === 0) continue;
530
+ handledNames.add(registrationName);
531
+ if (projectGroupId === void 0) {
532
+ const targetGroupId = classifyByProperties(registrationName, properties, exemplarsByFirstSegment, vanilla);
533
+ if (typeof targetGroupId === "string") {
534
+ const groupClassNames = assignments.get(targetGroupId) ?? [];
535
+ groupClassNames.push(registrationName);
536
+ assignments.set(targetGroupId, groupClassNames);
537
+ } else if (!targetGroupId.isJanus) unassigned.push({
538
+ className: registrationName,
539
+ reason: targetGroupId.reason
540
+ });
541
+ continue;
542
+ }
543
+ const vanillaProperties = declaredProperties(vanilla, className);
544
+ if (vanillaProperties !== null && havePropertiesEqual(properties, vanillaProperties)) {
545
+ if (projectGroupId !== vanillaGroupId) collisions.push({
546
+ className: registrationName,
547
+ claimingGroupId: projectGroupId,
548
+ ownerGroupId: vanillaGroupId,
549
+ resolution: "restore"
550
+ });
551
+ continue;
552
+ }
553
+ const claimingSignature = exemplarsByFirstSegment.get(firstNameSegment(className))?.get(projectGroupId);
554
+ const claimingProperties = claimingSignature === void 0 ? null : declaredProperties(vanilla, claimingSignature);
555
+ if (vanillaProperties !== null && claimingProperties !== null && haveProperSubset(vanillaProperties, properties) && [...claimingProperties].every((property) => properties.has(property))) {
556
+ collisions.push({
557
+ className: registrationName,
558
+ claimingGroupId: projectGroupId,
559
+ ownerGroupId: vanillaGroupId,
560
+ resolution: "neutralize"
561
+ });
562
+ continue;
563
+ }
564
+ const targetGroupId = classifyByProperties(className, properties, exemplarsByFirstSegment, vanilla);
565
+ if (targetGroupId === projectGroupId) continue;
566
+ if (typeof targetGroupId !== "string" && targetGroupId.isJanus) collisions.push({
567
+ className: registrationName,
568
+ claimingGroupId: projectGroupId,
569
+ ownerGroupId: vanillaGroupId,
570
+ resolution: "neutralize"
571
+ });
572
+ else if (typeof targetGroupId === "string") {
573
+ if (targetGroupId !== vanillaGroupId) {
574
+ const groupClassNames = assignments.get(targetGroupId) ?? [];
575
+ groupClassNames.push(registrationName);
576
+ assignments.set(targetGroupId, groupClassNames);
577
+ }
578
+ collisions.push({
579
+ className: registrationName,
580
+ claimingGroupId: projectGroupId,
581
+ ownerGroupId: targetGroupId,
582
+ resolution: "restore"
583
+ });
584
+ } else unassigned.push({
585
+ className: registrationName,
586
+ reason: `classification changed but ${targetGroupId.reason}`
587
+ });
588
+ }
589
+ return {
590
+ assignments,
591
+ collisions,
592
+ unassigned
593
+ };
594
+ }
595
+ /**
596
+ * Finds names added to or removed from each Tailwind suggestion branch, before getClassList merges duplicate names. A name can move between branches or gain a second interpretation while remaining in the final class list. Branches are compared by position within the same compiler's utility definition; a reordered branch only causes extra declaration checks. Negative forms share their positive registration path.
597
+ */
598
+ function changedUtilitySuggestions(project, vanilla) {
599
+ const changed = /* @__PURE__ */ new Set();
600
+ for (const root of vanilla.utilities.keys("functional")) {
601
+ const before = vanilla.utilities.getCompletions(root);
602
+ const after = project.utilities.getCompletions(root);
603
+ for (let index = 0; index < Math.max(before.length, after.length); index++) {
604
+ const previousValues = new Set(before[index]?.values ?? []);
605
+ const nextValues = new Set(after[index]?.values ?? []);
606
+ for (const value of /* @__PURE__ */ new Set([...previousValues, ...nextValues])) if (previousValues.has(value) !== nextValues.has(value)) changed.add(value === null ? root : `${root}-${value}`);
607
+ }
608
+ }
609
+ return changed;
610
+ }
611
+ /**
612
+ * One exemplar class per (first name segment, class group) pair, e.g. `text` → text-color: `text-red-500`. Only exemplars for segments that actually need classification get compiled later, so collecting names here is cheap.
613
+ */
614
+ function collectExemplars(vanillaClassNames, vanillaClassGroupId) {
615
+ const exemplarsByFirstSegment = /* @__PURE__ */ new Map();
616
+ for (const className of vanillaClassNames) {
617
+ const firstSegment = firstNameSegment(className);
618
+ let groupExemplars = exemplarsByFirstSegment.get(firstSegment);
619
+ if (groupExemplars === void 0) {
620
+ groupExemplars = /* @__PURE__ */ new Map();
621
+ exemplarsByFirstSegment.set(firstSegment, groupExemplars);
622
+ }
623
+ const classGroupId = vanillaClassGroupId(className);
624
+ if (classGroupId !== void 0 && !groupExemplars.has(classGroupId)) groupExemplars.set(classGroupId, className);
625
+ }
626
+ return exemplarsByFirstSegment;
627
+ }
628
+ function classifyByProperties(className, properties, exemplarsByFirstSegment, vanilla) {
629
+ const groupExemplars = exemplarsByFirstSegment.get(firstNameSegment(className));
630
+ if (!groupExemplars || groupExemplars.size === 0) return {
631
+ reason: "no vanilla classes share its root",
632
+ isJanus: false
633
+ };
634
+ const matches = [];
635
+ let containedSignatures = 0;
636
+ for (const [classGroupId, exemplarClassName] of groupExemplars) {
637
+ const exemplarProperties = declaredProperties(vanilla, exemplarClassName);
638
+ if (exemplarProperties === null) continue;
639
+ if (havePropertiesEqual(properties, exemplarProperties)) matches.push(classGroupId);
640
+ else if (haveProperSubset(exemplarProperties, properties)) containedSignatures += 1;
641
+ }
642
+ if (matches.length === 1) return matches[0];
643
+ if (matches.length === 0 && containedSignatures >= 2) return {
644
+ reason: "resolves as multiple utilities at once",
645
+ isJanus: true
646
+ };
647
+ return {
648
+ reason: matches.length === 0 ? "no candidate group declares the same CSS properties" : `ambiguous between class groups ${matches.join(", ")}`,
649
+ isJanus: false
650
+ };
651
+ }
652
+ function firstNameSegment(className) {
653
+ const separatorIndex = className.indexOf("-");
654
+ return separatorIndex === -1 ? className : className.slice(0, separatorIndex);
655
+ }
656
+ /**
657
+ * The same value under the target group's other class-name prefixes: `inset-s-sm` assigned to the `start` group (prefixes `inset-s` and `start`) yields the candidate `start-sm`. The longest matching prefix decides where the value part begins, so a prefix that happens to prefix another never mis-splits the name.
658
+ */
659
+ function aliasSpellings(className, groupId, groupPrefixKeys) {
660
+ const prefixes = groupPrefixKeys.get(groupId);
661
+ if (!prefixes || prefixes.length < 2) return [];
662
+ const matchedPrefix = prefixes.filter((prefix) => className.startsWith(`${prefix}-`)).sort((first, second) => second.length - first.length)[0];
663
+ if (matchedPrefix === void 0) return [];
664
+ const value = className.slice(matchedPrefix.length + 1);
665
+ return prefixes.filter((prefix) => prefix !== matchedPrefix).map((prefix) => `${prefix}-${value}`);
666
+ }
667
+ /** Exact equality of two compiled declaration lists — the bar for treating two spellings as the same utility. */
668
+ function declarationsEqual(first, second) {
669
+ return first.length === second.length && first.every((entry, index) => {
670
+ const other = second[index];
671
+ return sameDeclarationScope(entry, other) && entry.property === other.property && entry.important === other.important && entry.value === other.value;
672
+ });
673
+ }
674
+ //#endregion
675
+ //#region ../configurator/src/compress.ts
676
+ /**
677
+ * Encodes a theme scale's value names into the smallest class-group representation the mode allows.
678
+ *
679
+ * Policy: the encoding must never fail to match a name that exists in the theme; whether it may overmatch names that don't exist is what `EncodingMode` decides. Candidates are compared by estimated emitted size:
680
+ * - plain enumeration of all names
681
+ * - 'compact' only: a validator covering all names, plus enumerated outliers (e.g. t-shirt sizes with a `base` outlier)
682
+ * - families with a shared first segment collapsed into a nested entry (e.g. `{ red: [isNumber] }` for `red-50` … `red-950` — in 'exact' mode the tails enumerate instead, staying exact because only listed prefix+tail combinations match)
683
+ *
684
+ * The family encoding also mirrors Tailwind's own resolution order better than a flat validator: a named part match wins over sibling validators in the class map, exactly like Tailwind prefers a color namespace hit over a bare value interpretation.
685
+ */
686
+ function encodeScale(names, encoding) {
687
+ if (names.length === 0) return {
688
+ items: [],
689
+ strategy: "empty"
690
+ };
691
+ const candidates = [
692
+ ...encoding === "compact" ? encodeWithValidators(names) : [],
693
+ encodeAsFamilies(names, encoding),
694
+ {
695
+ items: names.map(literal),
696
+ strategy: "enumerated"
697
+ }
698
+ ].filter((candidate) => candidate !== null);
699
+ let best = candidates[0];
700
+ let bestCost = estimateCost(best.items);
701
+ for (let index = 1; index < candidates.length; index++) {
702
+ const candidate = candidates[index];
703
+ const cost = estimateCost(candidate.items);
704
+ if (cost < bestCost) {
705
+ best = candidate;
706
+ bestCost = cost;
707
+ }
708
+ }
709
+ return best;
710
+ }
711
+ /**
712
+ * Rough size in characters of the emitted representation. Only used to compare encodings of the same scale against each other, so precision doesn't matter as long as it is monotonic with real output size.
713
+ */
714
+ function estimateCost(items) {
715
+ let cost = 0;
716
+ for (const item of items) if (item.kind === "class") cost += item.value.length + 4;
717
+ else if (item.kind === "validator") cost += item.name.length + 4;
718
+ else {
719
+ cost += 4;
720
+ for (const [key, entryItems] of item.entries) cost += key.length + 4 + estimateCost(entryItems);
721
+ }
722
+ return cost;
723
+ }
724
+ /**
725
+ * Validators that are worth trying as whole-scale replacements. Ordered from most to least specific so that on equal cost the least overmatching candidate wins via first-strictly-smaller comparison in `encodeScale`.
726
+ */
727
+ const VALIDATOR_CANDIDATES = [
728
+ ["isTshirtSize", validators.isTshirtSize],
729
+ ["isFraction", validators.isFraction],
730
+ ["isNumber", validators.isNumber]
731
+ ];
732
+ function encodeWithValidators(names) {
733
+ return VALIDATOR_CANDIDATES.flatMap(([name, validator]) => {
734
+ const outliers = names.filter((value) => !validator(value));
735
+ const coveredCount = names.length - outliers.length;
736
+ if (coveredCount < 3 || outliers.length > coveredCount) return [];
737
+ return [{
738
+ items: [...outliers.map(literal), {
739
+ kind: "validator",
740
+ name
741
+ }],
742
+ strategy: outliers.length === 0 ? `validator:${name}` : `mixed:${name}`
743
+ }];
744
+ });
745
+ }
746
+ /**
747
+ * Collapses names sharing a first segment into one nested entry per family, with the tails themselves encoded recursively through `encodeScale` — so numeric shades still end in a validator (`{ red: [isNumber] }`), word tails enumerate without repeating the prefix (`{ gap: ['narrow', 'wide'] }`), and multi-segment names factor further (`{ background: [{ alternative: [isNumber] }] }`). Each family keeps the factored form only when it estimates smaller than enumerating its members, so short families don't pay the object overhead. Structural factoring measures smaller on minified AND compressed output (unlike reference-based sharing, it removes repetition without adding entropy — see EmitOptions.sharing for that contrast).
748
+ */
749
+ function encodeAsFamilies(names, encoding) {
750
+ const families = /* @__PURE__ */ new Map();
751
+ for (const name of names) {
752
+ const separatorIndex = name.indexOf("-");
753
+ if (separatorIndex > 0) {
754
+ const familyName = name.slice(0, separatorIndex);
755
+ const tail = name.slice(separatorIndex + 1);
756
+ let family = families.get(familyName);
757
+ if (!family) {
758
+ family = {
759
+ tails: [],
760
+ enumeratedCost: 0
761
+ };
762
+ families.set(familyName, family);
763
+ }
764
+ family.tails.push(tail);
765
+ family.enumeratedCost += name.length + 4;
766
+ }
767
+ }
768
+ const factoredFamilies = /* @__PURE__ */ new Map();
769
+ for (const [familyName, { tails, enumeratedCost }] of families) {
770
+ if (tails.length < 2) continue;
771
+ const tailEncoding = encodeScale(tails, encoding);
772
+ if (familyName.length + 4 + estimateCost(tailEncoding.items) < enumeratedCost) factoredFamilies.set(familyName, tailEncoding.items);
773
+ }
774
+ if (factoredFamilies.size === 0) return null;
775
+ const items = [];
776
+ const familiesObject = {
777
+ kind: "object",
778
+ entries: []
779
+ };
780
+ const emittedFamilies = /* @__PURE__ */ new Set();
781
+ for (const name of names) {
782
+ const separatorIndex = name.indexOf("-");
783
+ const familyName = separatorIndex > 0 ? name.slice(0, separatorIndex) : null;
784
+ if (familyName === null || !factoredFamilies.has(familyName)) {
785
+ items.push(literal(name));
786
+ continue;
787
+ }
788
+ if (!emittedFamilies.has(familyName)) {
789
+ emittedFamilies.add(familyName);
790
+ if (familiesObject.entries.length === 0) items.push(familiesObject);
791
+ familiesObject.entries.push([familyName, factoredFamilies.get(familyName)]);
792
+ }
793
+ }
794
+ return {
795
+ items,
796
+ strategy: "families"
797
+ };
798
+ }
799
+ function literal(value) {
800
+ return {
801
+ kind: "class",
802
+ value
803
+ };
804
+ }
805
+ //#endregion
806
+ //#region ../configurator/src/property-coverage.ts
807
+ /**
808
+ * Whether setting one property fully controls another, using only known CSS shorthand relationships. Name resemblance is insufficient: `font` excludes `font-synthesis`, and `border-width` excludes `border-image-width`. Unknown relationships preserve classes conservatively.
809
+ * Logical axis coverage follows the default config's horizontal-tb policy (`padding-inline` covers left/right). Single logical sides remain unrelated to physical sides because their mapping also depends on text direction.
810
+ */
811
+ function propertyCovers(property, target) {
812
+ if (property === target) return true;
813
+ const longhands = SHORTHAND_LONGHANDS.get(property);
814
+ if (!longhands) return false;
815
+ const targetLonghands = SHORTHAND_LONGHANDS.get(target);
816
+ return targetLonghands ? [...targetLonghands].every((longhand) => longhands.has(longhand)) : longhands.has(target);
817
+ }
818
+ /** Expands the known shorthand graph once, including reset-only longhands. This also handles overlapping shorthands, such as border-inline covering border-left under the horizontal-tb policy, without guessing from property names. */
819
+ function buildShorthandLonghands() {
820
+ const shorthands = new Map(Object.entries({
821
+ animation: [
822
+ "animation-name",
823
+ "animation-duration",
824
+ "animation-timing-function",
825
+ "animation-delay",
826
+ "animation-iteration-count",
827
+ "animation-direction",
828
+ "animation-fill-mode",
829
+ "animation-play-state"
830
+ ],
831
+ background: [
832
+ "background-image",
833
+ "background-position",
834
+ "background-size",
835
+ "background-repeat",
836
+ "background-origin",
837
+ "background-clip",
838
+ "background-attachment",
839
+ "background-color"
840
+ ],
841
+ "background-position": ["background-position-x", "background-position-y"],
842
+ border: [
843
+ "border-width",
844
+ "border-style",
845
+ "border-color",
846
+ "border-image"
847
+ ],
848
+ "border-image": [
849
+ "border-image-source",
850
+ "border-image-slice",
851
+ "border-image-width",
852
+ "border-image-outset",
853
+ "border-image-repeat"
854
+ ],
855
+ "border-radius": [
856
+ "border-top-left-radius",
857
+ "border-top-right-radius",
858
+ "border-bottom-left-radius",
859
+ "border-bottom-right-radius",
860
+ "border-start-start-radius",
861
+ "border-start-end-radius",
862
+ "border-end-start-radius",
863
+ "border-end-end-radius"
864
+ ],
865
+ "column-rule": [
866
+ "column-rule-width",
867
+ "column-rule-style",
868
+ "column-rule-color"
869
+ ],
870
+ columns: ["column-width", "column-count"],
871
+ container: ["container-name", "container-type"],
872
+ flex: [
873
+ "flex-grow",
874
+ "flex-shrink",
875
+ "flex-basis"
876
+ ],
877
+ "flex-flow": ["flex-direction", "flex-wrap"],
878
+ font: [
879
+ "font-family",
880
+ "font-size",
881
+ "font-width",
882
+ "font-stretch",
883
+ "font-style",
884
+ "font-weight",
885
+ "line-height",
886
+ "font-variant",
887
+ "font-feature-settings",
888
+ "font-kerning",
889
+ "font-language-override",
890
+ "font-optical-sizing",
891
+ "font-size-adjust",
892
+ "font-variation-settings"
893
+ ],
894
+ "font-variant": [
895
+ "font-variant-alternates",
896
+ "font-variant-caps",
897
+ "font-variant-east-asian",
898
+ "font-variant-emoji",
899
+ "font-variant-ligatures",
900
+ "font-variant-numeric",
901
+ "font-variant-position"
902
+ ],
903
+ "font-synthesis": [
904
+ "font-synthesis-weight",
905
+ "font-synthesis-style",
906
+ "font-synthesis-small-caps",
907
+ "font-synthesis-position"
908
+ ],
909
+ gap: ["row-gap", "column-gap"],
910
+ grid: [
911
+ "grid-template",
912
+ "grid-auto-flow",
913
+ "grid-auto-rows",
914
+ "grid-auto-columns"
915
+ ],
916
+ "grid-template": [
917
+ "grid-template-rows",
918
+ "grid-template-columns",
919
+ "grid-template-areas"
920
+ ],
921
+ "grid-area": ["grid-row", "grid-column"],
922
+ "grid-row": ["grid-row-start", "grid-row-end"],
923
+ "grid-column": ["grid-column-start", "grid-column-end"],
924
+ "list-style": [
925
+ "list-style-image",
926
+ "list-style-position",
927
+ "list-style-type"
928
+ ],
929
+ mask: [
930
+ "mask-image",
931
+ "mask-mode",
932
+ "mask-position",
933
+ "mask-size",
934
+ "mask-repeat",
935
+ "mask-origin",
936
+ "mask-clip",
937
+ "mask-composite"
938
+ ],
939
+ "mask-border": [
940
+ "mask-border-source",
941
+ "mask-border-slice",
942
+ "mask-border-width",
943
+ "mask-border-outset",
944
+ "mask-border-repeat",
945
+ "mask-border-mode"
946
+ ],
947
+ offset: [
948
+ "offset-position",
949
+ "offset-path",
950
+ "offset-distance",
951
+ "offset-rotate",
952
+ "offset-anchor"
953
+ ],
954
+ outline: [
955
+ "outline-width",
956
+ "outline-style",
957
+ "outline-color"
958
+ ],
959
+ overflow: ["overflow-x", "overflow-y"],
960
+ "overscroll-behavior": ["overscroll-behavior-x", "overscroll-behavior-y"],
961
+ "place-content": ["align-content", "justify-content"],
962
+ "place-items": ["align-items", "justify-items"],
963
+ "place-self": ["align-self", "justify-self"],
964
+ "text-decoration": [
965
+ "text-decoration-line",
966
+ "text-decoration-style",
967
+ "text-decoration-color",
968
+ "text-decoration-thickness"
969
+ ],
970
+ "text-emphasis": ["text-emphasis-style", "text-emphasis-color"],
971
+ transition: [
972
+ "transition-property",
973
+ "transition-duration",
974
+ "transition-timing-function",
975
+ "transition-delay",
976
+ "transition-behavior"
977
+ ],
978
+ "-webkit-text-stroke": ["-webkit-text-stroke-width", "-webkit-text-stroke-color"]
979
+ }));
980
+ for (const name of [
981
+ "margin",
982
+ "padding",
983
+ "scroll-margin",
984
+ "scroll-padding"
985
+ ]) addBoxShorthand(shorthands, name, (side) => `${name}-${side}`);
986
+ addBoxShorthand(shorthands, "inset", (side) => side.startsWith("inline") || side.startsWith("block") ? `inset-${side}` : side);
987
+ const borderComponents = [
988
+ "width",
989
+ "style",
990
+ "color"
991
+ ];
992
+ for (const component of borderComponents) addBoxShorthand(shorthands, `border-${component}`, (side) => `border-${side}-${component}`);
993
+ for (const side of [
994
+ "top",
995
+ "right",
996
+ "bottom",
997
+ "left",
998
+ "inline",
999
+ "block",
1000
+ "inline-start",
1001
+ "inline-end",
1002
+ "block-start",
1003
+ "block-end"
1004
+ ]) shorthands.set(`border-${side}`, borderComponents.map((part) => `border-${side}-${part}`));
1005
+ const expanded = /* @__PURE__ */ new Map();
1006
+ const expand = (property) => {
1007
+ const cached = expanded.get(property);
1008
+ if (cached) return cached;
1009
+ const longhands = /* @__PURE__ */ new Set();
1010
+ for (const child of shorthands.get(property)) for (const longhand of shorthands.has(child) ? expand(child) : [child]) longhands.add(longhand);
1011
+ expanded.set(property, longhands);
1012
+ return longhands;
1013
+ };
1014
+ for (const property of shorthands.keys()) expand(property);
1015
+ return expanded;
1016
+ }
1017
+ /** Registers a known four-sided shorthand and its logical axes, retaining the library's horizontal-tb axis policy without equating individual logical and physical sides. */
1018
+ function addBoxShorthand(shorthands, name, sideProperty) {
1019
+ shorthands.set(name, [
1020
+ "top",
1021
+ "right",
1022
+ "bottom",
1023
+ "left",
1024
+ "inline",
1025
+ "block"
1026
+ ].map(sideProperty));
1027
+ for (const [axis, sides] of [["inline", ["left", "right"]], ["block", ["top", "bottom"]]]) shorthands.set(sideProperty(axis), [
1028
+ `${axis}-start`,
1029
+ `${axis}-end`,
1030
+ ...sides
1031
+ ].map(sideProperty));
1032
+ }
1033
+ const SHORTHAND_LONGHANDS = buildShorthandLonghands();
1034
+ //#endregion
1035
+ //#region ../configurator/src/custom-utilities.ts
1036
+ /**
1037
+ * Plans tailwind-merge support for utilities the project registers beyond the built-ins — both `@utility` definitions in CSS and utilities added by `@plugin` JS plugins land in the same registry, so one diff covers both.
1038
+ *
1039
+ * Support is empirical, derived entirely from each utility's compiled declarations, in three tiers:
1040
+ *
1041
+ * 1. A static utility whose declarations match exactly one built-in group's signature and mutually cover its effects is an alias of that group (an unconditional `color` utility behaves like `text-red-500`), so it joins the group and merges with its classes in both directions.
1042
+ * 2. Other utilities receive self-conflict groups, splitting functional values when their compiled effects differ. When a group's declarations fully cover what another group sets (`btn` with `padding` + `border-radius` covers everything `p-4` sets; see `fullyCovers` for the exact rule), an override edge is added so the utility coming later removes the covered class. The reverse direction stays out on purpose: `p-4` after `btn` only overrides part of `btn`, and removing `btn` would lose the rest of its effect — the same partial-override rule the default config applies between `px` and `p`.
1043
+ * 3. Declarations that are conditional (media queries, dark-mode guards) or target other elements (pseudo-elements, child selectors) only count as covered when they are byte-identical shared scaffolding: they overlap only sometimes or somewhere else, so a utility that merely touches them stays side by side with whatever it partially overlaps.
1044
+ *
1045
+ * Functional roots whose slash modifiers change effects without a matching full-class group remain unclassified: the runtime's fallback from an unknown full class to its base group would otherwise discard those effects.
1046
+ *
1047
+ * Roots that already exist as built-ins are skipped entirely: shadowing changes built-in behavior in ways a registry diff cannot judge. Such roots are reported so the gap is visible instead of silent.
1048
+ */
1049
+ function buildCustomUtilityPlan({ project, vanilla, vanillaClassGroupId, encoding }) {
1050
+ const vanillaRoots = /* @__PURE__ */ new Set([...vanilla.utilities.keys("static"), ...vanilla.utilities.keys("functional")]);
1051
+ const functionalRoots = project.utilities.keys("functional").filter((root) => !vanillaRoots.has(root));
1052
+ const extendedBuiltInRoots = project.utilities.keys("functional").filter((root) => vanillaRoots.has(root) && project.utilities.getCompletions(root).length > vanilla.utilities.getCompletions(root).length);
1053
+ const staticRoots = project.utilities.keys("static").filter((root) => !vanillaRoots.has(root));
1054
+ const staticRootSet = new Set(staticRoots);
1055
+ const lookupRoots = [...project.utilities.keys("static"), ...project.utilities.keys("functional")].map((root) => root.startsWith("-") ? root.slice(1) : root);
1056
+ const functionalClasses = collectFunctionalClasses(project, functionalRoots);
1057
+ const functionalShapes = new Map([...functionalClasses].map(([root, classNames]) => [root, groupFunctionalClasses(project, root, classNames)]));
1058
+ const preservedClasses = reconcileNegativeRoots(project, functionalShapes, staticRoots);
1059
+ const functionalExemplars = new Map([...functionalShapes].map(([root, shapes]) => [root, shapes?.[0]?.exemplar ?? null]));
1060
+ const groupSignatures = collectGroupSignatures(vanilla, vanillaClassGroupId);
1061
+ const groups = /* @__PURE__ */ new Map();
1062
+ const aliases = /* @__PURE__ */ new Map();
1063
+ const postfixLookupClassGroups = [];
1064
+ for (const root of staticRoots) {
1065
+ const lookupRoot = root.startsWith("-") ? root.slice(1) : root;
1066
+ if (preservedClasses.has(lookupRoot)) continue;
1067
+ if (functionalClasses.has(root)) {
1068
+ const functionalExemplar = functionalExemplars.get(root) ?? null;
1069
+ if (functionalExemplar !== null && functionalShapes.get(root)?.length === 1 && fullyCovers(declaredDeclarations(project, root), declaredDeclarations(project, functionalExemplar)) && fullyCovers(declaredDeclarations(project, functionalExemplar), declaredDeclarations(project, root))) continue;
1070
+ groups.set(`${customUtilityGroupId(lookupRoot)}.static`, {
1071
+ items: [{
1072
+ kind: "class",
1073
+ value: lookupRoot
1074
+ }],
1075
+ exemplar: root
1076
+ });
1077
+ continue;
1078
+ }
1079
+ const aliasGroupId = findAliasGroup(project, root, groupSignatures);
1080
+ if (aliasGroupId !== null) aliases.set(lookupRoot, aliasGroupId);
1081
+ else groups.set(customUtilityGroupId(lookupRoot), {
1082
+ items: [{
1083
+ kind: "class",
1084
+ value: lookupRoot
1085
+ }],
1086
+ exemplar: root
1087
+ });
1088
+ }
1089
+ for (const root of functionalRoots) {
1090
+ const lookupRoot = root.startsWith("-") ? root.slice(1) : root;
1091
+ const groupId = customUtilityGroupId(lookupRoot);
1092
+ const shapes = functionalShapes.get(root);
1093
+ if (!shapes) {
1094
+ for (const className of functionalClasses.get(root)) preservedClasses.add(className.startsWith("-") ? className.slice(1) : className);
1095
+ continue;
1096
+ }
1097
+ if (shapes.length > 1) {
1098
+ for (const [index, shape] of shapes.entries()) {
1099
+ const valueItems = encodeScale(shape.classNames.map((className) => className.slice(root.length + 1)), "exact").items;
1100
+ valueItems.push(...shape.validators.filter((name) => name !== "isInteger" || !shape.validators.includes("isNumber")).map((name) => ({
1101
+ kind: "validator",
1102
+ name
1103
+ })));
1104
+ if (valueItems.length > 0) {
1105
+ const shapeGroupId = `${groupId}.${index}`;
1106
+ groups.set(shapeGroupId, {
1107
+ items: [{
1108
+ kind: "object",
1109
+ entries: [[lookupRoot, valueItems]]
1110
+ }],
1111
+ exemplar: shape.exemplar
1112
+ });
1113
+ postfixLookupClassGroups.push(shapeGroupId);
1114
+ }
1115
+ }
1116
+ continue;
1117
+ }
1118
+ const items = [];
1119
+ if (staticRootSet.has(root) && !preservedClasses.has(lookupRoot) && !groups.has(`${groupId}.static`)) items.push({
1120
+ kind: "class",
1121
+ value: lookupRoot
1122
+ });
1123
+ else if (!staticRootSet.has(root) && bareValueJoinsRoot(project, root, shapes[0])) items.push({
1124
+ kind: "class",
1125
+ value: lookupRoot
1126
+ });
1127
+ const hasDescendantRoot = lookupRoots.some((candidate) => candidate.startsWith(`${lookupRoot}-`));
1128
+ const valueItems = encoding === "compact" && !hasDescendantRoot ? [{
1129
+ kind: "validator",
1130
+ name: "isAny"
1131
+ }] : exactFunctionalValueItems(project, root, functionalClasses.get(root).map((className) => className.slice(root.length + 1)));
1132
+ if (valueItems.length > 0) items.push({
1133
+ kind: "object",
1134
+ entries: [[lookupRoot, valueItems]]
1135
+ });
1136
+ if (items.length > 0) {
1137
+ const existing = groups.get(groupId);
1138
+ groups.set(groupId, {
1139
+ items: [...existing?.items ?? [], ...items],
1140
+ exemplar: existing?.exemplar ?? functionalExemplars.get(root) ?? null
1141
+ });
1142
+ }
1143
+ }
1144
+ return {
1145
+ groups: new Map([...groups].map(([groupId, group]) => [groupId, group.items])),
1146
+ aliases,
1147
+ conflicts: inferOverrideConflicts(project, groups, groupSignatures),
1148
+ postfixLookupClassGroups,
1149
+ preservedClasses,
1150
+ extendedBuiltInRoots
1151
+ };
1152
+ }
1153
+ /** Runtime lookup removes a leading minus, so opposite static names, functional roots, and overlapping static/functional names share trie paths. Only compatible effects can share those paths. Preserve conflicting roots and their static claims together, including would-be aliases: either remaining claim could otherwise classify both signs and discard independent styles. */
1154
+ function reconcileNegativeRoots(project, shapesByRoot, staticRoots) {
1155
+ const preservedStaticClasses = /* @__PURE__ */ new Set();
1156
+ const staticRootSet = new Set(staticRoots);
1157
+ for (const root of staticRoots) {
1158
+ if (!root.startsWith("-") || !staticRootSet.has(root.slice(1))) continue;
1159
+ const negative = declaredDeclarations(project, root);
1160
+ const positive = declaredDeclarations(project, root.slice(1));
1161
+ if (!fullyCovers(negative, positive) || !fullyCovers(positive, negative)) preservedStaticClasses.add(root.slice(1));
1162
+ }
1163
+ for (const [root, shapes] of shapesByRoot) {
1164
+ if (!root.startsWith("-")) continue;
1165
+ const positiveRoot = root.slice(1);
1166
+ const positiveShapes = shapesByRoot.get(positiveRoot);
1167
+ let compatible = shapes !== null;
1168
+ if (positiveShapes !== void 0) {
1169
+ if (shapes?.length === 1 && positiveShapes?.length === 1) {
1170
+ const negative = declaredDeclarations(project, shapes[0].exemplar);
1171
+ const positive = declaredDeclarations(project, positiveShapes[0].exemplar);
1172
+ compatible &&= fullyCovers(negative, positive) && fullyCovers(positive, negative);
1173
+ } else compatible = false;
1174
+ }
1175
+ const staticClaims = [];
1176
+ for (const className of staticRoots) {
1177
+ if (className !== positiveRoot && !className.startsWith(`${positiveRoot}-`)) continue;
1178
+ const negative = declaredDeclarations(project, `-${className}`);
1179
+ if (!negative?.length) continue;
1180
+ staticClaims.push(className);
1181
+ const positive = declaredDeclarations(project, className);
1182
+ compatible &&= fullyCovers(negative, positive) && fullyCovers(positive, negative);
1183
+ }
1184
+ if (compatible) continue;
1185
+ shapesByRoot.set(root, null);
1186
+ if (shapesByRoot.has(positiveRoot)) shapesByRoot.set(positiveRoot, null);
1187
+ for (const className of staticClaims) preservedStaticClasses.add(className);
1188
+ }
1189
+ return preservedStaticClasses;
1190
+ }
1191
+ /** Whether a functional root's bare class (a plugin `DEFAULT` value) compiles and covers its shape's exemplar both ways, like a static root that joins its functional group. */
1192
+ function bareValueJoinsRoot(project, root, shape) {
1193
+ const bare = declaredDeclarations(project, root);
1194
+ if (!bare?.length) return false;
1195
+ const exemplar = declaredDeclarations(project, shape.exemplar);
1196
+ return fullyCovers(bare, exemplar) && fullyCovers(exemplar, bare);
1197
+ }
1198
+ /** Group IDs get a `utility.` prefix so they cannot collide with the skeleton's group IDs and are recognizable in reports and the emitted config. */
1199
+ function customUtilityGroupId(root) {
1200
+ return `utility.${root}`;
1201
+ }
1202
+ /**
1203
+ * Indexes suggested classes and their slash modifiers once for both encoding and conflict inference. Modifiers can enable independent declarations through --modifier(), so their complete candidates must participate in effect grouping. Bare utility names belong to their own roots; other classes belong to the longest functional root that prefixes them. Using the same ownership rule prevents a nested root such as `demo-child-*` from becoming the exemplar or a named value of `demo-*`.
1204
+ */
1205
+ function collectFunctionalClasses(project, functionalRoots) {
1206
+ const classesByRoot = new Map(functionalRoots.map((root) => [root, []]));
1207
+ if (functionalRoots.length === 0) return classesByRoot;
1208
+ const projectFunctionalRoots = project.utilities.keys("functional");
1209
+ const bareRoots = /* @__PURE__ */ new Set([...project.utilities.keys("static"), ...projectFunctionalRoots]);
1210
+ const longestRootsFirst = projectFunctionalRoots.sort((first, second) => second.length - first.length);
1211
+ const seen = /* @__PURE__ */ new Set();
1212
+ for (const [className, { modifiers }] of project.getClassList()) {
1213
+ if (bareRoots.has(className) || !functionalRoots.some((root) => className.startsWith(`${root}-`))) continue;
1214
+ const root = longestRootsFirst.find((candidate) => className.startsWith(`${candidate}-`));
1215
+ const classes = root === void 0 ? void 0 : classesByRoot.get(root);
1216
+ if (classes) {
1217
+ for (const candidate of [className, ...modifiers.map((modifier) => `${className}/${modifier}`)]) if (!seen.has(candidate)) {
1218
+ seen.add(candidate);
1219
+ classes.push(candidate);
1220
+ }
1221
+ }
1222
+ }
1223
+ return classesByRoot;
1224
+ }
1225
+ /** Groups named values and numeric kinds by their compiled effects, ignoring values but preserving rule scope and importance. Probe-only shapes prevent unsafe root-wide matchers without registering probe names. Returns null when postfix effects lack a matching full-class group: the runtime falls back to a recognized base group on a full-lookup miss, so leaving only the postfix unclassified would still discard its independent styles. */
1226
+ function groupFunctionalClasses(project, root, classNames) {
1227
+ const groups = /* @__PURE__ */ new Map();
1228
+ const groupsByClassName = /* @__PURE__ */ new Map();
1229
+ const namedClasses = new Set(classNames);
1230
+ const candidates = /* @__PURE__ */ new Set([...classNames, ...FUNCTIONAL_VALUE_PROBES.map((tail) => `${root}-${tail}`)]);
1231
+ for (const className of candidates) {
1232
+ const declarations = declaredDeclarations(project, className);
1233
+ if (!declarations?.length) continue;
1234
+ const signature = functionalEffectSignature(declarations);
1235
+ const group = groups.get(signature) ?? {
1236
+ classNames: [],
1237
+ exemplar: className,
1238
+ validators: []
1239
+ };
1240
+ if (namedClasses.has(className)) group.classNames.push(className);
1241
+ groups.set(signature, group);
1242
+ groupsByClassName.set(className, group);
1243
+ }
1244
+ for (const [validator, sentinels] of BARE_VALUE_PROBES) {
1245
+ const group = groupsByClassName.get(`${root}-${sentinels[0]}`);
1246
+ if (group && sentinels.every((tail) => groupsByClassName.get(`${root}-${tail}`) === group)) group.validators.push(validator);
1247
+ }
1248
+ const result = [...groups.values()];
1249
+ const integerGroup = result.find((group) => group.validators.includes("isInteger"));
1250
+ if (integerGroup) {
1251
+ result.splice(result.indexOf(integerGroup), 1);
1252
+ result.unshift(integerGroup);
1253
+ }
1254
+ const modifiers = /* @__PURE__ */ new Set([...project.utilities.getCompletions(root).flatMap((suggestion) => suggestion.modifiers), ...MODIFIER_PROBES]);
1255
+ for (const className of candidates) {
1256
+ if (segment(className, "/").length > 1) continue;
1257
+ const baseGroup = groupsByClassName.get(className);
1258
+ for (const modifier of modifiers) {
1259
+ const modifiedClass = `${className}/${modifier}`;
1260
+ const declarations = declaredDeclarations(project, modifiedClass);
1261
+ if (!declarations?.length) continue;
1262
+ const modifiedGroup = groups.get(functionalEffectSignature(declarations));
1263
+ if (baseGroup && modifiedGroup === baseGroup) continue;
1264
+ const tail = modifiedClass.slice(root.length + 1);
1265
+ const lookupGroup = namedClasses.has(modifiedClass) ? groupsByClassName.get(modifiedClass) : result.find((group) => group.validators.some((name) => validators[name](tail)));
1266
+ if (!modifiedGroup || lookupGroup !== modifiedGroup) return null;
1267
+ }
1268
+ }
1269
+ return result;
1270
+ }
1271
+ /** Values can vary within a functional group; its affected properties, targets, guards, and importance must remain the same. */
1272
+ function functionalEffectSignature(declarations) {
1273
+ return [...new Set(declarations.map((entry) => JSON.stringify([
1274
+ entry.context,
1275
+ entry.scope,
1276
+ entry.property,
1277
+ entry.important
1278
+ ])))].sort().join("\n");
1279
+ }
1280
+ /**
1281
+ * Value kinds a functional utility can accept beyond its named values, tested with sentinel candidates: accepting the kind (`--value(number)` compiles every number) permits an open validator. The sentinels use unusual theme-token names, with two per kind to reduce the chance that named tokens impersonate open-ended support.
1282
+ */
1283
+ const BARE_VALUE_PROBES = [
1284
+ ["isFraction", ["355/113", "19/97"]],
1285
+ ["isNumber", ["971.5", "823.25"]],
1286
+ ["isInteger", ["9713", "8231"]],
1287
+ ["isPercent", ["79%", "61%"]]
1288
+ ];
1289
+ /**
1290
+ * Representatives covering Tailwind's recognized arbitrary data types for both base values and slash modifiers. Missing a type can make a root with independent branches look uniform, granting an unsafe broad matcher. Some types overlap: lengths also cover position, background size, and line width; URLs cover images; identifiers cover family names.
1291
+ */
1292
+ const ARBITRARY_VALUE_PROBES = [
1293
+ "[3px]",
1294
+ "[7]",
1295
+ "[41%]",
1296
+ "[#650a1b]",
1297
+ "[twm-probe]",
1298
+ "[13/7]",
1299
+ "[url(twm-probe.svg)]",
1300
+ "[serif]",
1301
+ "[medium]",
1302
+ "[larger]",
1303
+ "[13deg]",
1304
+ "[1_2_3]"
1305
+ ];
1306
+ const ARBITRARY_VARIABLE_PROBE = "(--twm-probe)";
1307
+ const MODIFIER_PROBES = [
1308
+ ...BARE_VALUE_PROBES.filter(([name]) => name !== "isFraction").flatMap(([, sentinels]) => sentinels),
1309
+ ...ARBITRARY_VALUE_PROBES,
1310
+ ARBITRARY_VARIABLE_PROBE
1311
+ ];
1312
+ const FUNCTIONAL_VALUE_PROBES = [
1313
+ ...BARE_VALUE_PROBES.flatMap(([, sentinels]) => sentinels),
1314
+ ...ARBITRARY_VALUE_PROBES,
1315
+ ARBITRARY_VARIABLE_PROBE
1316
+ ];
1317
+ /**
1318
+ * The exact-mode value matchers of a functional root whose compiled effects are uniform across suggestions and probes: compile-verified named values (scale-encoded, so families still factor), plus validators for accepted open-ended value kinds. Arbitrary-value matchers still approximate Tailwind's type inference: `isArbitraryValue` can match a wrong type (`ll-[red]` on a `--value([length])` utility), and representative probes do not validate every possible arbitrary value or explicit type label. Mixed-effect roots bypass this helper to avoid assigning different arbitrary branches to one group.
1319
+ */
1320
+ function exactFunctionalValueItems(project, root, namedTails) {
1321
+ const allTails = [...namedTails, ...FUNCTIONAL_VALUE_PROBES];
1322
+ const compileResults = classesCompile(project, allTails.map((tail) => `${root}-${tail}`));
1323
+ const compiledTails = new Set(allTails.filter((_, index) => compileResults[index]));
1324
+ const items = encodeScale(namedTails.filter((tail) => compiledTails.has(tail)), "exact").items;
1325
+ const acceptedKinds = BARE_VALUE_PROBES.filter(([, sentinels]) => sentinels.every((sentinel) => compiledTails.has(sentinel))).map(([validatorName]) => validatorName);
1326
+ for (const validatorName of acceptedKinds) {
1327
+ if (validatorName === "isInteger" && acceptedKinds.includes("isNumber")) continue;
1328
+ items.push({
1329
+ kind: "validator",
1330
+ name: validatorName
1331
+ });
1332
+ }
1333
+ if (ARBITRARY_VALUE_PROBES.some((probe) => compiledTails.has(probe))) items.push({
1334
+ kind: "validator",
1335
+ name: "isArbitraryValue"
1336
+ });
1337
+ if (compiledTails.has(ARBITRARY_VARIABLE_PROBE)) items.push({
1338
+ kind: "validator",
1339
+ name: "isArbitraryVariable"
1340
+ });
1341
+ return items;
1342
+ }
1343
+ /**
1344
+ * One exemplar vanilla class per built-in group, with lazily computed signatures — most groups are never compared against, and compiling ~300 exemplars eagerly would cost more than the whole rest of the pass.
1345
+ */
1346
+ function collectGroupSignatures(vanilla, vanillaClassGroupId) {
1347
+ const exemplars = /* @__PURE__ */ new Map();
1348
+ for (const [className] of vanilla.getClassList()) {
1349
+ const groupId = vanillaClassGroupId(className);
1350
+ if (groupId !== void 0 && !exemplars.has(groupId)) exemplars.set(groupId, className);
1351
+ }
1352
+ const signatureCache = /* @__PURE__ */ new Map();
1353
+ return {
1354
+ groupIds: () => exemplars.keys(),
1355
+ signature: (groupId) => {
1356
+ let signature = signatureCache.get(groupId);
1357
+ if (signature === void 0) {
1358
+ const exemplar = exemplars.get(groupId);
1359
+ signature = exemplar === void 0 ? null : declaredProperties(vanilla, exemplar);
1360
+ signatureCache.set(groupId, signature);
1361
+ }
1362
+ return signature;
1363
+ },
1364
+ declarations: (groupId) => {
1365
+ const exemplar = exemplars.get(groupId);
1366
+ return exemplar === void 0 ? null : declaredDeclarations(vanilla, exemplar);
1367
+ }
1368
+ };
1369
+ }
1370
+ /**
1371
+ * Finds the single built-in group whose classes set exactly what the static utility sets: the signatures (context-qualified property names) must be equal, and utility and group exemplar must cover each other. Coverage checks conditions and importance, so media-query padding or inline-important padding cannot alias into the normal unconditional `p` group. Zero matches means the utility does its own thing; several matches would make the choice a guess, so both fall back to self-conflict grouping.
1372
+ */
1373
+ function findAliasGroup(project, className, groupSignatures) {
1374
+ const properties = declaredProperties(project, className);
1375
+ if (properties === null || properties.size === 0) return null;
1376
+ const declarations = declaredDeclarations(project, className);
1377
+ const matches = [];
1378
+ for (const groupId of groupSignatures.groupIds()) {
1379
+ const signature = groupSignatures.signature(groupId);
1380
+ if (signature !== null && signature.size === properties.size && [...properties].every((key) => signature.has(key)) && aliasEquivalent(declarations, groupSignatures.declarations(groupId))) matches.push(groupId);
1381
+ }
1382
+ return matches.length === 1 ? matches[0] : null;
1383
+ }
1384
+ /**
1385
+ * Signature equality alone is blind to conditions and importance. Aliases need an unconditional element-level effect and mutual coverage: a later built-in color must never remove a custom utility's important, hover, or dark-mode color, even when both touch only `color`.
1386
+ */
1387
+ function aliasEquivalent(first, second) {
1388
+ if (first === null || second === null) return false;
1389
+ return first.some((entry) => entry.context === "" && !entry.conditional) && fullyCovers(first, second) && fullyCovers(second, first);
1390
+ }
1391
+ /**
1392
+ * Whether a class fully covers another, meaning: with the coverer coming later, the covered class has no independent effect left, so removing it loses nothing. Each declaration of the target must be accounted for — an unconditional element-level real property by an equal or shorthand property of the coverer, an unconditional element-level custom property (a state carrier like `--hit-area-l`) by the coverer re-declaring the same one, and everything conditional or targeting another element (shared `::before` scaffolding) only by a byte-identical declaration under the same enclosing rules in the coverer. Anything unaccounted for means partial overlap, and partial overlap never justifies removal — the same rule the default config applies between `px` and `p`.
1393
+ * Every match also needs at least the target declaration's importance: a normal declaration cannot replace an inline-important property or custom-property state, even when the names and values agree.
1394
+ */
1395
+ function fullyCovers(coverer, target) {
1396
+ if (coverer === null || target === null || target.length === 0) return false;
1397
+ return target.every((targetEntry) => coverer.some((entry) => {
1398
+ if (targetEntry.important && !entry.important) return false;
1399
+ if (targetEntry.context === "" && !targetEntry.conditional) {
1400
+ if (entry.context !== "" || entry.conditional) return false;
1401
+ return targetEntry.property.startsWith("--") ? entry.property === targetEntry.property : !entry.property.startsWith("--") && propertyCovers(entry.property, targetEntry.property);
1402
+ }
1403
+ return sameDeclarationScope(entry, targetEntry) && entry.property === targetEntry.property && entry.value === targetEntry.value;
1404
+ }));
1405
+ }
1406
+ /**
1407
+ * For every custom group, finds the groups whose exemplar declarations its utility fully covers — whenever the utility comes later in a class list, the covered class is redundant and gets removed. One class stands in for each group: custom functional values are partitioned by their observed effects first, while built-in groups retain the representative-class approximation used in classification.
1408
+ */
1409
+ function inferOverrideConflicts(project, customGroups, groupSignatures) {
1410
+ const customDeclarations = /* @__PURE__ */ new Map();
1411
+ for (const [groupId, { exemplar }] of customGroups) customDeclarations.set(groupId, exemplar === null ? null : declaredDeclarations(project, exemplar));
1412
+ const conflicts = /* @__PURE__ */ new Map();
1413
+ for (const [groupId, declarations] of customDeclarations) {
1414
+ if (declarations === null || declarations.length === 0) continue;
1415
+ const covered = [];
1416
+ for (const targetGroupId of groupSignatures.groupIds()) if (fullyCovers(declarations, groupSignatures.declarations(targetGroupId))) covered.push(targetGroupId);
1417
+ for (const [otherGroupId, otherDeclarations] of customDeclarations) if (otherGroupId !== groupId && fullyCovers(declarations, otherDeclarations)) covered.push(otherGroupId);
1418
+ if (covered.length > 0) conflicts.set(groupId, covered);
1419
+ }
1420
+ return conflicts;
1421
+ }
1422
+ //#endregion
1423
+ //#region ../configurator/src/emit.ts
1424
+ /**
1425
+ * Serializes a plan into the source code of a standalone module exporting `getConfig` and `twMerge`.
1426
+ *
1427
+ * The whole config is built inside `getConfig` so that nothing is allocated at module evaluation — `createTailwindMerge` invokes the callback on the first `twMerge` call, preserving the library's lazy-init behavior. The module imports only `createTailwindMerge` and `validators` from tailwind-merge, so bundlers tree-shake the default config away.
1428
+ *
1429
+ * Two measured insights shape the output. Validators are destructured once and referenced as bare identifiers because property accesses like `v.isArbitraryVariable` survive minification while local bindings get mangled. And only the resolved theme scales are shared as consts, emitted as references or spreads (e.g. `['none', ...scaleBlur]`): deduplicating other repetition into consts shrinks the minified size but *grows* the compressed size, since references add entropy where gzip/brotli handled the repetition nearly for free (measured on the vanilla theme: 31,989 / 8,822 / 7,706 bytes minified / gzip / brotli with scale sharing, against 28,492 / 9,364 / 8,197 with every paying repetition hoisted). Sharing array identity across class groups is safe because tailwind-merge never mutates config arrays. Output is deterministic for identical input so the CLI's `--check` mode can diff against the file on disk.
1430
+ */
1431
+ function emitModule(plan, options = {}) {
1432
+ const format = options.format ?? "ts";
1433
+ const candidates = collectConstantCandidates(plan);
1434
+ const output = serializeAll(plan, candidates, new Map([...candidates].map(([canonical, candidate]) => [canonical, candidate.name])), format);
1435
+ const usedCanonicals = resolveTransitiveUsage(output);
1436
+ const { configBody } = output;
1437
+ const lines = [];
1438
+ lines.push("// Generated by @tailwind-merge/configurator. Do not edit manually.");
1439
+ if (options.banner) lines.push(options.banner);
1440
+ lines.push("");
1441
+ const usedValidators = collectUsedValidatorNames(plan);
1442
+ const importSource = options.importSource ?? "tailwind-merge";
1443
+ const validatorsImport = usedValidators.length > 0 ? ", validators as v" : "";
1444
+ lines.push(format === "ts" ? `import { createTailwindMerge${validatorsImport}, type Config } from '${importSource}'` : `import { createTailwindMerge${validatorsImport} } from '${importSource}'`);
1445
+ lines.push("");
1446
+ lines.push("/**");
1447
+ lines.push(" * Builds the tailwind-merge config for this project. Called lazily on the first `twMerge` call. Can be composed further via `createTailwindMerge(getConfig, ...extensions)`.");
1448
+ lines.push(" *");
1449
+ lines.push(" * The structure mirrors tailwind-merge's default config. The `scale*` consts hold this project's resolved theme scales — each one's comment says which theme namespace it came from — and class groups use them by reference or as spreads. Patterns like `isTshirtSize` come from tailwind-merge's public validators.");
1450
+ lines.push(" */");
1451
+ lines.push("export const getConfig = () => {");
1452
+ if (usedValidators.length > 0) {
1453
+ const inline = `${INDENT}const { ${usedValidators.join(", ")} } = v`;
1454
+ if (inline.length <= MAX_LINE_LENGTH) lines.push(inline);
1455
+ else {
1456
+ lines.push(`${INDENT}const {`);
1457
+ lines.push(...usedValidators.map((name) => `${INDENT}${INDENT}${name},`));
1458
+ lines.push(`${INDENT}} = v`);
1459
+ }
1460
+ lines.push("");
1461
+ }
1462
+ const emittedConstants = sortByDependencies(output, usedCanonicals);
1463
+ for (const [canonical, body] of emittedConstants) {
1464
+ const { name, comment } = candidates.get(canonical);
1465
+ if (comment) lines.push(`${INDENT}/** ${comment} */`);
1466
+ lines.push(`${INDENT}const ${name} = ${body}`);
1467
+ }
1468
+ if (emittedConstants.length > 0) lines.push("");
1469
+ lines.push(...configBody);
1470
+ lines.push("}");
1471
+ lines.push("");
1472
+ lines.push("export const twMerge = createTailwindMerge(getConfig)");
1473
+ lines.push("");
1474
+ return lines.join("\n");
1475
+ }
1476
+ const MAX_LINE_LENGTH = 100;
1477
+ const INDENT = " ";
1478
+ /** Validator names in first-use order, for the destructuring statement at the top of `getConfig`. */
1479
+ function collectUsedValidatorNames(plan) {
1480
+ const names = /* @__PURE__ */ new Set();
1481
+ function visitItems(items) {
1482
+ for (const item of items) if (item.kind === "validator") names.add(item.name);
1483
+ else if (item.kind === "object") for (const [, entryItems] of item.entries) visitItems(entryItems);
1484
+ }
1485
+ for (const [, items] of plan.classGroups) visitItems(items);
1486
+ return [...names];
1487
+ }
1488
+ /** Gathers the resolved theme scales as shared-const candidates; group arrays contain them verbatim wherever a theme getter was substituted. Theme keys are unique, so the derived names are too. */
1489
+ function collectConstantCandidates(plan) {
1490
+ const candidates = /* @__PURE__ */ new Map();
1491
+ for (const [themeKey, scale] of plan.scales) {
1492
+ if (scale.items.length < 2) continue;
1493
+ const canonical = canonicalArray(scale.items);
1494
+ const existing = candidates.get(canonical);
1495
+ if (!existing) candidates.set(canonical, {
1496
+ items: scale.items,
1497
+ itemCanonicals: scale.items.map(canonicalValue),
1498
+ name: scaleConstName(themeKey),
1499
+ comment: scale.comment
1500
+ });
1501
+ else if (scale.comment !== null) existing.comment = `${existing.comment ?? ""} ${scale.comment}`.trim();
1502
+ }
1503
+ return candidates;
1504
+ }
1505
+ /** `color` → `scaleColor`, `font-weight` → `scaleFontWeight`. The `scale` prefix avoids collisions with destructured validator names. */
1506
+ function scaleConstName(themeKey) {
1507
+ return `scale${themeKey.split("-").map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`).join("")}`;
1508
+ }
1509
+ /**
1510
+ * Serializes the config body and every candidate's body with the given name map, recording which candidates each output references so usage can be resolved before final naming.
1511
+ */
1512
+ function serializeAll(plan, candidates, names, format) {
1513
+ const directUsage = /* @__PURE__ */ new Set();
1514
+ const usageByConstant = /* @__PURE__ */ new Map();
1515
+ const constantBodies = /* @__PURE__ */ new Map();
1516
+ for (const [canonical, candidate] of candidates) {
1517
+ const usage = /* @__PURE__ */ new Set();
1518
+ constantBodies.set(canonical, serializeArrayBody(candidate.items, 4, {
1519
+ candidates,
1520
+ names,
1521
+ usage,
1522
+ selfCanonical: canonical
1523
+ }));
1524
+ usageByConstant.set(canonical, usage);
1525
+ }
1526
+ const context = {
1527
+ candidates,
1528
+ names,
1529
+ usage: directUsage,
1530
+ selfCanonical: null
1531
+ };
1532
+ const configBody = [];
1533
+ configBody.push(`${INDENT}return {`);
1534
+ configBody.push(`${INDENT}${INDENT}cacheSize: ${plan.cacheSize},`);
1535
+ if (plan.prefix !== null) configBody.push(`${INDENT}${INDENT}prefix: ${quote(plan.prefix)},`);
1536
+ configBody.push(`${INDENT}${INDENT}theme: {},`);
1537
+ configBody.push(`${INDENT}${INDENT}classGroups: {`);
1538
+ for (const [classGroupId, items] of plan.classGroups) configBody.push(`${INDENT.repeat(3)}${propertyKey(classGroupId)}: ${serializeArray(items, 12, context)},`);
1539
+ configBody.push(`${INDENT}${INDENT}},`);
1540
+ pushStringRecordMap(configBody, "conflictingClassGroups", plan.conflictingClassGroups);
1541
+ pushStringRecordMap(configBody, "conflictingClassGroupModifiers", plan.conflictingClassGroupModifiers);
1542
+ configBody.push(`${INDENT}${INDENT}postfixLookupClassGroups: ${serializeStringArray(plan.postfixLookupClassGroups)},`);
1543
+ configBody.push(`${INDENT}${INDENT}orderSensitiveModifiers: ${serializeStringArray(plan.orderSensitiveModifiers)},`);
1544
+ configBody.push(format === "ts" ? `${INDENT}} satisfies Config<string, never>` : `${INDENT}}`);
1545
+ return {
1546
+ configBody,
1547
+ constantBodies,
1548
+ directUsage,
1549
+ usageByConstant
1550
+ };
1551
+ }
1552
+ /** Usage is transitive: a const referenced only from another used const's body still has to be emitted. */
1553
+ function resolveTransitiveUsage(output) {
1554
+ const used = new Set(output.directUsage);
1555
+ let changed = true;
1556
+ while (changed) {
1557
+ changed = false;
1558
+ for (const canonical of used) for (const referenced of output.usageByConstant.get(canonical) ?? []) if (!used.has(referenced)) {
1559
+ used.add(referenced);
1560
+ changed = true;
1561
+ }
1562
+ }
1563
+ return used;
1564
+ }
1565
+ /** Orders the used consts so that every reference points to an already-declared name, using the exact usage sets recorded during serialization. Containment makes cycles impossible. */
1566
+ function sortByDependencies(output, used) {
1567
+ const remaining = new Map([...output.constantBodies].filter(([canonical]) => used.has(canonical)));
1568
+ const declared = /* @__PURE__ */ new Set();
1569
+ const sorted = [];
1570
+ while (remaining.size > 0) {
1571
+ let progressed = false;
1572
+ for (const [canonical, body] of remaining) if ([...output.usageByConstant.get(canonical) ?? []].filter((dependency) => used.has(dependency) && dependency !== canonical).every((dependency) => declared.has(dependency))) {
1573
+ declared.add(canonical);
1574
+ sorted.push([canonical, body]);
1575
+ remaining.delete(canonical);
1576
+ progressed = true;
1577
+ }
1578
+ if (!progressed) throw new Error("Cyclic references between generated consts, this is a bug in emit.ts");
1579
+ }
1580
+ return sorted;
1581
+ }
1582
+ function serializeArray(items, indent, context) {
1583
+ const canonical = canonicalArray(items);
1584
+ if (canonical !== context.selfCanonical) {
1585
+ const name = context.names.get(canonical);
1586
+ if (name) {
1587
+ context.usage.add(canonical);
1588
+ return name;
1589
+ }
1590
+ }
1591
+ return serializeArrayBody(items, indent, context);
1592
+ }
1593
+ function serializeArrayBody(items, indent, context) {
1594
+ const itemCanonicals = items.map(canonicalValue);
1595
+ const parts = [];
1596
+ let index = 0;
1597
+ while (index < items.length) {
1598
+ const run = findLongestRun(items, itemCanonicals, index, context);
1599
+ if (run) {
1600
+ parts.push(`...${run.name}`);
1601
+ context.usage.add(run.canonical);
1602
+ index += run.length;
1603
+ } else {
1604
+ parts.push(serializeValue(items[index], indent + 4, context));
1605
+ index += 1;
1606
+ }
1607
+ }
1608
+ const inline = `[${parts.join(", ")}]`;
1609
+ if (indent + inline.length <= MAX_LINE_LENGTH && !inline.includes("\n")) return inline;
1610
+ const itemIndent = INDENT.repeat(indent / 4 + 1);
1611
+ const closingIndent = INDENT.repeat(indent / 4);
1612
+ return `[\n${parts.map((part) => `${itemIndent}${part},`).join("\n")}\n${closingIndent}]`;
1613
+ }
1614
+ /** Finds the longest candidate array matching the items starting at `start`, to be emitted as a spread. Skips the candidate currently being defined and full-array matches (those are handled as plain references). */
1615
+ function findLongestRun(items, itemCanonicals, start, context) {
1616
+ let best = null;
1617
+ for (const [canonical, candidate] of context.candidates) {
1618
+ if (canonical === context.selfCanonical) continue;
1619
+ const length = candidate.itemCanonicals.length;
1620
+ if (length < 2 || start === 0 && length === items.length || start + length > items.length || best && length <= best.length) continue;
1621
+ const name = context.names.get(canonical);
1622
+ if (!name) continue;
1623
+ let matches = true;
1624
+ for (let offset = 0; offset < length; offset++) if (itemCanonicals[start + offset] !== candidate.itemCanonicals[offset]) {
1625
+ matches = false;
1626
+ break;
1627
+ }
1628
+ if (matches) best = {
1629
+ name,
1630
+ canonical,
1631
+ length
1632
+ };
1633
+ }
1634
+ return best;
1635
+ }
1636
+ function serializeValue(value, indent, context) {
1637
+ if (value.kind === "class") return quote(value.value);
1638
+ if (value.kind === "validator") return value.name;
1639
+ return serializeObjectBody(value, indent, context);
1640
+ }
1641
+ function serializeObjectBody(value, indent, context) {
1642
+ const entryParts = value.entries.map(([key, items]) => `${propertyKey(key)}: ${serializeArray(items, indent + 4, context)}`);
1643
+ const inline = `{ ${entryParts.join(", ")} }`;
1644
+ if (indent + inline.length <= MAX_LINE_LENGTH && !inline.includes("\n")) return inline;
1645
+ const entryIndent = INDENT.repeat(indent / 4 + 1);
1646
+ const closingIndent = INDENT.repeat(indent / 4);
1647
+ return `{\n${entryParts.map((part) => `${entryIndent}${part},`).join("\n")}\n${closingIndent}}`;
1648
+ }
1649
+ /**
1650
+ * Canonical string form used as identity for scale references and run matching: fully inlined, ignoring shared consts and line breaks, so identical content always produces identical keys. Cached by node identity since plans reuse instances for repeated scales.
1651
+ */
1652
+ function canonicalArray(items) {
1653
+ let canonical = canonicalArrayCache.get(items);
1654
+ if (!canonical) {
1655
+ canonical = `[${items.map(canonicalValue).join(", ")}]`;
1656
+ canonicalArrayCache.set(items, canonical);
1657
+ }
1658
+ return canonical;
1659
+ }
1660
+ function canonicalValue(value) {
1661
+ if (value.kind === "class") return quote(value.value);
1662
+ if (value.kind === "validator") return `v.${value.name}`;
1663
+ let canonical = canonicalObjectCache.get(value);
1664
+ if (!canonical) {
1665
+ canonical = `{ ${value.entries.map(([key, items]) => `${propertyKey(key)}: ${canonicalArray(items)}`).join(", ")} }`;
1666
+ canonicalObjectCache.set(value, canonical);
1667
+ }
1668
+ return canonical;
1669
+ }
1670
+ const canonicalArrayCache = /* @__PURE__ */ new WeakMap();
1671
+ const canonicalObjectCache = /* @__PURE__ */ new WeakMap();
1672
+ function pushStringRecordMap(lines, property, map) {
1673
+ if (map.size === 0) {
1674
+ lines.push(`${INDENT}${INDENT}${property}: {},`);
1675
+ return;
1676
+ }
1677
+ lines.push(`${INDENT}${INDENT}${property}: {`);
1678
+ for (const [key, values] of map) lines.push(`${INDENT.repeat(3)}${propertyKey(key)}: ${serializeStringArray(values)},`);
1679
+ lines.push(`${INDENT}${INDENT}},`);
1680
+ }
1681
+ function serializeStringArray(values) {
1682
+ return `[${values.map(quote).join(", ")}]`;
1683
+ }
1684
+ /** Object literals treat __proto__ as a prototype setter even when quoted; computed syntax preserves the theme entry as an own property. */
1685
+ function propertyKey(key) {
1686
+ if (key === "__proto__") return `[${quote(key)}]`;
1687
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : quote(key);
1688
+ }
1689
+ function quote(value) {
1690
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
1691
+ }
1692
+ //#endregion
1693
+ //#region ../configurator/src/materialize.ts
1694
+ /**
1695
+ * Turns a plan into a runtime config object, the in-memory equivalent of the emitted module.
1696
+ *
1697
+ * Exists so tests and programmatic consumers can use the generated behavior directly without writing the emitted source to disk and importing it. Because both this and the emitter read the same plan, the two outputs describe the same config by construction.
1698
+ */
1699
+ function materializeConfig(plan) {
1700
+ return {
1701
+ cacheSize: plan.cacheSize,
1702
+ ...plan.prefix === null ? {} : { prefix: plan.prefix },
1703
+ theme: {},
1704
+ classGroups: Object.fromEntries([...plan.classGroups].map(([classGroupId, items]) => [classGroupId, items.map(materializeValue)])),
1705
+ conflictingClassGroups: Object.fromEntries(plan.conflictingClassGroups),
1706
+ conflictingClassGroupModifiers: Object.fromEntries(plan.conflictingClassGroupModifiers),
1707
+ postfixLookupClassGroups: plan.postfixLookupClassGroups,
1708
+ orderSensitiveModifiers: plan.orderSensitiveModifiers
1709
+ };
1710
+ }
1711
+ function materializeValue(value) {
1712
+ if (value.kind === "class") return value.value;
1713
+ if (value.kind === "validator") return validators[value.name];
1714
+ return Object.fromEntries(value.entries.map(([key, items]) => [key, items.map(materializeValue)]));
1715
+ }
1716
+ //#endregion
1717
+ //#region ../configurator/src/plan.ts
1718
+ /**
1719
+ * Transforms the default config skeleton into a plan with all theme references resolved against the design-system snapshot.
1720
+ *
1721
+ * Walking `getDefaultConfig()` instead of maintaining a parallel structure means class group semantics, group ordering (which decides validator precedence in the class map), and conflict relationships automatically stay in sync with tailwind-merge.
1722
+ */
1723
+ function buildPlan({ snapshot, cacheSize, encoding = "compact" }) {
1724
+ const skeleton = getDefaultConfig();
1725
+ const scaleEncodings = /* @__PURE__ */ new Map();
1726
+ function resolveScale(themeKey) {
1727
+ let scaleEncoding = scaleEncodings.get(themeKey);
1728
+ if (!scaleEncoding) {
1729
+ scaleEncoding = encodeThemeScale(themeKey, snapshot, encoding);
1730
+ scaleEncodings.set(themeKey, scaleEncoding);
1731
+ }
1732
+ return scaleEncoding;
1733
+ }
1734
+ function planGroup(group) {
1735
+ return dedupeValues(group.flatMap(planDefinition));
1736
+ }
1737
+ function planDefinition(definition) {
1738
+ if (typeof definition === "string") return [{
1739
+ kind: "class",
1740
+ value: definition
1741
+ }];
1742
+ if (typeof definition === "function") {
1743
+ if (isThemeGetter(definition)) {
1744
+ if (definition.themeKey === void 0) throw new Error("Theme getter without a themeKey property — the configurator requires a tailwind-merge version that exposes it");
1745
+ return cloneValues(resolveScale(definition.themeKey).items);
1746
+ }
1747
+ const name = validatorNames.get(definition);
1748
+ if (!name) throw new Error("Unknown validator in default config, cannot emit a reference to it");
1749
+ return [{
1750
+ kind: "validator",
1751
+ name
1752
+ }];
1753
+ }
1754
+ const entries = Object.entries(definition).map(([key, value]) => [key, planGroup(value)]).filter(([, items]) => items.length > 0);
1755
+ return entries.length === 0 ? [] : [{
1756
+ kind: "object",
1757
+ entries
1758
+ }];
1759
+ }
1760
+ const classGroups = /* @__PURE__ */ new Map();
1761
+ const prunedClassGroups = [];
1762
+ for (const [classGroupId, group] of Object.entries(skeleton.classGroups)) {
1763
+ const items = planGroup(group);
1764
+ if (items.length === 0) prunedClassGroups.push(classGroupId);
1765
+ else classGroups.set(classGroupId, items);
1766
+ }
1767
+ for (const [classGroupId, classNames] of Object.entries(UTILITY_STATIC_CLASSES)) {
1768
+ const items = classGroups.get(classGroupId);
1769
+ if (items) items.push(...classNames.map((value) => ({
1770
+ kind: "class",
1771
+ value
1772
+ })));
1773
+ }
1774
+ return {
1775
+ cacheSize: cacheSize ?? skeleton.cacheSize,
1776
+ prefix: snapshot.prefix,
1777
+ classGroups,
1778
+ scales: new Map([...scaleEncodings].map(([themeKey, encoding]) => [themeKey, {
1779
+ items: encoding.items,
1780
+ comment: describeScale(themeKey, encoding.strategy)
1781
+ }])),
1782
+ conflictingClassGroups: filterConflictMap(skeleton.conflictingClassGroups, classGroups),
1783
+ conflictingClassGroupModifiers: filterConflictMap(skeleton.conflictingClassGroupModifiers, classGroups),
1784
+ postfixLookupClassGroups: (skeleton.postfixLookupClassGroups ?? []).filter((classGroupId) => classGroups.has(classGroupId)),
1785
+ orderSensitiveModifiers: [...skeleton.orderSensitiveModifiers],
1786
+ report: {
1787
+ encoding,
1788
+ scaleStrategies: Object.fromEntries([...scaleEncodings].map(([themeKey, scaleEncoding]) => [themeKey, scaleEncoding.strategy])),
1789
+ prunedClassGroups,
1790
+ customUtilityGroups: [],
1791
+ aliasedUtilityClasses: {},
1792
+ customUtilityConflicts: {},
1793
+ augmentedClassGroups: {},
1794
+ resolvedCollisions: [],
1795
+ unassignedClasses: []
1796
+ }
1797
+ };
1798
+ }
1799
+ /**
1800
+ * Applies the custom-utility plan: self-conflict groups join `classGroups` like any other group, alias classes join their built-in group as literals (the trie gives named paths precedence, and joining wires up the group's full conflict behavior), and inferred override relationships land in `conflictingClassGroups` so a custom utility coming later removes the classes it fully covers.
1801
+ */
1802
+ function applyCustomUtilityPlan(plan, customUtilityPlan) {
1803
+ for (const [groupId, items] of customUtilityPlan.groups) {
1804
+ plan.classGroups.set(groupId, items);
1805
+ plan.report.customUtilityGroups.push(groupId);
1806
+ }
1807
+ plan.postfixLookupClassGroups.push(...customUtilityPlan.postfixLookupClassGroups);
1808
+ for (const root of customUtilityPlan.extendedBuiltInRoots) plan.report.unassignedClasses.push({
1809
+ className: `${root}-*`,
1810
+ reason: "custom utility extends a built-in root; only values the built-in matchers accept are classified"
1811
+ });
1812
+ for (const [className, groupId] of customUtilityPlan.aliases) {
1813
+ const items = plan.classGroups.get(groupId);
1814
+ if (!items) plan.classGroups.set(groupId, [{
1815
+ kind: "class",
1816
+ value: className
1817
+ }]);
1818
+ else items.push({
1819
+ kind: "class",
1820
+ value: className
1821
+ });
1822
+ plan.report.aliasedUtilityClasses[className] = groupId;
1823
+ }
1824
+ for (const [groupId, coveredGroupIds] of customUtilityPlan.conflicts) {
1825
+ const existingTargets = coveredGroupIds.filter((targetId) => plan.classGroups.has(targetId));
1826
+ if (existingTargets.length === 0) continue;
1827
+ const conflictTargets = plan.conflictingClassGroups.get(groupId);
1828
+ if (conflictTargets) conflictTargets.push(...existingTargets);
1829
+ else plan.conflictingClassGroups.set(groupId, existingTargets);
1830
+ plan.report.customUtilityConflicts[groupId] = existingTargets;
1831
+ }
1832
+ }
1833
+ /**
1834
+ * Appends augmentation classes (full class names determined by the vanilla-diff pass) to their class groups and records them in the report. Appending literals is enough: the trie gives named paths precedence over validators, and joining an existing group wires up all its conflict relations automatically.
1835
+ */
1836
+ function applyAugmentations(plan, augmentations) {
1837
+ for (const [classGroupId, classNames] of augmentations.assignments) {
1838
+ const items = plan.classGroups.get(classGroupId);
1839
+ if (!items) plan.classGroups.set(classGroupId, classNames.map((value) => ({
1840
+ kind: "class",
1841
+ value
1842
+ })));
1843
+ else items.push(...classNames.map((value) => ({
1844
+ kind: "class",
1845
+ value
1846
+ })));
1847
+ plan.report.augmentedClassGroups[classGroupId] = classNames;
1848
+ }
1849
+ plan.report.unassignedClasses.push(...augmentations.unassigned);
1850
+ for (const { className, claimingGroupId, ownerGroupId, resolution } of augmentations.collisions) {
1851
+ const groupIdsToRemoveFrom = resolution === "restore" ? [claimingGroupId] : [.../* @__PURE__ */ new Set([claimingGroupId, ownerGroupId])];
1852
+ const removedFromGroupIds = groupIdsToRemoveFrom.filter((groupId) => {
1853
+ const items = plan.classGroups.get(groupId);
1854
+ return items !== void 0 && removeClassClaim(items, className);
1855
+ });
1856
+ const fullyRemoved = removedFromGroupIds.length === groupIdsToRemoveFrom.length;
1857
+ if (resolution === "restore") {
1858
+ const ownerItems = plan.classGroups.get(ownerGroupId);
1859
+ if (!fullyRemoved && ownerItems !== void 0 && !hasClassClaim(ownerItems, className)) ownerItems.push({
1860
+ kind: "class",
1861
+ value: className
1862
+ });
1863
+ plan.report.resolvedCollisions.push({
1864
+ className,
1865
+ keptGroupId: ownerGroupId,
1866
+ removedFromGroupIds
1867
+ });
1868
+ } else {
1869
+ if (!fullyRemoved) plan.classGroups.set(neutralizedGroupId(className), [{
1870
+ kind: "class",
1871
+ value: className
1872
+ }]);
1873
+ plan.report.resolvedCollisions.push({
1874
+ className,
1875
+ keptGroupId: null,
1876
+ removedFromGroupIds
1877
+ });
1878
+ }
1879
+ }
1880
+ }
1881
+ /** Group ID for a neutralized class that keeps a validator-based claim somewhere: a group of its own, referenced by no conflict map, so the class behaves like a non-Tailwind class while still outranking the validator through its literal path. */
1882
+ function neutralizedGroupId(className) {
1883
+ return `collision.${className}`;
1884
+ }
1885
+ /** Whether `className` already resolves into these items through a literal — the check that keeps a restore from appending a duplicate literal to the owner group. Mirrors the walk in `removeClassClaim`. */
1886
+ function hasClassClaim(items, className) {
1887
+ return items.some((item) => {
1888
+ if (item.kind === "class") return item.value === className;
1889
+ if (item.kind === "object") return item.entries.some(([key, entryItems]) => className.startsWith(`${key}-`) && hasClassClaim(entryItems, className.slice(key.length + 1)));
1890
+ return false;
1891
+ });
1892
+ }
1893
+ /** Deep copy of plan values, so group arrays can be edited independently of each other and of the shared scale definitions. */
1894
+ function cloneValues(values) {
1895
+ return values.map((value) => value.kind === "object" ? {
1896
+ kind: "object",
1897
+ entries: value.entries.map(([key, items]) => [key, cloneValues(items)])
1898
+ } : value);
1899
+ }
1900
+ /**
1901
+ * Removes the item that makes `className` resolve into this group: a full-class literal, or a literal reached through object entries whose keys prefix the class name — recursively, because the family encoding nests (`background-alternative-200` may live at `{ background: [{ alternative: ['200'] }] }`). Returns false when the claim comes from something else (a validator), which the caller reports instead of guessing.
1902
+ */
1903
+ function removeClassClaim(items, className) {
1904
+ for (let index = 0; index < items.length; index++) {
1905
+ const item = items[index];
1906
+ if (item.kind === "class" && item.value === className) {
1907
+ items.splice(index, 1);
1908
+ return true;
1909
+ }
1910
+ if (item.kind === "object") {
1911
+ for (const [key, entryItems] of item.entries) if (className.startsWith(`${key}-`) && removeClassClaim(entryItems, className.slice(key.length + 1))) return true;
1912
+ }
1913
+ }
1914
+ return false;
1915
+ }
1916
+ /**
1917
+ * Colors that every color utility accepts as static keywords in addition to theme values. They are utility semantics rather than theme variables, so the design system's theme does not contain them. Today's default config matches them implicitly through the permissive `isAny` color scale.
1918
+ */
1919
+ const COLOR_KEYWORDS = [
1920
+ "inherit",
1921
+ "current",
1922
+ "transparent"
1923
+ ];
1924
+ /**
1925
+ * Static utility classes that belong to specific class groups but are neither theme values nor skeleton literals — the default config catches them through permissive validators or its approximated theme scales, both of which the plan replaces with exact values. The shadow `*-initial` utilities reset the corresponding `--tw-*-shadow-color` custom property, the gradient `*-none` utilities reset gradient stops, and `perspective-none` is a static value the skeleton keeps in its theme approximation. All verified via `candidatesToCss` probing against tailwindcss 4.3; P2's probing infrastructure should derive or at least verify this list automatically.
1926
+ */
1927
+ const UTILITY_STATIC_CLASSES = {
1928
+ "shadow-color": ["shadow-initial"],
1929
+ "inset-shadow-color": ["inset-shadow-initial"],
1930
+ "text-shadow-color": ["text-shadow-initial"],
1931
+ "gradient-from": ["from-none"],
1932
+ "gradient-via": ["via-none"],
1933
+ "gradient-to": ["to-none"],
1934
+ accent: ["accent-auto"],
1935
+ leading: ["leading-none"],
1936
+ perspective: ["perspective-none"]
1937
+ };
1938
+ /**
1939
+ * Encodes the scale for one theme key, applying per-key knowledge on top of the generic encoding.
1940
+ */
1941
+ function encodeThemeScale(themeKey, snapshot, encoding) {
1942
+ const scale = snapshot.scales.get(themeKey);
1943
+ const names = scale?.names ?? [];
1944
+ if (themeKey === "color") {
1945
+ const scaleEncoding = encodeScale(names, encoding);
1946
+ return {
1947
+ items: [...COLOR_KEYWORDS.map((value) => ({
1948
+ kind: "class",
1949
+ value
1950
+ })), ...scaleEncoding.items],
1951
+ strategy: scaleEncoding.strategy
1952
+ };
1953
+ }
1954
+ if (themeKey === "spacing") {
1955
+ const scaleEncoding = encodeScale(names, encoding);
1956
+ const items = [{
1957
+ kind: "class",
1958
+ value: "px"
1959
+ }];
1960
+ let strategy = scaleEncoding.strategy;
1961
+ if (scale?.hasBareValue) {
1962
+ items.push({
1963
+ kind: "validator",
1964
+ name: "isNumber"
1965
+ });
1966
+ strategy = names.length === 0 ? "multiplier" : `multiplier+${scaleEncoding.strategy}`;
1967
+ }
1968
+ items.push(...scaleEncoding.items);
1969
+ return {
1970
+ items,
1971
+ strategy
1972
+ };
1973
+ }
1974
+ return encodeScale(names, encoding);
1975
+ }
1976
+ /**
1977
+ * Explains where a scale's values come from and why they are encoded the way they are, so the generated file stays debuggable without readers having to know the compression policy. Derived from the encoding strategy instead of restating the values, which the code right below the comment already shows.
1978
+ */
1979
+ function describeScale(themeKey, strategy) {
1980
+ const namespace = `\`--${themeKey}-*\``;
1981
+ if (themeKey === "spacing" && strategy.startsWith("multiplier")) {
1982
+ const base = `The bare \`--spacing\` multiplier is set, which makes every number a valid spacing value (e.g. p-13).`;
1983
+ return strategy === "multiplier" ? base : `Named ${namespace} theme values. ${base}`;
1984
+ }
1985
+ const prefix = themeKey === "color" ? `Color keywords plus the ${namespace} theme values` : `The ${namespace} theme values`;
1986
+ if (strategy === "families") return `${prefix}, with families sharing numeric suffixes compressed into nested matchers.`;
1987
+ if (strategy.startsWith("validator:")) return `${prefix}, all matching \`${strategy.slice(10)}\`.`;
1988
+ if (strategy.startsWith("mixed:")) return `${prefix}: enumerated outliers plus the \`${strategy.slice(6)}\` pattern covering the rest.`;
1989
+ return `${prefix}.`;
1990
+ }
1991
+ const validatorNames = new Map(Object.entries(validators).map(([name, validator]) => [validator, name]));
1992
+ function isThemeGetter(value) {
1993
+ return "isThemeGetter" in value && value.isThemeGetter === true;
1994
+ }
1995
+ /**
1996
+ * Removes duplicate literals and validator references while keeping the first occurrence, since substituting a theme scale can repeat values the skeleton already defines statically (e.g. `text-base` exists both as skeleton literal and as `--text-base` theme value).
1997
+ */
1998
+ function dedupeValues(values) {
1999
+ const seen = /* @__PURE__ */ new Set();
2000
+ return values.filter((value) => {
2001
+ if (value.kind === "object") return true;
2002
+ const key = value.kind === "class" ? `c:${value.value}` : `v:${value.name}`;
2003
+ if (seen.has(key)) return false;
2004
+ seen.add(key);
2005
+ return true;
2006
+ });
2007
+ }
2008
+ function filterConflictMap(conflictMap, classGroups) {
2009
+ const filtered = /* @__PURE__ */ new Map();
2010
+ for (const [classGroupId, conflicts] of Object.entries(conflictMap)) {
2011
+ if (!classGroups.has(classGroupId) || !conflicts) continue;
2012
+ const existingConflicts = conflicts.filter((conflictId) => classGroups.has(conflictId));
2013
+ if (existingConflicts.length > 0) filtered.set(classGroupId, existingConflicts);
2014
+ }
2015
+ return filtered;
2016
+ }
2017
+ //#endregion
2018
+ //#region ../configurator/src/prune.ts
2019
+ /**
2020
+ * Shrinks a plan to the class names a project actually uses: groups and members survive only when needed by a used class's lookup, including intermediate base matchers for slash-modified classes — literals a used class names, nested prefix objects a used class descends into, and validators a used class's remaining tail satisfies.
2021
+ *
2022
+ * The pruned plan merges exactly like the full plan for every class list made of used classes, by construction: it is a subset of the full plan with tailwind-merge's lookup precedence intact. Classification of a used class records the lookups of tailwind-merge's own merge engine (prefix, variants, important marker, and postfix modifiers), so a class matched via a literal keeps that literal and a class matched via a validator keeps that validator, in the same group and at the same trie position; removing other members can never promote a previously losing candidate, because lookup tries deeper literal paths first and validators in definition order, and that order is preserved among the survivors. Members are only ever removed, never re-encoded, and validators are kept whenever any used tail satisfies them — over-keeping is always safe, under-keeping never is. Retained validators can also classify names outside the used set, so the result is not a strict allowlist. Only supplied candidates carry the equivalence guarantee; consumers with externally supplied, separately styled class names should keep the full config.
2023
+ *
2024
+ * Used classes are raw tokens as a scanner finds them (`hover:bg-red-500/50`, `tw:p-4!`, `-mt-2`), deduplicated here; anything that does not classify is ignored. Conflict maps are filtered to the kept groups, `scales` stay untouched (the emitter drops shared consts nothing references anymore), and the result records what happened in `report.pruning`. The input plan is not mutated.
2025
+ */
2026
+ function prunePlan(plan, usedClasses) {
2027
+ const config = materializeConfig(plan);
2028
+ const lookupClass = createClassGroupLookup(config);
2029
+ /** Every successful lookup needed by a used class, including a base group that enables a subsequent postfix lookup. Names contain only the part the class map matched, with a leading minus removed. */
2030
+ const matchedNamesByGroup = /* @__PURE__ */ new Map();
2031
+ const seen = /* @__PURE__ */ new Set();
2032
+ let classifiedClassCount = 0;
2033
+ for (const rawClassName of usedClasses) {
2034
+ const className = rawClassName.trim();
2035
+ if (className === "" || seen.has(className)) continue;
2036
+ seen.add(className);
2037
+ const matches = lookupClass(className);
2038
+ if (matches.length > 0) classifiedClassCount += 1;
2039
+ for (const match of matches) {
2040
+ const { classGroupId } = match;
2041
+ let names = matchedNamesByGroup.get(classGroupId);
2042
+ if (!names) {
2043
+ names = /* @__PURE__ */ new Set();
2044
+ matchedNamesByGroup.set(classGroupId, names);
2045
+ }
2046
+ names.add(lookupName(match.className));
2047
+ }
2048
+ }
2049
+ const classGroups = /* @__PURE__ */ new Map();
2050
+ const removedClassGroups = [];
2051
+ const unprunedClassGroups = [];
2052
+ for (const [classGroupId, items] of plan.classGroups) {
2053
+ const names = matchedNamesByGroup.get(classGroupId);
2054
+ if (!names) {
2055
+ removedClassGroups.push(classGroupId);
2056
+ continue;
2057
+ }
2058
+ const prunedItems = pruneItems(items, names);
2059
+ if (prunedItems.length === 0) {
2060
+ unprunedClassGroups.push(classGroupId);
2061
+ classGroups.set(classGroupId, items);
2062
+ } else classGroups.set(classGroupId, prunedItems);
2063
+ }
2064
+ return {
2065
+ ...plan,
2066
+ classGroups,
2067
+ conflictingClassGroups: filterConflictMap(Object.fromEntries(plan.conflictingClassGroups), classGroups),
2068
+ conflictingClassGroupModifiers: filterConflictMap(Object.fromEntries(plan.conflictingClassGroupModifiers), classGroups),
2069
+ postfixLookupClassGroups: plan.postfixLookupClassGroups.filter((classGroupId) => classGroups.has(classGroupId)),
2070
+ report: {
2071
+ ...plan.report,
2072
+ pruning: {
2073
+ usedClassCount: seen.size,
2074
+ classifiedClassCount,
2075
+ classGroupsBefore: plan.classGroups.size,
2076
+ classGroupsAfter: classGroups.size,
2077
+ removedClassGroups,
2078
+ unprunedClassGroups
2079
+ }
2080
+ }
2081
+ };
2082
+ }
2083
+ /**
2084
+ * Keeps the items of one class-group level that some name in `names` reaches, walking nested prefix objects the way tailwind-merge's class map does: an object entry's key is a dash-separated prefix, so a name descends into it with the key stripped (`red-500` enters `{ red: […] }` as `500`), a name equal to the key enters as the empty string (the `''` literal marks "the prefix alone is a class"), and validators see the remaining tail at their level. Literal strings must match the remaining name exactly.
2085
+ */
2086
+ function pruneItems(items, names) {
2087
+ const kept = [];
2088
+ for (const item of items) if (item.kind === "class") {
2089
+ if (names.has(item.value)) kept.push(item);
2090
+ } else if (item.kind === "validator") {
2091
+ const validator = validators[item.name];
2092
+ for (const name of names) if (validator(name)) {
2093
+ kept.push(item);
2094
+ break;
2095
+ }
2096
+ } else {
2097
+ const entries = [];
2098
+ for (const [key, entryItems] of item.entries) {
2099
+ const tails = /* @__PURE__ */ new Set();
2100
+ for (const name of names) if (name === key) tails.add("");
2101
+ else if (name.startsWith(`${key}-`)) tails.add(name.slice(key.length + 1));
2102
+ if (tails.size === 0) continue;
2103
+ const prunedEntryItems = pruneItems(entryItems, tails);
2104
+ if (prunedEntryItems.length > 0) entries.push([key, prunedEntryItems]);
2105
+ }
2106
+ if (entries.length > 0) kept.push({
2107
+ kind: "object",
2108
+ entries
2109
+ });
2110
+ }
2111
+ return kept;
2112
+ }
2113
+ /** The string the class map walks for a matched class: negative classes like `-mt-2` start their lookup after the empty first part, exactly as `getClassGroupId` skips it. */
2114
+ function lookupName(matchedClassName) {
2115
+ const parts = matchedClassName.split("-");
2116
+ return parts[0] === "" && parts.length > 1 ? parts.slice(1).join("-") : matchedClassName;
2117
+ }
2118
+ //#endregion
2119
+ //#region ../configurator/src/snapshot.ts
2120
+ /**
2121
+ * Captures the effective theme per namespace from a loaded design system.
2122
+ *
2123
+ * Reading the design system instead of parsing CSS text means defaults, `@import` chains, namespace resets (`--color-*: initial`), `@config`/`@plugin` contributions and value precedence are all Tailwind's responsibility — the snapshot only records the result. Values are intentionally not resolved: only which names exist matters for class classification.
2124
+ *
2125
+ * Variables outside the supported namespaces (e.g. `--z-index-*`) and in Tailwind's compat sub-namespaces (e.g. `--text-color-*`, which shares the `--text` prefix but feeds other utilities) are not captured here — the classes they create are picked up by the vanilla-diff augmentation pass instead, which classifies them empirically.
2126
+ */
2127
+ function snapshotTheme(designSystem, themeKeys) {
2128
+ const prefix = designSystem.theme.prefix ?? null;
2129
+ const scales = new Map(themeKeys.map((themeKey) => [themeKey, {
2130
+ names: [],
2131
+ hasBareValue: false
2132
+ }]));
2133
+ const keysByLength = [...themeKeys].sort((a, b) => b.length - a.length);
2134
+ for (const [variableName] of designSystem.theme.entries()) {
2135
+ if (!variableName.startsWith("--")) continue;
2136
+ let path = variableName.slice(2);
2137
+ if (prefix !== null) {
2138
+ if (!path.startsWith(`${prefix}-`)) continue;
2139
+ path = path.slice(prefix.length + 1);
2140
+ }
2141
+ const themeKey = keysByLength.find((key) => path === key || path.startsWith(`${key}-`));
2142
+ if (!themeKey || IGNORED_SUB_NAMESPACES[themeKey]?.some((subNamespace) => path === subNamespace || path.startsWith(`${subNamespace}-`))) continue;
2143
+ const scale = scales.get(themeKey);
2144
+ if (path === themeKey) {
2145
+ scale.hasBareValue = true;
2146
+ continue;
2147
+ }
2148
+ const name = path.slice(themeKey.length + 1);
2149
+ if (!name.includes("--")) scale.names.push(name);
2150
+ }
2151
+ return {
2152
+ prefix,
2153
+ scales
2154
+ };
2155
+ }
2156
+ /**
2157
+ * Tailwind's compat sub-namespaces, keyed by the theme key whose prefix they share: `--text-color-brand` is not a `text-*` font size, `--font-size-huge` not a `font-*` family. Mirrors the map Tailwind's theme resolution excludes when it reads a namespace (`ignoredThemeKeyMap` in tailwindcss's theme.ts) in full; sub-namespaces that are theme keys of their own (`text-shadow`, `font-weight`, `inset-shadow`, `inset-ring`) are listed as well so the exclusion does not depend on key length. Prefix matching alone would enumerate these names as scale members, and a name Tailwind never turns into a class would then evict a real one.
2158
+ */
2159
+ const IGNORED_SUB_NAMESPACES = {
2160
+ font: ["font-weight", "font-size"],
2161
+ inset: ["inset-shadow", "inset-ring"],
2162
+ text: [
2163
+ "text-color",
2164
+ "text-decoration-color",
2165
+ "text-decoration-thickness",
2166
+ "text-indent",
2167
+ "text-shadow",
2168
+ "text-underline-offset"
2169
+ ],
2170
+ "grid-column": ["grid-column-start", "grid-column-end"],
2171
+ "grid-row": ["grid-row-start", "grid-row-end"]
2172
+ };
2173
+ //#endregion
2174
+ //#region ../configurator/src/generate.ts
2175
+ /**
2176
+ * Generates a project-specific tailwind-merge setup from a Tailwind CSS v4 entrypoint.
2177
+ *
2178
+ * The design system is loaded through Tailwind's own APIs with defaults merged, overrides applied, and resets executed. Keep the installed compiler aligned with the version that builds the project's CSS. The default tailwind-merge config acts as the structural skeleton — class group semantics and conflict relationships — while every theme reference in it is replaced with exact values from the design system.
2179
+ *
2180
+ * Classes the theme creates outside the standard namespaces (compat sub-namespaces like `--text-color-*`, or namespaces without a theme key like `--z-index-*`) are found by diffing against a vanilla design system of the same Tailwind installation and classified empirically by their compiled CSS declarations, so no namespace mapping needs to be hand-maintained anywhere.
2181
+ */
2182
+ async function generate(options) {
2183
+ return generateFromDesignSystems(await loadDesignSystems({
2184
+ css: options.css,
2185
+ base: options.base,
2186
+ integration: options.integration
2187
+ }), options);
2188
+ }
2189
+ /**
2190
+ * The classification and emission half of `generate`, on design systems the caller has already loaded. Not part of the public API — `generate` is the entry point and loads the systems itself. The test fixtures use the split to hand one loaded project to both generation and their own conformance sweeps, sharing its class list and compiled-declaration caches instead of loading and compiling the same theme twice, and to reuse one vanilla system across every fixture in a worker.
2191
+ */
2192
+ async function generateFromDesignSystems({ project, vanilla }, options) {
2193
+ const themeKeys = Object.keys(getDefaultConfig().theme);
2194
+ const encoding = options.encoding ?? "compact";
2195
+ const plan = buildPlan({
2196
+ snapshot: snapshotTheme(project, themeKeys),
2197
+ cacheSize: options.cacheSize,
2198
+ encoding
2199
+ });
2200
+ const vanillaPlan = buildPlan({ snapshot: snapshotTheme(vanilla, themeKeys) });
2201
+ const vanillaClassGroupUtils = createClassGroupUtils(materializeConfig(vanillaPlan));
2202
+ const customUtilities = buildCustomUtilityPlan({
2203
+ project,
2204
+ vanilla,
2205
+ vanillaClassGroupId: vanillaClassGroupUtils.getClassGroupId,
2206
+ encoding
2207
+ });
2208
+ applyCustomUtilityPlan(plan, customUtilities);
2209
+ const projectClassGroupUtils = createClassGroupUtils(materializeConfig({
2210
+ ...plan,
2211
+ prefix: null
2212
+ }));
2213
+ const groupPrefixKeys = /* @__PURE__ */ new Map();
2214
+ for (const [groupId, items] of plan.classGroups) {
2215
+ const prefixes = items.flatMap((item) => item.kind === "object" ? item.entries.map(([key]) => key) : []);
2216
+ if (prefixes.length > 0) groupPrefixKeys.set(groupId, prefixes);
2217
+ }
2218
+ applyAugmentations(plan, buildAugmentations({
2219
+ project,
2220
+ vanilla,
2221
+ projectClassGroupId: projectClassGroupUtils.getClassGroupId,
2222
+ vanillaClassGroupId: vanillaClassGroupUtils.getClassGroupId,
2223
+ groupPrefixKeys,
2224
+ customGroupIds: new Set(customUtilities.groups.keys()),
2225
+ preservedCustomClasses: customUtilities.preservedClasses
2226
+ }));
2227
+ plan.postfixLookupClassGroups = [.../* @__PURE__ */ new Set([...plan.postfixLookupClassGroups, ...staticPostfixLookupGroups(project, plan)])];
2228
+ plan.orderSensitiveModifiers = [.../* @__PURE__ */ new Set([...plan.orderSensitiveModifiers, ...customOrderSensitiveModifiers(project, vanilla)])];
2229
+ return assembleResult(plan, options.prune ? await options.prune.usedClasses : null, {
2230
+ banner: await options.banner,
2231
+ format: options.format,
2232
+ importSource: options.importSource
2233
+ });
2234
+ }
2235
+ /** Everything after classification: prune to the used classes, if any, then emit and materialize. Kept apart so a result can be re-pruned for other classes without repeating the expensive part. */
2236
+ function assembleResult(plan, usedClasses, emitOptions) {
2237
+ const finalPlan = usedClasses ? prunePlan(plan, usedClasses) : plan;
2238
+ return {
2239
+ code: emitModule(finalPlan, emitOptions),
2240
+ config: materializeConfig(finalPlan),
2241
+ plan: finalPlan,
2242
+ prune: (nextUsedClasses) => assembleResult(plan, nextUsedClasses, emitOptions)
2243
+ };
2244
+ }
2245
+ /** Added or redefined variants that style another target are ordering barriers, like the default before/after/child modifiers. Compile their suggested spellings so theme-only media changes and ordinary state variants keep commuting; selector lists are conservative because they can mix targets. */
2246
+ function customOrderSensitiveModifiers(project, vanilla) {
2247
+ const defaults = new Map(vanilla.getVariants().map((variant) => [variant.name, variant]));
2248
+ const modifiers = /* @__PURE__ */ new Set();
2249
+ for (const variant of project.getVariants()) {
2250
+ const original = defaults.get(variant.name);
2251
+ if (!original || JSON.stringify(variant.selectors()) !== JSON.stringify(original.selectors())) modifiers.add(variant.name);
2252
+ for (const value of variant.values) if (!original || !original.values.includes(value) || JSON.stringify(variant.selectors({ value })) !== JSON.stringify(original.selectors({ value }))) modifiers.add(`${variant.name}${variant.hasDash ? "-" : ""}${value}`);
2253
+ }
2254
+ return [...modifiers].filter((modifier) => declaredDeclarations(project, `${modifier}:block`)?.some((entry) => entry.context !== "" || entry.scope.some((rule) => !rule.startsWith("@") && segment(rule, ",").length > 1)));
2255
+ }
2256
+ /** Static names can contain slashes too: badge/icon must reach its own group instead of inheriting badge's group. Use the final classifier after aliases and augmentation establish both owners, and the runtime parser for the exact postfix boundary. Pruning then retains both lookups. */
2257
+ function staticPostfixLookupGroups(project, plan) {
2258
+ const classNames = project.utilities.keys("static").filter((name) => name.includes("/"));
2259
+ if (classNames.length === 0) return [];
2260
+ const config = materializeConfig({
2261
+ ...plan,
2262
+ prefix: null
2263
+ });
2264
+ const parseClassName = createParseClassName(config);
2265
+ const { getClassGroupId } = createClassGroupUtils(config);
2266
+ const groups = /* @__PURE__ */ new Set();
2267
+ for (const className of classNames) {
2268
+ const { baseClassName, maybePostfixModifierPosition } = parseClassName(className);
2269
+ if (!maybePostfixModifierPosition) continue;
2270
+ const baseGroup = getClassGroupId(baseClassName.slice(0, maybePostfixModifierPosition));
2271
+ const fullGroup = getClassGroupId(baseClassName);
2272
+ if (baseGroup && fullGroup && baseGroup !== fullGroup) groups.add(baseGroup);
2273
+ }
2274
+ return [...groups];
2275
+ }
2276
+ //#endregion
2277
+ //#region ../configurator/src/scan.ts
2278
+ /**
2279
+ * Finds the class names a project can render, the way Tailwind does: the same candidate extractor (`@tailwindcss/oxide`'s scanner), over the same sources (`source(…)`, automatic detection, every `@source`), plus the `@source inline(…)` safelist. Pruning uses these candidates to preserve the full generated config's merge behavior for the same source usage.
2280
+ *
2281
+ * The sources come from Tailwind's own `compile()` (its `root` and `sources` results), which is also what reports the CSS graph's dependencies, so one compile serves both. What `compile()` does not expose is the safelist: `@source inline(…)` candidates stay private to its `build()`, so they are parsed from the entrypoint and every CSS dependency here, with Tailwind's brace-expansion rules. Negated inline sources exclude candidates even when they occur in files, exactly like Tailwind, which refuses to generate them.
2282
+ *
2283
+ * `@tailwindcss/oxide` is loaded lazily because it is a native module: a setup without a binary for its platform should fail only when scanning is requested, not when the configurator is imported.
2284
+ */
2285
+ async function createSourceScanner(options) {
2286
+ const dependencies = /* @__PURE__ */ new Set();
2287
+ const stylesheets = /* @__PURE__ */ new Set();
2288
+ const compiler = await compile(options.css, {
2289
+ base: options.base,
2290
+ customCssResolver: createStylesheetResolver(options.integration?.resolveCss, (file) => stylesheets.add(file), options.integration?.onDependency),
2291
+ customJsResolver: createModuleResolver(options.integration?.resolveJs, options.integration?.onDependency),
2292
+ onDependency: (dependencyPath) => {
2293
+ dependencies.add(dependencyPath);
2294
+ options.integration?.onDependency?.(dependencyPath);
2295
+ }
2296
+ });
2297
+ const sources = [...autoDetectSources(compiler.root, options.autoDetectBases), ...compiler.sources];
2298
+ const scansUtilities = (compiler.features & Features.Utilities) !== 0;
2299
+ const { safelist, exclusions } = await collectInlineSources(options.css, stylesheets);
2300
+ const scanner = new (await (loadScanner()))({ sources });
2301
+ return {
2302
+ dependencies,
2303
+ sources,
2304
+ scansUtilities,
2305
+ safelist,
2306
+ scan() {
2307
+ const classes = new Set(scansUtilities ? scanner.scan() : []);
2308
+ for (const className of safelist) classes.add(className);
2309
+ for (const className of exclusions) classes.delete(className);
2310
+ return {
2311
+ classes: [...classes],
2312
+ files: scanner.files,
2313
+ globs: scanner.globs
2314
+ };
2315
+ }
2316
+ };
2317
+ }
2318
+ /** Mirrors how `@tailwindcss/vite`, `@tailwindcss/postcss`, and the CLI turn the compiler's `root` into scanner sources: `source(none)` disables automatic detection, no `source(…)` means "everything under the base directory", and an explicit `source(…)` is the one directory to detect in. */
2319
+ function autoDetectSources(root, autoDetectBases) {
2320
+ if (root === "none") return [];
2321
+ if (root === null) return autoDetectBases.map((base) => ({
2322
+ base,
2323
+ pattern: "**/*",
2324
+ negated: false
2325
+ }));
2326
+ return [{
2327
+ ...root,
2328
+ negated: false
2329
+ }];
2330
+ }
2331
+ async function loadScanner() {
2332
+ try {
2333
+ return (await import("@tailwindcss/oxide")).Scanner;
2334
+ } catch (error) {
2335
+ throw new Error(`Scanning sources needs @tailwindcss/oxide with a binary for this platform (${process.platform}-${process.arch}): ${error instanceof Error ? error.message : String(error)}`);
2336
+ }
2337
+ }
2338
+ /**
2339
+ * Collects `@source inline(…)` safelist entries and `@source not inline(…)` exclusions from the entrypoint and the CSS files of its import graph. Tailwind's rules: the quoted argument is split into patterns at top-level whitespace, each pattern is brace-expanded, and the result is a plain candidate list — variants included (`{hover:,}underline`).
2340
+ */
2341
+ async function collectInlineSources(css, stylesheets) {
2342
+ const safelist = /* @__PURE__ */ new Set();
2343
+ const exclusions = /* @__PURE__ */ new Set();
2344
+ const cssTexts = [css];
2345
+ for (const file of stylesheets) {
2346
+ const content = await readFile(file, "utf8").catch(() => null);
2347
+ if (content !== null) cssTexts.push(content);
2348
+ }
2349
+ for (const text of cssTexts) for (const statement of cssStatements(text)) {
2350
+ const match = INLINE_SOURCE_RE.exec(statement);
2351
+ if (!match) continue;
2352
+ const target = match[1] ? exclusions : safelist;
2353
+ for (const pattern of segment(match[3], " ")) {
2354
+ if (pattern === "") continue;
2355
+ for (const candidate of expandBraces(pattern)) target.add(candidate);
2356
+ }
2357
+ }
2358
+ return {
2359
+ safelist: [...safelist],
2360
+ exclusions: [...exclusions]
2361
+ };
2362
+ }
2363
+ /** `@source inline("…")` and `@source not inline('…')`, argument in either quote style. Tailwind requires the quotes, so unquoted forms are not a thing. */
2364
+ const INLINE_SOURCE_RE = /^@source\s+(not\s+)?inline\(\s*(["'])((?:\\.|(?!\2)[^\\])*)\2\s*\)$/;
2365
+ const NUMERICAL_RANGE_RE = /^(-?\d+)\.\.(-?\d+)(?:\.\.(-?\d+))?$/;
2366
+ /**
2367
+ * Brace expansion with Tailwind's semantics (`packages/tailwindcss/src/utils/brace-expansion.ts`): comma lists (`{hover:,focus:}`), numeric ranges with optional step and direction (`{100..900..100}`, `{5..0}`), nesting (`bg-red-{50,{100..900..100},950}`), and cartesian products across several groups. Unbalanced braces and zero steps throw like upstream; decimal ranges stay literal.
2368
+ */
2369
+ function expandBraces(pattern) {
2370
+ const openIndex = pattern.indexOf("{");
2371
+ if (openIndex === -1) return [pattern];
2372
+ const prefix = pattern.slice(0, openIndex);
2373
+ const rest = pattern.slice(openIndex);
2374
+ let depth = 0;
2375
+ let closeIndex = -1;
2376
+ for (let index = 0; index < rest.length; index++) {
2377
+ const character = rest[index];
2378
+ if (character === "{") depth += 1;
2379
+ else if (character === "}") {
2380
+ depth -= 1;
2381
+ if (depth === 0) {
2382
+ closeIndex = index;
2383
+ break;
2384
+ }
2385
+ }
2386
+ }
2387
+ if (closeIndex === -1) throw new Error(`The pattern \`${pattern}\` is not balanced.`);
2388
+ const inside = rest.slice(1, closeIndex);
2389
+ const suffix = rest.slice(closeIndex + 1);
2390
+ const parts = (NUMERICAL_RANGE_RE.test(inside) ? expandSequence(inside) : segment(inside, ",")).flatMap((part) => expandBraces(part));
2391
+ const expanded = [];
2392
+ for (const suffixVariant of expandBraces(suffix)) for (const part of parts) expanded.push(prefix + part + suffixVariant);
2393
+ return expanded;
2394
+ }
2395
+ function expandSequence(sequence) {
2396
+ const match = NUMERICAL_RANGE_RE.exec(sequence);
2397
+ if (!match) return [sequence];
2398
+ const start = Number.parseInt(match[1], 10);
2399
+ const end = Number.parseInt(match[2], 10);
2400
+ let step = match[3] === void 0 ? start <= end ? 1 : -1 : Number.parseInt(match[3], 10);
2401
+ if (step === 0) throw new Error("Step cannot be zero in sequence expansion.");
2402
+ const increasing = start < end;
2403
+ if (increasing && step < 0) step = -step;
2404
+ if (!increasing && step > 0) step = -step;
2405
+ const values = [];
2406
+ for (let value = start; increasing ? value <= end : value >= end; value += step) values.push(String(value));
2407
+ return values;
2408
+ }
2409
+ //#endregion
2410
+ //#region ../plugin-core/src/discovery.ts
2411
+ /**
2412
+ * Finds the project's Tailwind CSS entrypoint by scanning `root` for CSS files with Tailwind root markers.
2413
+ *
2414
+ * The scan is eager and filesystem-based on purpose: bundler integrations of Tailwind discover roots lazily from the module graph, but a plugin's runtime module can be requested before any CSS has flowed through the pipeline, so the plugin must know the root up front.
2415
+ *
2416
+ * When several files carry markers, files transitively `@import`ed by another candidate are dropped — a root is the top of its own import graph (a multi-file theme's token and utility layers all contain `@theme`/`@utility` markers of their own). Follow import-only intermediates, including explicit paths outside the scan root, and visit each file once to bound shared dependencies and cycles. More than one root after that is a hard error asking for the plugin's `css` option; none found returns null and the caller falls back to default tailwind-merge behavior.
2417
+ */
2418
+ async function discoverCssRoot(root, { resolveCss, packageName } = {}) {
2419
+ const candidates = /* @__PURE__ */ new Map();
2420
+ const statementsByFile = /* @__PURE__ */ new Map();
2421
+ for (const file of await collectCssFiles(root)) {
2422
+ const identity = await realpath(file).catch(() => file);
2423
+ const statements = await readStatements(file);
2424
+ statementsByFile.set(identity, statements);
2425
+ if (statements?.some((statement) => ROOT_MARKER_RE.test(statement))) candidates.set(identity, file);
2426
+ }
2427
+ if (candidates.size === 0) return null;
2428
+ const importedByCandidate = /* @__PURE__ */ new Set();
2429
+ const resolveImport = createStylesheetResolver(resolveCss);
2430
+ const pending = [...candidates];
2431
+ const visited = /* @__PURE__ */ new Set();
2432
+ while (pending.length > 0) {
2433
+ const [identity, file] = pending.pop();
2434
+ if (visited.has(identity)) continue;
2435
+ visited.add(identity);
2436
+ if (!statementsByFile.has(identity)) statementsByFile.set(identity, await readStatements(file));
2437
+ const statements = statementsByFile.get(identity);
2438
+ if (statements === null || statements === void 0) continue;
2439
+ for (const statement of statements) {
2440
+ const match = CSS_IMPORT_RE.exec(statement);
2441
+ if (!match) continue;
2442
+ const target = await resolveImport(match[1], path.dirname(file)).catch(() => null);
2443
+ if (target !== null) {
2444
+ const targetIdentity = await realpath(target).catch(() => target);
2445
+ importedByCandidate.add(targetIdentity);
2446
+ pending.push([targetIdentity, target]);
2447
+ }
2448
+ }
2449
+ }
2450
+ const roots = [...candidates].filter(([identity]) => !importedByCandidate.has(identity)).map(([, file]) => file);
2451
+ if (roots.length === 1) return roots[0];
2452
+ const listed = (roots.length > 1 ? roots : [...candidates.values()]).map((file) => ` - ${path.relative(root, file)}`).join("\n");
2453
+ const prefix = packageName ? `[${packageName}] ` : "";
2454
+ throw new Error(`${prefix}Found multiple Tailwind CSS roots and cannot decide which one configures tailwind-merge:\n${listed}\nSet the plugin's \`css\` option to the entrypoint that defines your theme.`);
2455
+ }
2456
+ /** Matches active statements that can mark a Tailwind v4 root: the `tailwindcss` import (or one of its sub-imports) or Tailwind's own at-rules, `@source` included — an app entrypoint may add nothing but source directives on top of an imported shared theme, and the pruning scanner only sees the directives of the compiled root. Anchoring excludes directive-like text inside selectors and declaration values. */
2457
+ const ROOT_MARKER_RE = /^@import\s+(?:url\(\s*)?["']tailwindcss(?:\/[^"']*)?["']|^@(?:theme|config|plugin|tailwind|utility|custom-variant|source)(?:\s|$)/;
2458
+ const CSS_IMPORT_RE = /^@import\s+(?:url\(\s*)?["']([^"']+)["']/;
2459
+ /** Tokenize each stylesheet once so marker detection and import traversal share the same comment/string boundaries. Missing or unreadable files cannot contribute discovery candidates. */
2460
+ async function readStatements(file) {
2461
+ const content = await readFile(file, "utf-8").catch(() => null);
2462
+ return content === null ? null : [...cssStatements(content)];
2463
+ }
2464
+ /** Directories that never contain the project's own Tailwind entrypoint. Dot-directories (.git, .next, .svelte-kit, …) are skipped wholesale in the walk. */
2465
+ const IGNORED_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
2466
+ "node_modules",
2467
+ "dist",
2468
+ "build",
2469
+ "out",
2470
+ "coverage",
2471
+ "public"
2472
+ ]);
2473
+ const CSS_EXTENSIONS = /* @__PURE__ */ new Set([
2474
+ ".css",
2475
+ ".pcss",
2476
+ ".postcss"
2477
+ ]);
2478
+ /** Limit the eager scan to plain-CSS filenames. Following explicit imports is separate and does not impose an extension requirement. Symlinks are followed (a shared theme package linked into the app is a common layout), with directories visited once by real path so linked cycles terminate. */
2479
+ async function collectCssFiles(directory, visited = /* @__PURE__ */ new Set()) {
2480
+ const identity = await realpath(directory).catch(() => directory);
2481
+ if (visited.has(identity)) return [];
2482
+ visited.add(identity);
2483
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
2484
+ const files = [];
2485
+ await Promise.all(entries.map(async (entry) => {
2486
+ const entryPath = path.join(directory, entry.name);
2487
+ const target = entry.isSymbolicLink() ? await stat(entryPath).catch(() => null) : entry;
2488
+ if (target?.isDirectory()) {
2489
+ if (entry.name.startsWith(".") || IGNORED_DIRECTORY_NAMES.has(entry.name)) return;
2490
+ files.push(...await collectCssFiles(entryPath, visited));
2491
+ } else if (target?.isFile() && CSS_EXTENSIONS.has(path.extname(entry.name))) files.push(entryPath);
2492
+ }));
2493
+ return files;
2494
+ }
2495
+ //#endregion
2496
+ //#region ../plugin-core/src/generation.ts
2497
+ /**
2498
+ * Generates the runtime module from the project's Tailwind CSS entrypoint.
2499
+ *
2500
+ * Always supplies dependency hooks to the configurator's loaders, even without custom resolution. The same load that generates the config therefore discovers its dependencies; a separate compile for watching is unnecessary. Each dependency's modification time is taken when it is reported, as it is read: taken after generation, an edit landing mid-generation would be recorded as the baseline and a later refresh would keep the stale module.
2501
+ *
2502
+ * The source scan runs alongside generation rather than before it — its compile of the CSS graph and the source walk only have to finish before pruning, which runs last. A failing scan (no oxide binary for the platform, sources Tailwind can't resolve) never fails the generation: the module is generated with the full config and `pruningError` carries the reason — pruning is an optimization, and falling back preserves the full generated config's behavior.
2503
+ */
2504
+ async function generateRuntimeModule(options) {
2505
+ const dependencies = /* @__PURE__ */ new Set();
2506
+ const dependencyMtimes = /* @__PURE__ */ new Map();
2507
+ const recordDependency = (file) => {
2508
+ dependencies.add(file);
2509
+ if (!dependencyMtimes.has(file)) dependencyMtimes.set(file, readMtime(file));
2510
+ };
2511
+ recordDependency(options.cssPath);
2512
+ const css = await readFile(options.cssPath, "utf-8");
2513
+ const base = path.dirname(options.cssPath);
2514
+ const integration = {
2515
+ ...options.integration,
2516
+ onDependency(file) {
2517
+ recordDependency(file);
2518
+ options.integration?.onDependency?.(file);
2519
+ }
2520
+ };
2521
+ let pruningError;
2522
+ const scanning = options.prune ? createSourceScanner({
2523
+ css,
2524
+ base,
2525
+ autoDetectBases: options.prune.autoDetectBases,
2526
+ integration
2527
+ }).then((scanner) => ({
2528
+ scanner,
2529
+ scan: scanner.scan()
2530
+ })).catch((error) => {
2531
+ pruningError = error instanceof Error ? error : new Error(String(error));
2532
+ return null;
2533
+ }) : Promise.resolve(null);
2534
+ const sourceLine = `// Source: ${path.relative(options.root, options.cssPath) || options.cssPath} (served in-memory by ${options.packageName})`;
2535
+ const result = await generate({
2536
+ css,
2537
+ base,
2538
+ integration,
2539
+ cacheSize: options.cacheSize,
2540
+ encoding: options.encoding,
2541
+ format: "js",
2542
+ importSource: options.importSource,
2543
+ banner: scanning.then((scanned) => [sourceLine, ...scanned ? [PRUNED_LINE] : []].join("\n")),
2544
+ prune: options.prune ? { usedClasses: scanning.then((scanned) => scanned?.scan.classes ?? null) } : void 0
2545
+ });
2546
+ const scanned = await scanning;
2547
+ for (const file of scanned?.scanner.dependencies ?? []) recordDependency(file);
2548
+ return {
2549
+ ...assembleModule(result, options.importSource),
2550
+ dependencies,
2551
+ dependencyMtimes: new Map(await Promise.all([...dependencyMtimes].map(async ([file, mtime]) => [file, await mtime]))),
2552
+ cssPath: options.cssPath,
2553
+ importSource: options.importSource,
2554
+ pruning: scanned && result.plan.report.pruning ? pruningState(scanned.scanner, scanned.scan, result.plan.report.pruning) : void 0,
2555
+ pruningError
2556
+ };
2557
+ }
2558
+ /**
2559
+ * Re-prunes a module for a fresh scan of the same, unchanged CSS graph — a plugin's reaction to a source edit that changed the used classes. Pruning and emission run on the retained classification; nothing is loaded or classified again, and the dependency graph and its modification times carry over.
2560
+ */
2561
+ function pruneRuntimeModule(generated, pruning, scan) {
2562
+ const result = generated.result.prune(scan.classes);
2563
+ return {
2564
+ ...generated,
2565
+ ...assembleModule(result, generated.importSource),
2566
+ pruning: pruningState(pruning.scanner, scan, result.plan.report.pruning)
2567
+ };
2568
+ }
2569
+ function assembleModule(result, importSource) {
2570
+ const code = result.code + runtimeAppendix(importSource);
2571
+ return {
2572
+ code,
2573
+ hash: createHash("sha256").update(code).digest("hex"),
2574
+ result
2575
+ };
2576
+ }
2577
+ function pruningState(scanner, scan, report) {
2578
+ return {
2579
+ report,
2580
+ scanner,
2581
+ classesHash: hashClasses(scan.classes),
2582
+ files: scan.files,
2583
+ globs: scan.globs
2584
+ };
2585
+ }
2586
+ const PRUNED_LINE = "// Pruned to the classes found in the project's sources";
2587
+ /** Order-independent fingerprint of a scan's class names. */
2588
+ function hashClasses(classes) {
2589
+ return createHash("sha256").update([...classes].sort().join("\n")).digest("hex");
2590
+ }
2591
+ /** Whether any dependency's modification time differs from the recorded one — deleted files count as changed. */
2592
+ async function dependenciesChanged(generated) {
2593
+ const current = await readMtimes(generated.dependencies);
2594
+ for (const [file, mtime] of generated.dependencyMtimes) if (current.get(file) !== mtime) return true;
2595
+ return false;
2596
+ }
2597
+ async function readMtimes(files) {
2598
+ const mtimes = /* @__PURE__ */ new Map();
2599
+ await Promise.all([...files].map(async (file) => {
2600
+ mtimes.set(file, await readMtime(file));
2601
+ }));
2602
+ return mtimes;
2603
+ }
2604
+ /** A missing file reads as null, which differs from any real modification time. */
2605
+ function readMtime(file) {
2606
+ return stat(file).then((stats) => stats.mtimeMs, () => null);
2607
+ }
2608
+ /**
2609
+ * Appended to the configurator's emitted module (which exports `getConfig` and `twMerge` and already imports `createTailwindMerge`) so the served module's export surface mirrors each plugin's runtime fallback. Plain JavaScript on purpose — types live in the plugin's runtime file, which is what TypeScript resolves for the subpath.
2610
+ */
2611
+ function runtimeAppendix(importSource) {
2612
+ return `
2613
+ export { createTailwindMerge, mergeConfigs, twJoin, validators } from '${importSource}'
2614
+ import { mergeConfigs as mergeConfigsForExtend } from '${importSource}'
2615
+
2616
+ // Like tailwind-merge's extendTailwindMerge, but extending this project's generated config instead of the default one.
2617
+ export const extendTailwindMerge = (configExtension, ...createConfig) =>
2618
+ typeof configExtension === 'function'
2619
+ ? createTailwindMerge(getConfig, configExtension, ...createConfig)
2620
+ : createTailwindMerge(() => mergeConfigsForExtend(getConfig(), configExtension), ...createConfig)
2621
+ `;
2622
+ }
2623
+ /**
2624
+ * Served for the runtime subpath when generation has never succeeded (the CSS is broken from the start, or a `@plugin` package is missing): the same default-config surface as a plugin's runtime fallback file. Once a generation has succeeded, later failures keep serving the last good module instead.
2625
+ */
2626
+ function fallbackModuleCode(importSource) {
2627
+ return `
2628
+ export { createTailwindMerge, extendTailwindMerge, getDefaultConfig as getConfig, mergeConfigs, twJoin, twMerge, validators } from '${importSource}'
2629
+ `;
2630
+ }
2631
+ //#endregion
2632
+ //#region ../plugin-core/src/generation-session.ts
2633
+ /** Owns generation for one resolved plugin configuration. Failed attempts retain their dependency discoveries alongside the last good graph; only a success replaces serving state and dependencies. Dev updates, watch builds, and bundler loader re-runs all go through this lifecycle. */
2634
+ function createGenerationSession(options) {
2635
+ const { cssRoot, onGenerated, onError, ...generationOptions } = options;
2636
+ let current = null;
2637
+ let dependencies = /* @__PURE__ */ new Set();
2638
+ let generation = run();
2639
+ return {
2640
+ get current() {
2641
+ return current;
2642
+ },
2643
+ get dependencies() {
2644
+ return dependencies;
2645
+ },
2646
+ get generation() {
2647
+ return generation;
2648
+ },
2649
+ /** Schedule after the previous attempt, including a rejection. */
2650
+ regenerate() {
2651
+ generation = generation.catch(() => null).then(() => run());
2652
+ return generation;
2653
+ },
2654
+ /** Source-only changes re-prune the current module for the caller's fresh scan — no loading, no classification — behind any running attempt, like a regeneration. Without a pruned module there is nothing to re-prune. */
2655
+ reprune(scan) {
2656
+ generation = generation.catch(() => null).then(() => {
2657
+ if (current?.pruning) reprune(current, current.pruning, scan);
2658
+ return current;
2659
+ });
2660
+ return generation;
2661
+ },
2662
+ /** Reuses an unchanged success but always retries a failure, even before the first successful generation. A module holding the full config because its source scan failed is retried too, so pruning returns (and the warning repeats) once the cause is gone rather than after the next CSS edit. */
2663
+ refresh() {
2664
+ generation = generation.catch(() => null).then(async (previous) => {
2665
+ if (!previous || previous.pruningError || await dependenciesChanged(previous)) return run();
2666
+ if (previous.pruning) {
2667
+ const scan = previous.pruning.scanner.scan();
2668
+ if (hashClasses(scan.classes) !== previous.pruning.classesHash) reprune(previous, previous.pruning, scan);
2669
+ }
2670
+ return current;
2671
+ });
2672
+ return generation;
2673
+ },
2674
+ /** Programmatic restarts may reuse the plugin object. Drain its old attempt before clearing cached modules and replacing this session. */
2675
+ async dispose() {
2676
+ await generation.catch(() => {});
2677
+ clearRequireCache([...dependencies]);
2678
+ }
2679
+ };
2680
+ async function run() {
2681
+ const cssPath = await cssRoot;
2682
+ if (cssPath === null) return null;
2683
+ try {
2684
+ clearRequireCache([...dependencies]);
2685
+ trackDependency(cssPath);
2686
+ const generated = await generateRuntimeModule({
2687
+ ...generationOptions,
2688
+ cssPath,
2689
+ integration: {
2690
+ ...generationOptions.integration,
2691
+ onDependency: trackDependency
2692
+ }
2693
+ });
2694
+ current = generated;
2695
+ dependencies = new Set(generated.dependencies);
2696
+ onGenerated(generated);
2697
+ } catch (error) {
2698
+ onError(error, current);
2699
+ }
2700
+ return current;
2701
+ }
2702
+ function reprune(generated, pruning, scan) {
2703
+ current = pruneRuntimeModule(generated, pruning, scan);
2704
+ onGenerated(current);
2705
+ }
2706
+ /** Observe files, missing targets, and recovery directories before an attempt can fail. The caller registers its watcher immediately or drains the accumulated set when the watcher starts. */
2707
+ function trackDependency(file) {
2708
+ if (!dependencies.has(file)) {
2709
+ dependencies.add(file);
2710
+ generationOptions.integration?.onDependency?.(file);
2711
+ }
2712
+ }
2713
+ }
2714
+ //#endregion
2715
+ //#region src/loader.ts
2716
+ /**
2717
+ * Replaces the content of this package's runtime module with the module generated from the project's Tailwind CSS. Registered by `withTailwindMerge` (src/index.ts) with both Turbopack and webpack; runs once per compilation environment (server components, server rendering, browser), which the per-process project state below turns into one generation.
2718
+ *
2719
+ * Every run registers the CSS configuration graph — entrypoint, imported stylesheets, `@config`/`@plugin` modules, and the directories a missing import would appear in — as dependencies, so the bundler re-runs the loader when any of them changes and `refresh()` decides whether that change regenerates the module. A regeneration with identical output changes nothing downstream: both bundlers hash module content. With pruning active (builds, or dev with `prune.dev`), the scanned source directories are context dependencies too, so a class-usage change re-prunes the retained classification, and Turbopack's persistent cache cannot serve a module pruned for other sources.
2720
+ *
2721
+ * Failure policy mirrors the Vite plugin's: a build fails on a configuration-generation failure (the loader rejects, which fails the compilation), while the dev server logs the error and keeps serving the last good module — or the default fallback before the first success. A project without a Tailwind root warns once and serves the default fallback as well. A failed source scan is not a generation failure: the module holds the full config and the warning says so.
2722
+ */
2723
+ async function tailwindMergeLoader() {
2724
+ const options = this.getOptions();
2725
+ const project = projectState(this.rootContext, options);
2726
+ const generated = await project.next();
2727
+ registerDependencies(this, project, generated);
2728
+ return generated?.code ?? fallbackModuleCode(IMPORT_SOURCE);
2729
+ }
2730
+ /** One state per project and option set for the life of the loader process — Next.js runs webpack loaders in its own process and Turbopack loaders in a pool process it keeps alive across compilations, so the session's retained classification and dependency graph survive from one loader run to the next. */
2731
+ const projects = /* @__PURE__ */ new Map();
2732
+ function projectState(root, options) {
2733
+ const key = `${root}\0${JSON.stringify(options)}`;
2734
+ let state = projects.get(key);
2735
+ if (state) return state;
2736
+ const cssRoot = options.css !== void 0 ? Promise.resolve(path.resolve(root, options.css)) : discoverCssRoot(root, { packageName: PACKAGE_NAME }).then((found) => {
2737
+ if (found === null) console.warn(`[${PACKAGE_NAME}] No Tailwind CSS root found — serving default tailwind-merge behavior. Set the \`css\` option to your Tailwind entrypoint if it uses another extension or is outside the scanned directories.`);
2738
+ return found;
2739
+ });
2740
+ state = {
2741
+ root,
2742
+ session: createGenerationSession({
2743
+ cssRoot,
2744
+ root,
2745
+ packageName: PACKAGE_NAME,
2746
+ importSource: IMPORT_SOURCE,
2747
+ cacheSize: options.cacheSize,
2748
+ encoding: options.encoding,
2749
+ prune: options.prune ? { autoDetectBases: [root] } : void 0,
2750
+ onGenerated: (generated) => reportPruning(generated, options),
2751
+ onError(error, current) {
2752
+ if (options.mode === "build") throw error;
2753
+ console.error(`[${PACKAGE_NAME}] Generating the tailwind-merge config failed${current ? " — keeping the previous one" : ""}: ${error instanceof Error ? error.message : String(error)}`);
2754
+ }
2755
+ }),
2756
+ cssRoot: void 0,
2757
+ inFlight: void 0,
2758
+ started: false,
2759
+ next() {
2760
+ if (!this.inFlight) {
2761
+ const attempt = this.started ? this.session.refresh() : this.session.generation;
2762
+ this.started = true;
2763
+ this.inFlight = attempt.finally(() => {
2764
+ this.inFlight = void 0;
2765
+ });
2766
+ }
2767
+ return this.inFlight;
2768
+ }
2769
+ };
2770
+ cssRoot.then((found) => {
2771
+ state.cssRoot = found;
2772
+ }, () => {});
2773
+ projects.set(key, state);
2774
+ return state;
2775
+ }
2776
+ /**
2777
+ * Hands the session's dependency graph to the bundler: files as file dependencies, directories (the recovery directories reported for missing imports, and a missing explicit entrypoint's nearest existing parent) as context dependencies, and nonexistent paths as missing dependencies for webpack, which acts on them. With pruning, the scanned source bases join as context dependencies.
2778
+ */
2779
+ function registerDependencies(context, project, generated) {
2780
+ const dependencies = new Set(project.session.dependencies);
2781
+ if (project.cssRoot) dependencies.add(project.cssRoot);
2782
+ for (const dependency of dependencies) {
2783
+ const stats = statSync(dependency, { throwIfNoEntry: false });
2784
+ if (!stats) {
2785
+ context.addMissingDependency(dependency);
2786
+ if (dependency === project.cssRoot) context.addContextDependency(nearestExistingDirectory(dependency));
2787
+ } else if (stats.isDirectory()) context.addContextDependency(dependency);
2788
+ else context.addDependency(dependency);
2789
+ }
2790
+ for (const source of generated?.pruning?.scanner.sources ?? []) if (!source.negated) context.addContextDependency(source.base);
2791
+ }
2792
+ function nearestExistingDirectory(file) {
2793
+ let directory = path.dirname(file);
2794
+ while (!statSync(directory, { throwIfNoEntry: false })?.isDirectory()) {
2795
+ const parent = path.dirname(directory);
2796
+ if (parent === directory) break;
2797
+ directory = parent;
2798
+ }
2799
+ return directory;
2800
+ }
2801
+ /** One line per generation about pruning, so the behavior and its effect are visible where someone debugging a production-only merge difference would look; a failed scan is always reported, since the module then silently holds the full config. */
2802
+ function reportPruning(generated, options) {
2803
+ if (generated.pruningError) console.warn(`[${PACKAGE_NAME}] Could not scan your sources, serving the full tailwind-merge config instead: ${generated.pruningError.message}`);
2804
+ else if (generated.pruning && options.log) {
2805
+ const { classGroupsAfter, classGroupsBefore, classifiedClassCount } = generated.pruning.report;
2806
+ console.log(`[${PACKAGE_NAME}] Pruned the tailwind-merge config to ${classGroupsAfter} of ${classGroupsBefore} class groups from the ${classifiedClassCount} classes found in your sources`);
2807
+ }
2808
+ }
2809
+ const PACKAGE_NAME = "@tailwind-merge/next";
2810
+ /** The generated module replaces a real file inside this package, so it can import tailwind-merge the ordinary way: resolution starts in the package's own directory, where its dependency is installed under any package manager. */
2811
+ const IMPORT_SOURCE = "tailwind-merge";
2812
+ //#endregion
2813
+ export { tailwindMergeLoader as default };
2814
+
2815
+ //# sourceMappingURL=loader.mjs.map