praxis-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/_shared/diagnostics.d.ts +312 -0
- package/dist/_shared/diagnostics.js +360 -0
- package/dist/build-runtime-CJ_nQEaZ.js +5065 -0
- package/dist/codemod/index.d.ts +2 -0
- package/dist/codemod/index.js +176520 -0
- package/dist/contract/index.d.ts +677 -0
- package/dist/contract/index.js +341 -0
- package/dist/eslint/index.d.ts +90 -0
- package/dist/eslint/index.js +1047 -0
- package/dist/guards/index.d.ts +78 -0
- package/dist/guards/index.js +118 -0
- package/dist/html/index.d.ts +151 -0
- package/dist/html/index.js +1244 -0
- package/dist/index-BIBd_iPD.d.ts +951 -0
- package/dist/lit/index.d.ts +862 -0
- package/dist/lit/index.js +4893 -0
- package/dist/preact/index.d.ts +796 -0
- package/dist/preact/index.js +5043 -0
- package/dist/react/index.d.ts +28 -0
- package/dist/react/index.js +205 -0
- package/dist/react/legacy.d.ts +29 -0
- package/dist/react/legacy.js +80 -0
- package/dist/solid/index.d.ts +728 -0
- package/dist/solid/index.js +4821 -0
- package/dist/svelte/Polymorphic.svelte +190 -0
- package/dist/svelte/_polymorphic-runtime.d.ts +102 -0
- package/dist/svelte/_polymorphic-runtime.js +371 -0
- package/dist/svelte/index.d.ts +994 -0
- package/dist/svelte/index.js +4482 -0
- package/dist/tailwind/index.d.ts +197 -0
- package/dist/tailwind/index.js +767 -0
- package/dist/tailwind/safelist.css +20 -0
- package/dist/ts-plugin/index.cjs +166 -0
- package/dist/ts-plugin/index.d.cts +9 -0
- package/dist/utils/index.d.ts +19 -0
- package/dist/utils/index.js +21 -0
- package/dist/vite-plugin/index.d.ts +200 -0
- package/dist/vite-plugin/index.js +2106 -0
- package/dist/vue/index.d.ts +729 -0
- package/dist/vue/index.js +4945 -0
- package/dist/web/index.d.ts +832 -0
- package/dist/web/index.js +4868 -0
- package/package.json +258 -0
|
@@ -0,0 +1,2106 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { DiagnosticCategory, DiagnosticCode } from "../_shared/diagnostics.js";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
//#region ../../lib/foundation/src/iterate.ts
|
|
6
|
+
function find(iterable, callback) {
|
|
7
|
+
for (const value of iterable) {
|
|
8
|
+
const result = callback(value);
|
|
9
|
+
if (result != null) return result;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function some(iterable, predicate) {
|
|
14
|
+
for (const value of iterable) if (predicate(value)) return true;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
function every(iterable, predicate) {
|
|
18
|
+
let index = 0;
|
|
19
|
+
for (const value of iterable) if (!predicate(value, index++)) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function* filter(iterable, predicate) {
|
|
23
|
+
let index = 0;
|
|
24
|
+
for (const value of iterable) if (predicate(value, index++)) yield value;
|
|
25
|
+
}
|
|
26
|
+
function* map(iterable, callback) {
|
|
27
|
+
let index = 0;
|
|
28
|
+
for (const value of iterable) yield callback(value, index++);
|
|
29
|
+
}
|
|
30
|
+
function forEach(iterable, callback) {
|
|
31
|
+
let index = 0;
|
|
32
|
+
for (const value of iterable) callback(value, index++);
|
|
33
|
+
}
|
|
34
|
+
function reduce(iterable, initial, callback) {
|
|
35
|
+
let accumulator = initial;
|
|
36
|
+
let index = 0;
|
|
37
|
+
for (const value of iterable) accumulator = callback(accumulator, value, index++);
|
|
38
|
+
return accumulator;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Transforms an iterable into a Record.
|
|
42
|
+
*
|
|
43
|
+
* The callback returns a `[key, value]` tuple for each element. Returning
|
|
44
|
+
* `null` aborts the collection and causes `collect()` to return `null`.
|
|
45
|
+
*/
|
|
46
|
+
function collect(iterable, callback) {
|
|
47
|
+
const result = {};
|
|
48
|
+
let index = 0;
|
|
49
|
+
for (const value of iterable) {
|
|
50
|
+
const entry = callback(value, index++);
|
|
51
|
+
if (entry === null) return null;
|
|
52
|
+
result[entry[0]] = entry[1];
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function findLast(value, callback) {
|
|
57
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
58
|
+
const result = callback(value[index], index);
|
|
59
|
+
if (result != null) return result;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function* items(collection) {
|
|
64
|
+
for (let i = 0; i < collection.length; i++) {
|
|
65
|
+
const item = collection.item(i);
|
|
66
|
+
if (item !== null) yield item;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function nodeList(list) {
|
|
70
|
+
return { *[Symbol.iterator]() {
|
|
71
|
+
for (let i = 0; i < list.length; i++) {
|
|
72
|
+
const node = list.item(i);
|
|
73
|
+
if (node !== null) yield node;
|
|
74
|
+
}
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
77
|
+
function mapEntries(m) {
|
|
78
|
+
return m.entries();
|
|
79
|
+
}
|
|
80
|
+
function set(s) {
|
|
81
|
+
return s.values();
|
|
82
|
+
}
|
|
83
|
+
function hasOwn(object, key) {
|
|
84
|
+
return Object.hasOwn(object, key);
|
|
85
|
+
}
|
|
86
|
+
function* entries(object) {
|
|
87
|
+
for (const key in object) {
|
|
88
|
+
if (!hasOwn(object, key)) continue;
|
|
89
|
+
yield [key, object[key]];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function* keys(object) {
|
|
93
|
+
for (const [key] of entries(object)) yield key;
|
|
94
|
+
}
|
|
95
|
+
function* values(object) {
|
|
96
|
+
for (const [, value] of entries(object)) yield value;
|
|
97
|
+
}
|
|
98
|
+
function mapValues(object, callback) {
|
|
99
|
+
const result = {};
|
|
100
|
+
for (const [key, value] of entries(object)) result[key] = callback(value, key);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function forEachEntry(object, callback) {
|
|
104
|
+
for (const [key, value] of entries(object)) callback(key, value);
|
|
105
|
+
}
|
|
106
|
+
function forEachKey(object, callback) {
|
|
107
|
+
for (const key of keys(object)) callback(key);
|
|
108
|
+
}
|
|
109
|
+
function forEachValue(object, callback) {
|
|
110
|
+
for (const value of values(object)) callback(value);
|
|
111
|
+
}
|
|
112
|
+
function forEachSet(s, callback) {
|
|
113
|
+
for (const value of s) callback(value);
|
|
114
|
+
}
|
|
115
|
+
const iterate = Object.freeze({
|
|
116
|
+
entries,
|
|
117
|
+
filter,
|
|
118
|
+
find,
|
|
119
|
+
findLast,
|
|
120
|
+
forEach,
|
|
121
|
+
forEachEntry,
|
|
122
|
+
forEachKey,
|
|
123
|
+
forEachSet,
|
|
124
|
+
forEachValue,
|
|
125
|
+
items,
|
|
126
|
+
keys,
|
|
127
|
+
map,
|
|
128
|
+
mapEntries,
|
|
129
|
+
mapValues,
|
|
130
|
+
nodeList,
|
|
131
|
+
reduce,
|
|
132
|
+
collect,
|
|
133
|
+
set,
|
|
134
|
+
some,
|
|
135
|
+
every,
|
|
136
|
+
values
|
|
137
|
+
});
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region ../../lib/primitive/src/utils/lazy.ts
|
|
140
|
+
/**
|
|
141
|
+
* Lazily initializes a value. The factory is invoked exactly once on the first
|
|
142
|
+
* call, and every subsequent call returns the cached result, even if the
|
|
143
|
+
* result is `undefined`, `null`, `false`, `0`, or `''`.
|
|
144
|
+
*/
|
|
145
|
+
function lazy(factory) {
|
|
146
|
+
let initialized = false;
|
|
147
|
+
let value;
|
|
148
|
+
return () => {
|
|
149
|
+
if (!initialized) {
|
|
150
|
+
value = factory();
|
|
151
|
+
initialized = true;
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region ../../plugins/vite/src/ast.ts
|
|
158
|
+
/**
|
|
159
|
+
* Returns the initializer of a named property from an object literal.
|
|
160
|
+
*
|
|
161
|
+
* Only direct `PropertyAssignment` nodes are considered. Shorthand
|
|
162
|
+
* properties, spread assignments, getters, setters, and methods are ignored.
|
|
163
|
+
*
|
|
164
|
+
* @param obj Object literal to search.
|
|
165
|
+
* @param key Property name to locate.
|
|
166
|
+
* @returns The property's initializer expression, or `undefined` if the
|
|
167
|
+
* property is not present or is not a standard property assignment.
|
|
168
|
+
*/
|
|
169
|
+
function getProperty(obj, key) {
|
|
170
|
+
return iterate.find(obj.properties, (prop) => {
|
|
171
|
+
if (!ts.isPropertyAssignment(prop)) return null;
|
|
172
|
+
const { name } = prop;
|
|
173
|
+
if ((ts.isIdentifier(name) || ts.isStringLiteral(name)) && name.text === key) return prop.initializer;
|
|
174
|
+
return null;
|
|
175
|
+
}) ?? void 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Narrows a node to an ObjectLiteralExpression.
|
|
179
|
+
*
|
|
180
|
+
* This is a convenience type guard for optional AST nodes.
|
|
181
|
+
*
|
|
182
|
+
* @param node Node to inspect.
|
|
183
|
+
* @returns The same node when it is an object literal; otherwise `undefined`.
|
|
184
|
+
*/
|
|
185
|
+
function asObject(node) {
|
|
186
|
+
if (!node) return void 0;
|
|
187
|
+
return ts.isObjectLiteralExpression(node) ? node : void 0;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Narrows a node to an ArrayLiteralExpression.
|
|
191
|
+
*
|
|
192
|
+
* @param node Node to inspect.
|
|
193
|
+
* @returns The same node when it is an array literal; otherwise `undefined`.
|
|
194
|
+
*/
|
|
195
|
+
function asArray(node) {
|
|
196
|
+
if (!node) return void 0;
|
|
197
|
+
return ts.isArrayLiteralExpression(node) ? node : void 0;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extracts a non-negative integer from a numeric literal.
|
|
201
|
+
*
|
|
202
|
+
* Accepts only literal numeric values present in the AST.
|
|
203
|
+
* Expressions such as `1 + 2`, identifiers, and function calls
|
|
204
|
+
* are intentionally ignored.
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* This helper does not evaluate constant expressions. It only
|
|
208
|
+
* reads literal values already present in the syntax tree.
|
|
209
|
+
*
|
|
210
|
+
* @param node Node to inspect.
|
|
211
|
+
* @returns A non-negative integer, or `undefined` when the node
|
|
212
|
+
* is not a numeric literal or represents a negative value.
|
|
213
|
+
*/
|
|
214
|
+
function asNonNegativeInt(node) {
|
|
215
|
+
if (!node) return void 0;
|
|
216
|
+
if (ts.isNumericLiteral(node)) {
|
|
217
|
+
const n = Number(node.text);
|
|
218
|
+
return Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Extracts a boolean literal from an AST node.
|
|
223
|
+
*
|
|
224
|
+
* Only the `true` and `false` keyword nodes are recognized.
|
|
225
|
+
*
|
|
226
|
+
* @param node Node to inspect.
|
|
227
|
+
* @returns `true`, `false`, or `undefined`.
|
|
228
|
+
*/
|
|
229
|
+
function asBooleanLiteral(node) {
|
|
230
|
+
if (!node) return void 0;
|
|
231
|
+
switch (node.kind) {
|
|
232
|
+
case ts.SyntaxKind.TrueKeyword: return true;
|
|
233
|
+
case ts.SyntaxKind.FalseKeyword: return false;
|
|
234
|
+
default: return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Determines whether a call expression invokes one of the known
|
|
239
|
+
* Praxis Kit factory functions.
|
|
240
|
+
*
|
|
241
|
+
* Supports both direct identifiers and property access expressions.
|
|
242
|
+
*
|
|
243
|
+
* Examples:
|
|
244
|
+
*
|
|
245
|
+
* ```ts
|
|
246
|
+
* defineComponent(...)
|
|
247
|
+
* factory.defineComponent(...)
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* @param call Call expression to inspect.
|
|
251
|
+
* @param names Set of accepted factory names.
|
|
252
|
+
* @returns `true` when the call targets one of the supplied names.
|
|
253
|
+
*/
|
|
254
|
+
function isFactoryCall(call, names) {
|
|
255
|
+
const { expression } = call;
|
|
256
|
+
if (ts.isIdentifier(expression)) return names.has(expression.text);
|
|
257
|
+
if (ts.isPropertyAccessExpression(expression)) return names.has(expression.name.text);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Returns the first argument when it is an object literal.
|
|
262
|
+
*
|
|
263
|
+
* Useful for parsing APIs that accept an options object as their
|
|
264
|
+
* first parameter.
|
|
265
|
+
*
|
|
266
|
+
* @param call Call expression to inspect.
|
|
267
|
+
* @returns The first argument if it is an object literal;
|
|
268
|
+
* otherwise `undefined`.
|
|
269
|
+
*/
|
|
270
|
+
function firstObjectArg(call) {
|
|
271
|
+
const first = call.arguments[0];
|
|
272
|
+
return first && ts.isObjectLiteralExpression(first) ? first : void 0;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Performs a depth-first traversal of an AST subtree.
|
|
276
|
+
*
|
|
277
|
+
* The starting node is yielded first, followed by every
|
|
278
|
+
* descendant in lexical order.
|
|
279
|
+
*
|
|
280
|
+
* @remarks
|
|
281
|
+
* This is implemented as a generator, allowing consumers to
|
|
282
|
+
* iterate lazily using `for...of`.
|
|
283
|
+
*
|
|
284
|
+
* @param node Root node.
|
|
285
|
+
* @returns An iterator over every node in the subtree.
|
|
286
|
+
*/
|
|
287
|
+
function* walk(node) {
|
|
288
|
+
yield node;
|
|
289
|
+
const children = [];
|
|
290
|
+
ts.forEachChild(node, (child) => {
|
|
291
|
+
children.push(child);
|
|
292
|
+
});
|
|
293
|
+
for (const child of children) yield* walk(child);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Traverses an AST subtree depth-first, invoking a visitor for
|
|
297
|
+
* every node encountered.
|
|
298
|
+
*
|
|
299
|
+
* This is a callback-oriented alternative to {@link walk}.
|
|
300
|
+
*
|
|
301
|
+
* @param node Root node.
|
|
302
|
+
* @param visitor Function invoked for every visited node.
|
|
303
|
+
*/
|
|
304
|
+
function walkEach(node, visitor) {
|
|
305
|
+
for (const child of walk(node)) visitor(child);
|
|
306
|
+
}
|
|
307
|
+
const SCRIPT_KIND_BY_EXT = {
|
|
308
|
+
tsx: ts.ScriptKind.TSX,
|
|
309
|
+
jsx: ts.ScriptKind.JSX,
|
|
310
|
+
ts: ts.ScriptKind.TS,
|
|
311
|
+
mts: ts.ScriptKind.TS,
|
|
312
|
+
cts: ts.ScriptKind.TS,
|
|
313
|
+
js: ts.ScriptKind.JS,
|
|
314
|
+
mjs: ts.ScriptKind.JS,
|
|
315
|
+
cjs: ts.ScriptKind.JS
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Parses source text into a TypeScript SourceFile.
|
|
319
|
+
*
|
|
320
|
+
* The `ScriptKind` is derived from the filename extension so each source is
|
|
321
|
+
* parsed with its real syntax semantics — a `.ts` file's `<T>expr` is a type
|
|
322
|
+
* assertion, not a broken JSX element. Unknown extensions fall back to TSX (the
|
|
323
|
+
* most permissive). Latest language version, parent pointers enabled.
|
|
324
|
+
*
|
|
325
|
+
* @param filename Virtual filename used for diagnostics and to pick the ScriptKind.
|
|
326
|
+
* @param code Source code to parse.
|
|
327
|
+
* @returns Parsed SourceFile.
|
|
328
|
+
*/
|
|
329
|
+
function parseSource(filename, code) {
|
|
330
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
331
|
+
const scriptKind = SCRIPT_KIND_BY_EXT[ext] ?? ts.ScriptKind.TSX;
|
|
332
|
+
return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true, scriptKind);
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region ../../plugins/vite/src/class-extract.ts
|
|
336
|
+
/**
|
|
337
|
+
* Compile-time variant class precomputation.
|
|
338
|
+
*
|
|
339
|
+
* Extracts `styling.variants`, `styling.defaults`, and `styling.compounds` from
|
|
340
|
+
* factory call ASTs, enumerates all statically-known prop combinations, computes
|
|
341
|
+
* the variant class string for each, and injects the resulting map as
|
|
342
|
+
* `styling.precomputedClasses` directly into the source.
|
|
343
|
+
*
|
|
344
|
+
* At runtime, `VariantClassResolver` checks `precomputedClasses` before
|
|
345
|
+
* calling CVA — a plain object lookup replaces a CVA invocation + LRU cache
|
|
346
|
+
* write for every covered combination.
|
|
347
|
+
*
|
|
348
|
+
* **Skipped when any of the following are true:**
|
|
349
|
+
* - `styling.variants` is absent or contains non-literal values
|
|
350
|
+
* - `styling.compounds` contains non-literal conditions or class values
|
|
351
|
+
* - The total number of combinations exceeds MAX_COMBINATIONS
|
|
352
|
+
* - The styling object already has a `precomputedClasses` property
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* Before:
|
|
356
|
+
* ```ts
|
|
357
|
+
* styling: {
|
|
358
|
+
* variants: { size: { sm: 'text-sm', lg: 'text-lg' } },
|
|
359
|
+
* }
|
|
360
|
+
* ```
|
|
361
|
+
* After:
|
|
362
|
+
* ```ts
|
|
363
|
+
* styling: {
|
|
364
|
+
* variants: { size: { sm: 'text-sm', lg: 'text-lg' } },
|
|
365
|
+
* precomputedClasses: {
|
|
366
|
+
* '__none__:': '',
|
|
367
|
+
* '__none__:size:s:sm': 'text-sm',
|
|
368
|
+
* '__none__:size:s:lg': 'text-lg',
|
|
369
|
+
* },
|
|
370
|
+
* }
|
|
371
|
+
* ```
|
|
372
|
+
*/
|
|
373
|
+
const MAX_COMBINATIONS = 512;
|
|
374
|
+
/** Returns the text of a string literal node, or undefined. */
|
|
375
|
+
function asString(node) {
|
|
376
|
+
return node && ts.isStringLiteral(node) ? node.text : void 0;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Returns the text of a string literal, the texts of all elements of an array literal
|
|
380
|
+
* (every element must be a string literal), or undefined for any other shape.
|
|
381
|
+
*/
|
|
382
|
+
function asStringOrStringArray(node) {
|
|
383
|
+
if (!node) return void 0;
|
|
384
|
+
if (ts.isStringLiteral(node)) return node.text;
|
|
385
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
386
|
+
const items = [];
|
|
387
|
+
if (!iterate.every(node.elements, (elem) => {
|
|
388
|
+
if (!ts.isStringLiteral(elem)) return false;
|
|
389
|
+
items.push(elem.text);
|
|
390
|
+
return true;
|
|
391
|
+
})) return void 0;
|
|
392
|
+
return items;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** Returns the name text of a property assignment with an identifier or string literal key, or undefined. */
|
|
396
|
+
function propKey(prop) {
|
|
397
|
+
if (!ts.isPropertyAssignment(prop)) return void 0;
|
|
398
|
+
const n = prop.name;
|
|
399
|
+
return ts.isIdentifier(n) || ts.isStringLiteral(n) ? n.text : void 0;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Extracts `styling.variants` into a nested `VariantMap`.
|
|
403
|
+
* Returns null when any variant key or value entry is non-literal (bail — can't enumerate).
|
|
404
|
+
*/
|
|
405
|
+
function extractVariantMap$1(stylingObj) {
|
|
406
|
+
const variantsObj = asObject(getProperty(stylingObj, "variants"));
|
|
407
|
+
if (!variantsObj) return null;
|
|
408
|
+
return iterate.collect(variantsObj.properties, (prop) => {
|
|
409
|
+
const key = propKey(prop);
|
|
410
|
+
if (!key || !ts.isPropertyAssignment(prop)) return null;
|
|
411
|
+
const valuesObj = asObject(prop.initializer);
|
|
412
|
+
if (!valuesObj) return null;
|
|
413
|
+
const values = iterate.collect(valuesObj.properties, (vp) => {
|
|
414
|
+
const vk = propKey(vp);
|
|
415
|
+
if (!vk || !ts.isPropertyAssignment(vp)) return null;
|
|
416
|
+
const value = asStringOrStringArray(vp.initializer);
|
|
417
|
+
if (value === void 0) return null;
|
|
418
|
+
return [vk, value];
|
|
419
|
+
});
|
|
420
|
+
if (!values) return null;
|
|
421
|
+
return [key, values];
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/** Extracts `styling.defaults` as a `Record<variantKey, defaultValue>`. Returns `{}` when absent or non-literal. */
|
|
425
|
+
function extractDefaults(stylingObj) {
|
|
426
|
+
const defaultsObj = asObject(getProperty(stylingObj, "defaults"));
|
|
427
|
+
if (!defaultsObj) return {};
|
|
428
|
+
return iterate.collect(defaultsObj.properties, (prop) => {
|
|
429
|
+
const key = propKey(prop);
|
|
430
|
+
if (!key || !ts.isPropertyAssignment(prop)) return null;
|
|
431
|
+
const value = asString(prop.initializer);
|
|
432
|
+
if (value === void 0) return null;
|
|
433
|
+
return [key, value];
|
|
434
|
+
}) ?? {};
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Extracts `styling.compounds` as a typed array.
|
|
438
|
+
* Returns null when any entry has a non-literal condition value or class — signals bail to the caller.
|
|
439
|
+
*/
|
|
440
|
+
function extractCompounds(stylingObj) {
|
|
441
|
+
const compoundsArr = asArray(getProperty(stylingObj, "compounds"));
|
|
442
|
+
if (!compoundsArr) return [];
|
|
443
|
+
const result = [];
|
|
444
|
+
return iterate.every(compoundsArr.elements, (elem) => {
|
|
445
|
+
const obj = asObject(elem);
|
|
446
|
+
if (!obj) return false;
|
|
447
|
+
const cls = asStringOrStringArray(getProperty(obj, "class"));
|
|
448
|
+
if (cls === void 0) return false;
|
|
449
|
+
const conditions = {};
|
|
450
|
+
if (!iterate.every(obj.properties, (cp) => {
|
|
451
|
+
const key = propKey(cp);
|
|
452
|
+
if (!key || !ts.isPropertyAssignment(cp)) return false;
|
|
453
|
+
if (key === "class") return true;
|
|
454
|
+
const v = asStringOrStringArray(cp.initializer);
|
|
455
|
+
if (v === void 0) return false;
|
|
456
|
+
conditions[key] = v;
|
|
457
|
+
return true;
|
|
458
|
+
})) return false;
|
|
459
|
+
result.push({
|
|
460
|
+
conditions,
|
|
461
|
+
cls
|
|
462
|
+
});
|
|
463
|
+
return true;
|
|
464
|
+
}) ? result : null;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Enumerates every subset of variant props (each dimension either absent or set
|
|
468
|
+
* to one of its declared values). Returns null if total would exceed MAX_COMBINATIONS.
|
|
469
|
+
*/
|
|
470
|
+
function enumerateCombinations(variantMap) {
|
|
471
|
+
const keys = Object.keys(variantMap);
|
|
472
|
+
if (keys.length === 0) return [{}];
|
|
473
|
+
let total = 1;
|
|
474
|
+
if (!iterate.every(keys, (key) => {
|
|
475
|
+
total *= Object.keys(variantMap[key]).length + 1;
|
|
476
|
+
return total <= MAX_COMBINATIONS;
|
|
477
|
+
})) return null;
|
|
478
|
+
function enumerateRecursive(remaining) {
|
|
479
|
+
if (remaining.length === 0) return [{}];
|
|
480
|
+
const first = remaining[0];
|
|
481
|
+
const restCombos = enumerateRecursive(remaining.slice(1));
|
|
482
|
+
const valueKeys = Object.keys(variantMap[first]);
|
|
483
|
+
const out = [];
|
|
484
|
+
iterate.forEach(restCombos, (combo) => {
|
|
485
|
+
out.push(combo);
|
|
486
|
+
iterate.forEach(valueKeys, (v) => {
|
|
487
|
+
out.push({
|
|
488
|
+
[first]: v,
|
|
489
|
+
...combo
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
return enumerateRecursive(keys);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Builds the VariantClassResolver cache key for a given explicit prop combination.
|
|
499
|
+
* Absent variant dimensions are excluded from the key (defaults apply in compute).
|
|
500
|
+
*/
|
|
501
|
+
function buildCacheKey(props) {
|
|
502
|
+
return `__none__:${Object.keys(props).sort().map((k) => `${k}:s:${props[k]}`).join("|")}`;
|
|
503
|
+
}
|
|
504
|
+
/** Computes the final class string for a given explicit prop combination against a resolved styling config. */
|
|
505
|
+
function computeClasses(config, props) {
|
|
506
|
+
const { variantMap, defaults, compounds } = config;
|
|
507
|
+
const effective = {
|
|
508
|
+
...defaults,
|
|
509
|
+
...props
|
|
510
|
+
};
|
|
511
|
+
const classes = [];
|
|
512
|
+
iterate.forEachEntry(variantMap, (key, values) => {
|
|
513
|
+
const v = effective[key];
|
|
514
|
+
if (v === void 0) return;
|
|
515
|
+
const cls = values[v];
|
|
516
|
+
if (cls === void 0) return;
|
|
517
|
+
if (Array.isArray(cls)) classes.push(...cls);
|
|
518
|
+
else classes.push(cls);
|
|
519
|
+
});
|
|
520
|
+
iterate.forEach(compounds, ({ conditions, cls }) => {
|
|
521
|
+
if (iterate.every(iterate.entries(conditions), ([key, cond]) => {
|
|
522
|
+
const value = effective[key];
|
|
523
|
+
return Array.isArray(cond) ? value !== void 0 && cond.includes(value) : value === cond;
|
|
524
|
+
})) {
|
|
525
|
+
if (Array.isArray(cls)) classes.push(...cls);
|
|
526
|
+
else classes.push(cls);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
return classes.filter(Boolean).join(" ");
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Builds a precomputed class map for the given styling object literal.
|
|
533
|
+
*
|
|
534
|
+
* Returns null when static extraction is not possible (non-literal values,
|
|
535
|
+
* no variants, or combination count exceeds MAX_COMBINATIONS).
|
|
536
|
+
*/
|
|
537
|
+
function buildPrecomputedClasses(stylingObj) {
|
|
538
|
+
if (getProperty(stylingObj, "precomputedClasses") !== void 0) return null;
|
|
539
|
+
const variantMap = extractVariantMap$1(stylingObj);
|
|
540
|
+
if (!variantMap || Object.keys(variantMap).length === 0) return null;
|
|
541
|
+
const defaults = extractDefaults(stylingObj);
|
|
542
|
+
const compounds = extractCompounds(stylingObj);
|
|
543
|
+
if (!compounds) return null;
|
|
544
|
+
const combos = enumerateCombinations(variantMap);
|
|
545
|
+
if (!combos) return null;
|
|
546
|
+
const config = {
|
|
547
|
+
variantMap,
|
|
548
|
+
defaults,
|
|
549
|
+
compounds
|
|
550
|
+
};
|
|
551
|
+
return iterate.collect(combos, (combo) => [buildCacheKey(combo), computeClasses(config, combo)]);
|
|
552
|
+
}
|
|
553
|
+
/** Returns a TS transformer that injects a `precomputedClasses` property into each eligible factory call's `styling` object. */
|
|
554
|
+
function createClassExtractTransformer(factory, calleeNames, onInjected) {
|
|
555
|
+
return (context) => {
|
|
556
|
+
function visit(node) {
|
|
557
|
+
if (!ts.isCallExpression(node)) return ts.visitEachChild(node, visit, context);
|
|
558
|
+
if (!isFactoryCall(node, calleeNames)) return ts.visitEachChild(node, visit, context);
|
|
559
|
+
const arg = firstObjectArg(node);
|
|
560
|
+
if (!arg) return node;
|
|
561
|
+
const stylingObj = asObject(getProperty(arg, "styling"));
|
|
562
|
+
if (!stylingObj) return ts.visitEachChild(node, visit, context);
|
|
563
|
+
const map = buildPrecomputedClasses(stylingObj);
|
|
564
|
+
if (!map) return ts.visitEachChild(node, visit, context);
|
|
565
|
+
onInjected();
|
|
566
|
+
const mapProps = Array.from(iterate.map(iterate.entries(map), ([key, value]) => factory.createPropertyAssignment(factory.createStringLiteral(key), factory.createStringLiteral(value))));
|
|
567
|
+
const mapLiteral = factory.createObjectLiteralExpression(mapProps, true);
|
|
568
|
+
const precomputedProp = factory.createPropertyAssignment(factory.createIdentifier("precomputedClasses"), mapLiteral);
|
|
569
|
+
const newStylingObj = factory.createObjectLiteralExpression([...stylingObj.properties, precomputedProp], true);
|
|
570
|
+
const newArgProps = arg.properties.map((p) => {
|
|
571
|
+
if (ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "styling") return factory.createPropertyAssignment(p.name, newStylingObj);
|
|
572
|
+
return p;
|
|
573
|
+
});
|
|
574
|
+
const newArg = factory.createObjectLiteralExpression(newArgProps, true);
|
|
575
|
+
return factory.createCallExpression(node.expression, node.typeArguments, [newArg, ...node.arguments.slice(1)]);
|
|
576
|
+
}
|
|
577
|
+
return (sourceFile) => ts.visitEachChild(sourceFile, visit, context);
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Injects precomputed variant class maps into all factory calls in the given
|
|
582
|
+
* source file that have fully-static `styling.variants` configurations.
|
|
583
|
+
*
|
|
584
|
+
* Returns null when no factory calls with injectable variants are found.
|
|
585
|
+
*/
|
|
586
|
+
function injectPrecomputedClasses(source, calleeNames) {
|
|
587
|
+
if (!iterate.some(walk(source), (node) => ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) && node.name.text === "variants")) return null;
|
|
588
|
+
let didInject = false;
|
|
589
|
+
const result = ts.transform(source, [createClassExtractTransformer(ts.factory, calleeNames, () => {
|
|
590
|
+
didInject = true;
|
|
591
|
+
})], { target: ts.ScriptTarget.Latest });
|
|
592
|
+
if (!didInject) {
|
|
593
|
+
result.dispose();
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
const output = ts.createPrinter({
|
|
597
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
598
|
+
removeComments: false
|
|
599
|
+
}).printFile(result.transformed[0]);
|
|
600
|
+
result.dispose();
|
|
601
|
+
return output;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region ../../plugins/vite/src/collect.ts
|
|
605
|
+
/**
|
|
606
|
+
* Extracts a StaticBound from one element of the enforcement.children array,
|
|
607
|
+
* if and only if the element is an object literal with a statically-readable
|
|
608
|
+
* cardinality. Returns undefined for elements that use dynamic values.
|
|
609
|
+
*/
|
|
610
|
+
function extractBound(element) {
|
|
611
|
+
const obj = asObject(element);
|
|
612
|
+
if (!obj) return void 0;
|
|
613
|
+
const cardObj = asObject(getProperty(obj, "cardinality"));
|
|
614
|
+
let cardinality;
|
|
615
|
+
if (cardObj) {
|
|
616
|
+
const minNode = getProperty(cardObj, "min");
|
|
617
|
+
const maxNode = getProperty(cardObj, "max");
|
|
618
|
+
const min = asNonNegativeInt(minNode) ?? 0;
|
|
619
|
+
const max = asNonNegativeInt(maxNode);
|
|
620
|
+
if (min === 0 && max === void 0) cardinality = { kind: "unbounded" };
|
|
621
|
+
else cardinality = {
|
|
622
|
+
kind: "bounded",
|
|
623
|
+
min,
|
|
624
|
+
max: max ?? Infinity
|
|
625
|
+
};
|
|
626
|
+
} else cardinality = { kind: "unbounded" };
|
|
627
|
+
const positionNode = getProperty(obj, "position");
|
|
628
|
+
let position = "any";
|
|
629
|
+
if (positionNode && ts.isStringLiteral(positionNode)) {
|
|
630
|
+
const p = positionNode.text;
|
|
631
|
+
if (p === "first" || p === "last" || p === "any") position = p;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
cardinality,
|
|
635
|
+
position
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/** Returns the lower bound of a Cardinality as an integer (unbounded → 0). */
|
|
639
|
+
function cardinalityMin(c) {
|
|
640
|
+
return c.kind === "bounded" ? c.min : 0;
|
|
641
|
+
}
|
|
642
|
+
/** Returns the upper bound of a Cardinality (unbounded → Infinity). */
|
|
643
|
+
function cardinalityMax(c) {
|
|
644
|
+
return c.kind === "bounded" ? c.max : Infinity;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Walks a SourceFile and returns one ComponentConstraint for each
|
|
648
|
+
* `const X = createPolymorphicComponent({ enforcement: { children: [...] } })`
|
|
649
|
+
* where at least one children rule has a statically-extractable cardinality.
|
|
650
|
+
*
|
|
651
|
+
* Only handles simple `const X = factory(...)` declarations — named exports,
|
|
652
|
+
* default exports, and destructured patterns are not collected. Cross-file
|
|
653
|
+
* constraint collection (factory defined in module A, consumed in module B)
|
|
654
|
+
* is handled separately via the registry + import resolver path in the plugin.
|
|
655
|
+
* Dynamic cardinality values (computed variables, spreads) are silently skipped;
|
|
656
|
+
* extending to data-flow patterns is deferred.
|
|
657
|
+
*/
|
|
658
|
+
function processVariableStatement(node, calleeNames, out) {
|
|
659
|
+
iterate.forEach(node.declarationList.declarations, (decl) => {
|
|
660
|
+
const { initializer, name } = decl;
|
|
661
|
+
if (!initializer || !ts.isCallExpression(initializer)) return;
|
|
662
|
+
if (!isFactoryCall(initializer, calleeNames)) return;
|
|
663
|
+
const arg = firstObjectArg(initializer);
|
|
664
|
+
if (!arg) return;
|
|
665
|
+
const tagNode = getProperty(arg, "tag");
|
|
666
|
+
const defaultTag = tagNode && ts.isStringLiteral(tagNode) ? tagNode.text : void 0;
|
|
667
|
+
const enfObj = asObject(getProperty(arg, "enforcement"));
|
|
668
|
+
const ariaArr = asArray(enfObj ? getProperty(enfObj, "aria") : void 0);
|
|
669
|
+
const hasAriaRules = ariaArr !== void 0 && ariaArr.elements.length > 0;
|
|
670
|
+
const exclusiveChildren = asBooleanLiteral(enfObj ? getProperty(enfObj, "exclusiveChildren") : void 0) ?? false;
|
|
671
|
+
const childrenArr = asArray(enfObj ? getProperty(enfObj, "children") : void 0);
|
|
672
|
+
const rules = [];
|
|
673
|
+
if (childrenArr) iterate.forEach(childrenArr.elements, (element) => {
|
|
674
|
+
const bound = extractBound(element);
|
|
675
|
+
if (bound) rules.push(bound);
|
|
676
|
+
});
|
|
677
|
+
if (rules.length === 0 && !hasAriaRules && !defaultTag && !exclusiveChildren) return;
|
|
678
|
+
let totalMin = 0;
|
|
679
|
+
let totalMax = 0;
|
|
680
|
+
iterate.forEach(rules, (rule) => {
|
|
681
|
+
totalMin += cardinalityMin(rule.cardinality);
|
|
682
|
+
const max = cardinalityMax(rule.cardinality);
|
|
683
|
+
totalMax = totalMax === Infinity || max === Infinity ? Infinity : totalMax + max;
|
|
684
|
+
});
|
|
685
|
+
const componentName = ts.isIdentifier(name) ? name.text : void 0;
|
|
686
|
+
if (!componentName) return;
|
|
687
|
+
out.push({
|
|
688
|
+
name: componentName,
|
|
689
|
+
rules,
|
|
690
|
+
totalMin,
|
|
691
|
+
totalMax,
|
|
692
|
+
...defaultTag !== void 0 && { defaultTag },
|
|
693
|
+
hasAriaRules,
|
|
694
|
+
exclusiveChildren
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Single-pass variant of collectConstraints + extractImportSpecifiers.
|
|
700
|
+
* Collects factory constraints and named import bindings in one AST walk
|
|
701
|
+
* instead of two, for use in the hot Vite transform hook.
|
|
702
|
+
*/
|
|
703
|
+
function collectFileDeclarations(source, calleeNames) {
|
|
704
|
+
const constraints = [];
|
|
705
|
+
const importSpecifiers = /* @__PURE__ */ new Map();
|
|
706
|
+
walkEach(source, (node) => {
|
|
707
|
+
if (ts.isVariableStatement(node)) processVariableStatement(node, calleeNames, constraints);
|
|
708
|
+
else if (ts.isImportDeclaration(node)) {
|
|
709
|
+
const spec = node.moduleSpecifier;
|
|
710
|
+
if (!ts.isStringLiteral(spec)) return;
|
|
711
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
712
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) return;
|
|
713
|
+
const specifier = spec.text;
|
|
714
|
+
iterate.forEach(namedBindings.elements, ({ isTypeOnly, name, propertyName }) => {
|
|
715
|
+
if (isTypeOnly) return;
|
|
716
|
+
const localName = name.text;
|
|
717
|
+
const importedName = propertyName?.text ?? localName;
|
|
718
|
+
importSpecifiers.set(localName, {
|
|
719
|
+
importedName,
|
|
720
|
+
specifier
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
return {
|
|
726
|
+
constraints,
|
|
727
|
+
importSpecifiers
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region ../../plugins/vite/src/compound-prune.ts
|
|
732
|
+
/**
|
|
733
|
+
* Compile-time dead compound variant pruning.
|
|
734
|
+
*
|
|
735
|
+
* Removes entries from `styling.compounds` whose conditions can never fire:
|
|
736
|
+
* - condition key is not in `styling.variants`
|
|
737
|
+
* - condition value (string) is not a valid value for that variant key
|
|
738
|
+
* - condition value (array) has no element that is a valid value
|
|
739
|
+
*
|
|
740
|
+
* Only entries whose conditions are all statically evaluable (string/array
|
|
741
|
+
* literals throughout) are candidates for pruning — dynamic conditions (variable
|
|
742
|
+
* references, computed values) are left unchanged.
|
|
743
|
+
*
|
|
744
|
+
* Returns null if no factory calls with `styling.compounds` are found, or if
|
|
745
|
+
* every compound in every factory call passes the validity check.
|
|
746
|
+
*/
|
|
747
|
+
/** Builds a variant map from a `styling.variants` object literal. */
|
|
748
|
+
function extractVariantMap(stylingObj) {
|
|
749
|
+
const result = /* @__PURE__ */ new Map();
|
|
750
|
+
const variantsObj = asObject(getProperty(stylingObj, "variants"));
|
|
751
|
+
if (!variantsObj) return result;
|
|
752
|
+
iterate.forEach(variantsObj.properties, (prop) => {
|
|
753
|
+
if (!ts.isPropertyAssignment(prop)) return;
|
|
754
|
+
const { name } = prop;
|
|
755
|
+
const key = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : void 0;
|
|
756
|
+
if (!key) return;
|
|
757
|
+
const valuesObj = asObject(prop.initializer);
|
|
758
|
+
if (!valuesObj) return;
|
|
759
|
+
const valid = /* @__PURE__ */ new Set();
|
|
760
|
+
iterate.forEach(valuesObj.properties, (vp) => {
|
|
761
|
+
if (!ts.isPropertyAssignment(vp)) return;
|
|
762
|
+
const { name } = vp;
|
|
763
|
+
const vk = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : void 0;
|
|
764
|
+
if (vk) valid.add(vk);
|
|
765
|
+
});
|
|
766
|
+
result.set(key, valid);
|
|
767
|
+
});
|
|
768
|
+
return result;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Returns true if the compound object literal entry is dead given `variantMap`.
|
|
772
|
+
*
|
|
773
|
+
* Conservative: returns false (not dead) whenever a condition cannot be fully
|
|
774
|
+
* evaluated as string/array literals.
|
|
775
|
+
*/
|
|
776
|
+
function isDeadCompound(entry, variantMap) {
|
|
777
|
+
return iterate.find(entry.properties, (prop) => {
|
|
778
|
+
if (!ts.isPropertyAssignment(prop)) return null;
|
|
779
|
+
const { name } = prop;
|
|
780
|
+
const key = ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : void 0;
|
|
781
|
+
if (!key || key === "class") return null;
|
|
782
|
+
const validValues = variantMap.get(key);
|
|
783
|
+
if (!validValues) return true;
|
|
784
|
+
const { initializer: val } = prop;
|
|
785
|
+
if (ts.isStringLiteral(val)) {
|
|
786
|
+
if (!validValues.has(val.text)) return true;
|
|
787
|
+
} else if (ts.isArrayLiteralExpression(val)) {
|
|
788
|
+
if (!val.elements.every(ts.isStringLiteral)) return null;
|
|
789
|
+
if (!val.elements.some((e) => ts.isStringLiteral(e) && validValues.has(e.text))) return true;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}) ?? false;
|
|
793
|
+
}
|
|
794
|
+
/** Returns a TS transformer that removes dead compound entries from every eligible factory call in a source file. */
|
|
795
|
+
function createCompoundPruner(factory, calleeNames, onPruned) {
|
|
796
|
+
return (context) => {
|
|
797
|
+
function visit(node) {
|
|
798
|
+
if (!ts.isCallExpression(node)) return ts.visitEachChild(node, visit, context);
|
|
799
|
+
if (!isFactoryCall(node, calleeNames)) return ts.visitEachChild(node, visit, context);
|
|
800
|
+
const arg = firstObjectArg(node);
|
|
801
|
+
if (!arg) return node;
|
|
802
|
+
const stylingObj = asObject(getProperty(arg, "styling"));
|
|
803
|
+
if (!stylingObj) return ts.visitEachChild(node, visit, context);
|
|
804
|
+
const compoundsArr = asArray(getProperty(stylingObj, "compounds"));
|
|
805
|
+
if (!compoundsArr || compoundsArr.elements.length === 0) return ts.visitEachChild(node, visit, context);
|
|
806
|
+
const variantMap = extractVariantMap(stylingObj);
|
|
807
|
+
if (variantMap.size === 0) return ts.visitEachChild(node, visit, context);
|
|
808
|
+
const liveEntries = compoundsArr.elements.filter((elem) => {
|
|
809
|
+
const obj = asObject(elem);
|
|
810
|
+
return !obj || !isDeadCompound(obj, variantMap);
|
|
811
|
+
});
|
|
812
|
+
if (liveEntries.length === compoundsArr.elements.length) return ts.visitEachChild(node, visit, context);
|
|
813
|
+
onPruned();
|
|
814
|
+
const newCompoundsArr = factory.createArrayLiteralExpression(liveEntries, true);
|
|
815
|
+
const newStylingProps = stylingObj.properties.map((p) => {
|
|
816
|
+
if (ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "compounds") return factory.createPropertyAssignment(p.name, newCompoundsArr);
|
|
817
|
+
return p;
|
|
818
|
+
});
|
|
819
|
+
const newStylingObj = factory.createObjectLiteralExpression(newStylingProps, true);
|
|
820
|
+
const newArgProps = arg.properties.map((p) => {
|
|
821
|
+
if (ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "styling") return factory.createPropertyAssignment(p.name, newStylingObj);
|
|
822
|
+
return p;
|
|
823
|
+
});
|
|
824
|
+
const newArg = factory.createObjectLiteralExpression(newArgProps, true);
|
|
825
|
+
return factory.createCallExpression(node.expression, node.typeArguments, [newArg, ...node.arguments.slice(1)]);
|
|
826
|
+
}
|
|
827
|
+
return (sourceFile) => ts.visitEachChild(sourceFile, visit, context);
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Applies dead-compound pruning to the given TypeScript source file.
|
|
832
|
+
*
|
|
833
|
+
* Returns null when the file has no factory calls with `styling.compounds`, or
|
|
834
|
+
* when every compound entry passes validity — i.e., when no pruning is needed.
|
|
835
|
+
*/
|
|
836
|
+
function pruneDeadCompounds(source, calleeNames) {
|
|
837
|
+
let hasCompounds = false;
|
|
838
|
+
walkEach(source, (n) => {
|
|
839
|
+
if (hasCompounds) return;
|
|
840
|
+
if (ts.isPropertyAssignment(n)) {
|
|
841
|
+
if ((ts.isIdentifier(n.name) || ts.isStringLiteral(n.name) ? n.name.text : void 0) === "compounds") hasCompounds = true;
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
if (!hasCompounds) return null;
|
|
845
|
+
let didPrune = false;
|
|
846
|
+
const result = ts.transform(source, [createCompoundPruner(ts.factory, calleeNames, () => {
|
|
847
|
+
didPrune = true;
|
|
848
|
+
})], { target: ts.ScriptTarget.Latest });
|
|
849
|
+
if (!didPrune) {
|
|
850
|
+
result.dispose();
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const output = ts.createPrinter({
|
|
854
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
855
|
+
removeComments: false
|
|
856
|
+
}).printFile(result.transformed[0]);
|
|
857
|
+
result.dispose();
|
|
858
|
+
return output;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region ../../plugins/vite/src/constants.ts
|
|
862
|
+
const DEFAULT_CALLEE_NAMES = ["createContractComponent", "createPolymorphicComponent"];
|
|
863
|
+
const JSX_EXTS = /* @__PURE__ */ new Set(["tsx", "jsx"]);
|
|
864
|
+
const ALL_EXTS = /* @__PURE__ */ new Set([
|
|
865
|
+
"ts",
|
|
866
|
+
"tsx",
|
|
867
|
+
"js",
|
|
868
|
+
"jsx"
|
|
869
|
+
]);
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region ../../plugins/vite/src/vite-diagnostics.ts
|
|
872
|
+
const ViteDiagnostics = {
|
|
873
|
+
cardinalityViolation(name, totalMin, totalMax, receivedMin, receivedMax) {
|
|
874
|
+
const rangeText = totalMax === Infinity ? `at least ${totalMin}` : totalMin === totalMax ? `exactly ${totalMin}` : `${totalMin}–${totalMax}`;
|
|
875
|
+
const childWord = totalMax === 1 && totalMin === 1 ? "child" : "children";
|
|
876
|
+
const receivedText = receivedMin === receivedMax ? `${receivedMin}` : `${receivedMin}–${receivedMax}`;
|
|
877
|
+
return {
|
|
878
|
+
code: DiagnosticCode.LintCardinalityViolation,
|
|
879
|
+
category: DiagnosticCategory.Lint,
|
|
880
|
+
message: `<${name}> expects ${rangeText} ${childWord} but received ${receivedText}.`
|
|
881
|
+
};
|
|
882
|
+
},
|
|
883
|
+
ariaTagOverride(tagName, asValue, defaultTag) {
|
|
884
|
+
return {
|
|
885
|
+
code: DiagnosticCode.LintAriaTagOverride,
|
|
886
|
+
category: DiagnosticCategory.Lint,
|
|
887
|
+
message: `<${tagName} as="${asValue}"> changes the element type from '${defaultTag}' — ARIA enforcement rules may not apply as expected.`
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region ../../plugins/vite/src/diagnose.ts
|
|
893
|
+
/**
|
|
894
|
+
* Single-pass variant of diagnoseUsages + diagnoseAriaTagOverrides + collectJsxUsages.
|
|
895
|
+
* Visits each JSX node once and dispatches to all three checks, rather than walking
|
|
896
|
+
* the source three times. For use in the hot Vite transform hook.
|
|
897
|
+
*/
|
|
898
|
+
function analyzeJsxSites(source, constraints, severity) {
|
|
899
|
+
const byName = new Map(constraints.filter((c) => c.rules.length > 0).map((c) => [c.name, c]));
|
|
900
|
+
const byNameAria = new Map(constraints.filter((c) => c.hasAriaRules && c.defaultTag !== void 0).map((c) => [c.name, c]));
|
|
901
|
+
const diagnostics = [];
|
|
902
|
+
const usages = [];
|
|
903
|
+
walkEach(source, (node) => {
|
|
904
|
+
let tagName;
|
|
905
|
+
let attributes;
|
|
906
|
+
let count;
|
|
907
|
+
if (ts.isJsxElement(node)) {
|
|
908
|
+
const opening = node.openingElement;
|
|
909
|
+
tagName = ts.isIdentifier(opening.tagName) ? opening.tagName.text : void 0;
|
|
910
|
+
attributes = opening.attributes;
|
|
911
|
+
count = countJsxChildren(node.children);
|
|
912
|
+
} else if (ts.isJsxSelfClosingElement(node)) {
|
|
913
|
+
tagName = ts.isIdentifier(node.tagName) ? node.tagName.text : void 0;
|
|
914
|
+
attributes = node.attributes;
|
|
915
|
+
count = ZERO;
|
|
916
|
+
}
|
|
917
|
+
if (!tagName) return;
|
|
918
|
+
const getPos = lazy(() => source.getLineAndCharacterOfPosition(node.getStart(source)));
|
|
919
|
+
if (count !== void 0) {
|
|
920
|
+
const c = byName.get(tagName);
|
|
921
|
+
if (c && (count.max < c.totalMin || c.exclusiveChildren && count.min > c.totalMax)) {
|
|
922
|
+
const { line, character } = getPos();
|
|
923
|
+
diagnostics.push({
|
|
924
|
+
diagnostic: ViteDiagnostics.cardinalityViolation(c.name, c.totalMin, c.totalMax, count.min, count.max),
|
|
925
|
+
line: line + 1,
|
|
926
|
+
col: character + 1,
|
|
927
|
+
severity
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (attributes) {
|
|
932
|
+
const c = byNameAria.get(tagName);
|
|
933
|
+
if (c) iterate.forEach(attributes.properties, (attr) => {
|
|
934
|
+
if (!ts.isJsxAttribute(attr)) return;
|
|
935
|
+
const { initializer, name } = attr;
|
|
936
|
+
if ((ts.isIdentifier(name) ? name.text : void 0) !== "as" || !initializer) return;
|
|
937
|
+
let asValue;
|
|
938
|
+
if (ts.isStringLiteral(initializer)) asValue = initializer.text;
|
|
939
|
+
else if (ts.isJsxExpression(initializer) && initializer.expression !== void 0 && ts.isStringLiteral(initializer.expression)) asValue = initializer.expression.text;
|
|
940
|
+
if (asValue !== void 0 && asValue !== c.defaultTag) {
|
|
941
|
+
const { line, character } = getPos();
|
|
942
|
+
diagnostics.push({
|
|
943
|
+
diagnostic: ViteDiagnostics.ariaTagOverride(tagName, asValue, c.defaultTag),
|
|
944
|
+
line: line + 1,
|
|
945
|
+
col: character + 1,
|
|
946
|
+
severity
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
if (/^[A-Z]/.test(tagName)) {
|
|
952
|
+
const { line, character } = getPos();
|
|
953
|
+
usages.push({
|
|
954
|
+
tagName,
|
|
955
|
+
count,
|
|
956
|
+
line: line + 1,
|
|
957
|
+
col: character + 1
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
diagnostics,
|
|
963
|
+
usages
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const ZERO = {
|
|
967
|
+
min: 0,
|
|
968
|
+
max: 0
|
|
969
|
+
};
|
|
970
|
+
const ONE = {
|
|
971
|
+
min: 1,
|
|
972
|
+
max: 1
|
|
973
|
+
};
|
|
974
|
+
/**
|
|
975
|
+
* Analyzes an expression to determine how many JSX children it renders.
|
|
976
|
+
*
|
|
977
|
+
* Returns a `ChildCount` range when the count is statically determinable —
|
|
978
|
+
* including partially-dynamic patterns like conditionals and array literals.
|
|
979
|
+
* Returns `undefined` for unknowable cases (`.map()`, variable references,
|
|
980
|
+
* spread elements).
|
|
981
|
+
*
|
|
982
|
+
* Handles:
|
|
983
|
+
* - `null`, `false`, `undefined`, empty `{}` → 0
|
|
984
|
+
* - `cond && <El />` → [0, 1]
|
|
985
|
+
* - `cond || <El />` / `cond ?? <El />` → [min(sides), max(sides)]
|
|
986
|
+
* - `cond ? <A /> : <B />` → [min(branches), max(branches)]
|
|
987
|
+
* - `[<A />, <B />]` (no spreads) → exact array count
|
|
988
|
+
* - JSX element / fragment → 1 / fragment child count
|
|
989
|
+
* - Parenthesized expressions → delegate to inner
|
|
990
|
+
*/
|
|
991
|
+
function countExpression(node) {
|
|
992
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return ZERO;
|
|
993
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return ZERO;
|
|
994
|
+
if (ts.isIdentifier(node) && node.text === "undefined") return ZERO;
|
|
995
|
+
if (ts.isParenthesizedExpression(node)) return countExpression(node.expression);
|
|
996
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) return ONE;
|
|
997
|
+
if (ts.isJsxFragment(node)) return countJsxChildren(node.children);
|
|
998
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
999
|
+
let min = 0;
|
|
1000
|
+
let max = 0;
|
|
1001
|
+
for (const el of node.elements) {
|
|
1002
|
+
if (ts.isSpreadElement(el)) return void 0;
|
|
1003
|
+
const c = countExpression(el);
|
|
1004
|
+
if (!c) return void 0;
|
|
1005
|
+
min += c.min;
|
|
1006
|
+
max += c.max;
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
min,
|
|
1010
|
+
max
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
if (ts.isBinaryExpression(node)) {
|
|
1014
|
+
const op = node.operatorToken.kind;
|
|
1015
|
+
if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
1016
|
+
const right = countExpression(node.right);
|
|
1017
|
+
if (!right) return void 0;
|
|
1018
|
+
return {
|
|
1019
|
+
min: 0,
|
|
1020
|
+
max: right.max
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) {
|
|
1024
|
+
const left = countExpression(node.left);
|
|
1025
|
+
const right = countExpression(node.right);
|
|
1026
|
+
if (!left || !right) return void 0;
|
|
1027
|
+
return {
|
|
1028
|
+
min: Math.min(left.min, right.min),
|
|
1029
|
+
max: Math.max(left.max, right.max)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (ts.isConditionalExpression(node)) {
|
|
1034
|
+
const whenTrue = countExpression(node.whenTrue);
|
|
1035
|
+
const whenFalse = countExpression(node.whenFalse);
|
|
1036
|
+
if (!whenTrue || !whenFalse) return void 0;
|
|
1037
|
+
return {
|
|
1038
|
+
min: Math.min(whenTrue.min, whenFalse.min),
|
|
1039
|
+
max: Math.max(whenTrue.max, whenFalse.max)
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Counts the meaningful children of a JSX element's child list.
|
|
1045
|
+
* Whitespace-only `JsxText` nodes are ignored. Fragments are flattened.
|
|
1046
|
+
* Returns `undefined` if any child contribution is unknowable.
|
|
1047
|
+
*/
|
|
1048
|
+
function countJsxChildren(children) {
|
|
1049
|
+
let min = 0;
|
|
1050
|
+
let max = 0;
|
|
1051
|
+
for (const child of children) {
|
|
1052
|
+
let c;
|
|
1053
|
+
if (ts.isJsxExpression(child)) c = child.expression === void 0 ? ZERO : countExpression(child.expression);
|
|
1054
|
+
else if (ts.isJsxText(child)) c = child.text.trim().length > 0 ? ONE : ZERO;
|
|
1055
|
+
else if (ts.isJsxFragment(child)) c = countJsxChildren(child.children);
|
|
1056
|
+
else c = ONE;
|
|
1057
|
+
if (!c) return void 0;
|
|
1058
|
+
min += c.min;
|
|
1059
|
+
max += c.max;
|
|
1060
|
+
}
|
|
1061
|
+
return {
|
|
1062
|
+
min,
|
|
1063
|
+
max
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region ../../plugins/vite/src/imports.ts
|
|
1068
|
+
/**
|
|
1069
|
+
* Extracts named import bindings from a source file.
|
|
1070
|
+
* Returns a Map from local binding name to its ImportBinding.
|
|
1071
|
+
*
|
|
1072
|
+
* `import { Button } from './button'` → Map { 'Button' => { importedName: 'Button', specifier: './button' } }
|
|
1073
|
+
* `import { Button as MyBtn } from './button'` → Map { 'MyBtn' => { importedName: 'Button', specifier: './button' } }
|
|
1074
|
+
* `import { Btn as Button } from './button'` → Map { 'Button' => { importedName: 'Btn', specifier: './button' } }
|
|
1075
|
+
*
|
|
1076
|
+
* Default imports and namespace imports (`* as X`) are ignored — only named
|
|
1077
|
+
* bindings that could correspond to component identifiers in JSX are collected.
|
|
1078
|
+
*/
|
|
1079
|
+
function extractImportSpecifiers(source) {
|
|
1080
|
+
const result = /* @__PURE__ */ new Map();
|
|
1081
|
+
walkEach(source, (node) => {
|
|
1082
|
+
if (!ts.isImportDeclaration(node)) return;
|
|
1083
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
1084
|
+
if (!ts.isStringLiteral(moduleSpecifier)) return;
|
|
1085
|
+
const specifier = moduleSpecifier.text;
|
|
1086
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
1087
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) return;
|
|
1088
|
+
iterate.forEach(namedBindings.elements, (element) => {
|
|
1089
|
+
const { isTypeOnly, name, propertyName } = element;
|
|
1090
|
+
if (isTypeOnly) return;
|
|
1091
|
+
const { text: localName } = name;
|
|
1092
|
+
const importedName = propertyName?.text ?? localName;
|
|
1093
|
+
result.set(localName, {
|
|
1094
|
+
importedName,
|
|
1095
|
+
specifier
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
return result;
|
|
1100
|
+
}
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region ../../plugins/vite/src/registry.ts
|
|
1103
|
+
/**
|
|
1104
|
+
* Accumulates constraint and import data across multiple `transform` calls so
|
|
1105
|
+
* that cross-file cardinality violations can be detected in `buildEnd`.
|
|
1106
|
+
*
|
|
1107
|
+
* Lifecycle:
|
|
1108
|
+
* 1. `registerConstraints(id, ...)` — called for every TSX/JSX file that
|
|
1109
|
+
* defines constrained components.
|
|
1110
|
+
* 2. `registerImports(id, resolvedMap)` — called when a file imports component
|
|
1111
|
+
* names that appear in JSX; the map keys are local binding names, values are
|
|
1112
|
+
* the absolute file IDs returned by `this.resolve()`.
|
|
1113
|
+
* 3. `addPendingUsage(id, usage)` — records a JSX usage whose tag name is
|
|
1114
|
+
* imported from another file, for deferred validation.
|
|
1115
|
+
* 4. `diagnostics(severity)` — called once in `buildEnd`; resolves each pending
|
|
1116
|
+
* usage to its source constraint and emits a `FileDiagnostic` for any
|
|
1117
|
+
* cardinality violation.
|
|
1118
|
+
*/
|
|
1119
|
+
var ConstraintRegistry = class {
|
|
1120
|
+
constraints = /* @__PURE__ */ new Map();
|
|
1121
|
+
importMap = /* @__PURE__ */ new Map();
|
|
1122
|
+
pending = /* @__PURE__ */ new Map();
|
|
1123
|
+
/** Records the component constraints declared in a source file, keyed by component name. */
|
|
1124
|
+
registerConstraints(fileId, cs) {
|
|
1125
|
+
this.constraints.set(fileId, new Map(cs.map((c) => [c.name, c])));
|
|
1126
|
+
}
|
|
1127
|
+
/** resolvedImports: local name → absolute file ID of the exporting module. */
|
|
1128
|
+
registerImports(fileId, resolvedImports) {
|
|
1129
|
+
this.importMap.set(fileId, resolvedImports);
|
|
1130
|
+
}
|
|
1131
|
+
/** Queues a JSX usage site whose constraint lives in another file for deferred cross-file validation at buildEnd. */
|
|
1132
|
+
addPendingUsage(fileId, usage) {
|
|
1133
|
+
let list = this.pending.get(fileId);
|
|
1134
|
+
if (!list) {
|
|
1135
|
+
list = [];
|
|
1136
|
+
this.pending.set(fileId, list);
|
|
1137
|
+
}
|
|
1138
|
+
list.push(usage);
|
|
1139
|
+
}
|
|
1140
|
+
/** Resolves a component name used in `fileId` to its constraint definition. */
|
|
1141
|
+
resolveConstraint(fileId, name) {
|
|
1142
|
+
const imports = this.importMap.get(fileId);
|
|
1143
|
+
if (!imports) return void 0;
|
|
1144
|
+
const sourceId = imports.get(name);
|
|
1145
|
+
if (!sourceId) return void 0;
|
|
1146
|
+
return this.constraints.get(sourceId)?.get(name);
|
|
1147
|
+
}
|
|
1148
|
+
/** Returns cardinality violations across all pending cross-file usages. */
|
|
1149
|
+
diagnostics(severity) {
|
|
1150
|
+
const result = [];
|
|
1151
|
+
iterate.forEach(this.pending, ([fileId, usages]) => {
|
|
1152
|
+
iterate.forEach(usages, ({ col, count, line, tagName }) => {
|
|
1153
|
+
if (count === void 0) return;
|
|
1154
|
+
const constraint = this.resolveConstraint(fileId, tagName);
|
|
1155
|
+
if (!constraint) return;
|
|
1156
|
+
const { totalMin, totalMax, name, exclusiveChildren } = constraint;
|
|
1157
|
+
const { min, max } = count;
|
|
1158
|
+
if (!(max < totalMin) && !(exclusiveChildren && min > totalMax)) return;
|
|
1159
|
+
result.push({
|
|
1160
|
+
fileId,
|
|
1161
|
+
diagnostic: ViteDiagnostics.cardinalityViolation(name, totalMin, totalMax, min, max),
|
|
1162
|
+
line,
|
|
1163
|
+
col,
|
|
1164
|
+
severity
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
//#endregion
|
|
1172
|
+
//#region ../../plugins/vite/src/slot-transform.ts
|
|
1173
|
+
/**
|
|
1174
|
+
* Compile-time asChild → render-prop transform.
|
|
1175
|
+
*
|
|
1176
|
+
* Rewrites JSX usage sites of the form:
|
|
1177
|
+
* <Component asChild ...props>
|
|
1178
|
+
* <childTag ...childProps>children</childTag>
|
|
1179
|
+
* </Component>
|
|
1180
|
+
*
|
|
1181
|
+
* to the render-prop form:
|
|
1182
|
+
* <Component render={(_p) => <childTag ...childProps {..._p} />} ...props />
|
|
1183
|
+
*
|
|
1184
|
+
* The render-prop form eliminates the Slot/cloneElement/mergeProps path at
|
|
1185
|
+
* runtime — resolved props are passed directly to the render callback with no
|
|
1186
|
+
* element cloning.
|
|
1187
|
+
*
|
|
1188
|
+
* **Safety conditions** — the transform is skipped if any of these are true:
|
|
1189
|
+
* 1. The child has a dynamic `className` expression (cannot merge safely).
|
|
1190
|
+
* A string-literal `className` IS handled: the transform generates
|
|
1191
|
+
* `{..._p, className: _p.className + ' childCls'}`.
|
|
1192
|
+
* 2. The child has a bare `style` or `on*` attribute without an initializer.
|
|
1193
|
+
* Static object-literal and expression-valued `style` props are merged:
|
|
1194
|
+
* `style={{..._p.style, ...childStyle}}`. Event handlers are composed:
|
|
1195
|
+
* `onClick={(_e) => { (childHandler)(_e); _p.onClick?.(_e); }}`.
|
|
1196
|
+
* 3. The component name starts with a lowercase letter (HTML intrinsic — not
|
|
1197
|
+
* a polymorphic component).
|
|
1198
|
+
* 4. There are zero or more than one meaningful child elements.
|
|
1199
|
+
*
|
|
1200
|
+
* The transform is conservative: any condition that is not statically clear
|
|
1201
|
+
* causes the node to be left unchanged.
|
|
1202
|
+
*/
|
|
1203
|
+
/** Returns true when the first character of `s` is an uppercase ASCII letter (A–Z). */
|
|
1204
|
+
function isUpperCase(s) {
|
|
1205
|
+
return s.charCodeAt(0) >= 65 && s.charCodeAt(0) <= 90;
|
|
1206
|
+
}
|
|
1207
|
+
/** Returns the attribute name string; empty string for namespaced names. */
|
|
1208
|
+
function jsxAttrName(attr) {
|
|
1209
|
+
return ts.isIdentifier(attr.name) ? attr.name.text : "";
|
|
1210
|
+
}
|
|
1211
|
+
function getStaticClassName(child) {
|
|
1212
|
+
const attrs = ts.isJsxElement(child) ? child.openingElement.attributes.properties : child.attributes.properties;
|
|
1213
|
+
for (const attr of attrs) {
|
|
1214
|
+
if (!ts.isJsxAttribute(attr) || jsxAttrName(attr) !== "className") continue;
|
|
1215
|
+
const init = attr.initializer;
|
|
1216
|
+
if (!init) return {
|
|
1217
|
+
absent: false,
|
|
1218
|
+
value: ""
|
|
1219
|
+
};
|
|
1220
|
+
if (ts.isStringLiteral(init)) return {
|
|
1221
|
+
absent: false,
|
|
1222
|
+
value: init.text
|
|
1223
|
+
};
|
|
1224
|
+
if (ts.isJsxExpression(init) && init.expression !== void 0 && ts.isStringLiteral(init.expression)) return {
|
|
1225
|
+
absent: false,
|
|
1226
|
+
value: init.expression.text
|
|
1227
|
+
};
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
return { absent: true };
|
|
1231
|
+
}
|
|
1232
|
+
function getStyleInfo(child) {
|
|
1233
|
+
const attrs = ts.isJsxElement(child) ? child.openingElement.attributes.properties : child.attributes.properties;
|
|
1234
|
+
for (const attr of attrs) {
|
|
1235
|
+
if (!ts.isJsxAttribute(attr) || jsxAttrName(attr) !== "style") continue;
|
|
1236
|
+
const init = attr.initializer;
|
|
1237
|
+
if (!init || ts.isStringLiteral(init)) return null;
|
|
1238
|
+
if (ts.isJsxExpression(init) && init.expression !== void 0) return {
|
|
1239
|
+
absent: false,
|
|
1240
|
+
expr: init.expression
|
|
1241
|
+
};
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
return { absent: true };
|
|
1245
|
+
}
|
|
1246
|
+
function getEventHandlers(child) {
|
|
1247
|
+
const attrs = ts.isJsxElement(child) ? child.openingElement.attributes.properties : child.attributes.properties;
|
|
1248
|
+
const handlers = [];
|
|
1249
|
+
for (const attr of attrs) {
|
|
1250
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1251
|
+
const name = jsxAttrName(attr);
|
|
1252
|
+
if (!/^on[A-Z]/.test(name)) continue;
|
|
1253
|
+
const init = attr.initializer;
|
|
1254
|
+
if (!init) return null;
|
|
1255
|
+
if (ts.isJsxExpression(init) && init.expression !== void 0) {
|
|
1256
|
+
handlers.push({
|
|
1257
|
+
name,
|
|
1258
|
+
expr: init.expression
|
|
1259
|
+
});
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
return handlers;
|
|
1265
|
+
}
|
|
1266
|
+
/** Returns true if the opening element has an `asChild` attribute (bare or `={true}`). */
|
|
1267
|
+
function hasAsChild(opening) {
|
|
1268
|
+
for (const attr of opening.attributes.properties) {
|
|
1269
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1270
|
+
if (jsxAttrName(attr) !== "asChild") continue;
|
|
1271
|
+
if (attr.initializer === void 0) return true;
|
|
1272
|
+
if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression !== void 0 && attr.initializer.expression.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
1273
|
+
}
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
/** Returns the single meaningful JSX element child, or undefined if there isn't exactly one. */
|
|
1277
|
+
function getSingleElementChild(node) {
|
|
1278
|
+
const meaningful = [];
|
|
1279
|
+
for (const child of node.children) {
|
|
1280
|
+
if (ts.isJsxText(child)) {
|
|
1281
|
+
if (child.text.trim().length > 0) return void 0;
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
if (ts.isJsxExpression(child)) return void 0;
|
|
1285
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
1286
|
+
meaningful.push(child);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
return meaningful.length === 1 ? meaningful[0] : void 0;
|
|
1292
|
+
}
|
|
1293
|
+
/** Returns the tag name string if the opening element has a simple identifier tag. */
|
|
1294
|
+
function getTagName(child) {
|
|
1295
|
+
const tag = ts.isJsxElement(child) ? child.openingElement.tagName : child.tagName;
|
|
1296
|
+
return ts.isIdentifier(tag) ? tag.text : void 0;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Builds the attribute list for the transformed parent, omitting `asChild`
|
|
1300
|
+
* and adding `render={(_p) => <childTag ...childAttrs {..._p} />}`.
|
|
1301
|
+
*
|
|
1302
|
+
* Merged props are placed after the `{..._p}` spread so they override _p:
|
|
1303
|
+
* - Static `className` → `className={_p.className + ' childCls'}`
|
|
1304
|
+
* - `style` → `style={{..._p.style, ...childStyleExpr}}`
|
|
1305
|
+
* - `on*` handlers → `onClick={(_e) => { (childHandler)(_e); _p.onClick?.(_e); }}`
|
|
1306
|
+
*/
|
|
1307
|
+
function buildTransformedAttributes(factory, original, child, tagName, clsResult, styleInfo, handlers) {
|
|
1308
|
+
const parentAttrs = original.attributes.properties.filter((attr) => !(ts.isJsxAttribute(attr) && jsxAttrName(attr) === "asChild"));
|
|
1309
|
+
const hasStaticCls = !clsResult.absent;
|
|
1310
|
+
const hasStyle = !styleInfo.absent;
|
|
1311
|
+
const handlerNames = new Set(iterate.map(handlers, (h) => h.name));
|
|
1312
|
+
const childOpeningAttrs = (ts.isJsxElement(child) ? child.openingElement.attributes.properties : child.attributes.properties).filter((attr) => !(ts.isJsxAttribute(attr) && (jsxAttrName(attr) === "ref" || hasStaticCls && jsxAttrName(attr) === "className" || hasStyle && jsxAttrName(attr) === "style" || handlerNames.has(jsxAttrName(attr)))));
|
|
1313
|
+
const childContent = ts.isJsxElement(child) ? child.children : void 0;
|
|
1314
|
+
const spreadProp = factory.createJsxSpreadAttribute(factory.createIdentifier("_p"));
|
|
1315
|
+
const extraAttrs = [];
|
|
1316
|
+
if (hasStaticCls && clsResult.value !== "") {
|
|
1317
|
+
const mergedExpr = factory.createBinaryExpression(factory.createPropertyAccessExpression(factory.createIdentifier("_p"), "className"), ts.SyntaxKind.PlusToken, factory.createStringLiteral(` ${clsResult.value}`));
|
|
1318
|
+
extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier("className"), factory.createJsxExpression(void 0, mergedExpr)));
|
|
1319
|
+
}
|
|
1320
|
+
if (hasStyle) {
|
|
1321
|
+
const styleExpr = styleInfo.expr;
|
|
1322
|
+
const pStyleSpread = factory.createSpreadAssignment(factory.createPropertyAccessExpression(factory.createIdentifier("_p"), "style"));
|
|
1323
|
+
let mergedStyleObj;
|
|
1324
|
+
if (ts.isObjectLiteralExpression(styleExpr)) mergedStyleObj = factory.createObjectLiteralExpression([pStyleSpread, ...styleExpr.properties], false);
|
|
1325
|
+
else mergedStyleObj = factory.createObjectLiteralExpression([pStyleSpread, factory.createSpreadAssignment(styleExpr)], false);
|
|
1326
|
+
extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier("style"), factory.createJsxExpression(void 0, mergedStyleObj)));
|
|
1327
|
+
}
|
|
1328
|
+
for (const { name, expr } of handlers) {
|
|
1329
|
+
const eParam = factory.createParameterDeclaration(void 0, void 0, "_e");
|
|
1330
|
+
const callChild = factory.createExpressionStatement(factory.createCallExpression(factory.createParenthesizedExpression(expr), void 0, [factory.createIdentifier("_e")]));
|
|
1331
|
+
const callParent = factory.createExpressionStatement(factory.createCallChain(factory.createPropertyAccessExpression(factory.createIdentifier("_p"), name), factory.createToken(ts.SyntaxKind.QuestionDotToken), void 0, [factory.createIdentifier("_e")]));
|
|
1332
|
+
const composedFn = factory.createArrowFunction(void 0, void 0, [eParam], void 0, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([callChild, callParent], true));
|
|
1333
|
+
extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier(name), factory.createJsxExpression(void 0, composedFn)));
|
|
1334
|
+
}
|
|
1335
|
+
const childAttrsWithSpread = factory.createJsxAttributes([
|
|
1336
|
+
...childOpeningAttrs,
|
|
1337
|
+
spreadProp,
|
|
1338
|
+
...extraAttrs
|
|
1339
|
+
]);
|
|
1340
|
+
const childElement = ts.isJsxElement(child) ? factory.createJsxElement(factory.createJsxOpeningElement(factory.createIdentifier(tagName), void 0, childAttrsWithSpread), childContent ?? [], factory.createJsxClosingElement(factory.createIdentifier(tagName))) : factory.createJsxSelfClosingElement(factory.createIdentifier(tagName), void 0, childAttrsWithSpread);
|
|
1341
|
+
const renderArrow = factory.createArrowFunction(void 0, void 0, [factory.createParameterDeclaration(void 0, void 0, "_p")], void 0, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createParenthesizedExpression(childElement));
|
|
1342
|
+
const renderAttr = factory.createJsxAttribute(factory.createIdentifier("render"), factory.createJsxExpression(void 0, renderArrow));
|
|
1343
|
+
return factory.createJsxAttributes([renderAttr, ...parentAttrs]);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Returns a TypeScript transformer that rewrites safe `asChild` JSX patterns
|
|
1347
|
+
* to the render-prop form in a single source file.
|
|
1348
|
+
*/
|
|
1349
|
+
function createAsChildTransformer(factory) {
|
|
1350
|
+
return (context) => {
|
|
1351
|
+
function visit(node) {
|
|
1352
|
+
if (!ts.isJsxElement(node)) return ts.visitEachChild(node, visit, context);
|
|
1353
|
+
const opening = node.openingElement;
|
|
1354
|
+
const tagName = ts.isIdentifier(opening.tagName) ? opening.tagName.text : void 0;
|
|
1355
|
+
if (!tagName || !isUpperCase(tagName)) return ts.visitEachChild(node, visit, context);
|
|
1356
|
+
if (!hasAsChild(opening)) return ts.visitEachChild(node, visit, context);
|
|
1357
|
+
const child = getSingleElementChild(node);
|
|
1358
|
+
if (!child) return ts.visitEachChild(node, visit, context);
|
|
1359
|
+
const clsResult = getStaticClassName(child);
|
|
1360
|
+
if (clsResult === null) return ts.visitEachChild(node, visit, context);
|
|
1361
|
+
const styleInfo = getStyleInfo(child);
|
|
1362
|
+
if (styleInfo === null) return ts.visitEachChild(node, visit, context);
|
|
1363
|
+
const handlers = getEventHandlers(child);
|
|
1364
|
+
if (handlers === null) return ts.visitEachChild(node, visit, context);
|
|
1365
|
+
const childTag = getTagName(child);
|
|
1366
|
+
if (!childTag) return ts.visitEachChild(node, visit, context);
|
|
1367
|
+
const newAttrs = buildTransformedAttributes(factory, opening, child, childTag, clsResult, styleInfo, handlers);
|
|
1368
|
+
return factory.createJsxSelfClosingElement(opening.tagName, opening.typeArguments, newAttrs);
|
|
1369
|
+
}
|
|
1370
|
+
return (sourceFile) => ts.visitEachChild(sourceFile, visit, context);
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Applies the asChild → render-prop transform to the given TypeScript source
|
|
1375
|
+
* file and returns the printed output.
|
|
1376
|
+
*
|
|
1377
|
+
* Returns null if no `asChild` attribute is found in the source (fast path —
|
|
1378
|
+
* avoids parsing overhead for files that don't use the pattern).
|
|
1379
|
+
*/
|
|
1380
|
+
function transformAsChild(source) {
|
|
1381
|
+
if (!iterate.some(walk(source), (node) => ts.isJsxAttribute(node) && jsxAttrName(node) === "asChild")) return null;
|
|
1382
|
+
const result = ts.transform(source, [createAsChildTransformer(ts.factory)], {
|
|
1383
|
+
jsx: ts.JsxEmit.Preserve,
|
|
1384
|
+
target: ts.ScriptTarget.Latest
|
|
1385
|
+
});
|
|
1386
|
+
const output = ts.createPrinter({
|
|
1387
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
1388
|
+
removeComments: false
|
|
1389
|
+
}).printFile(result.transformed[0]);
|
|
1390
|
+
result.dispose();
|
|
1391
|
+
return output;
|
|
1392
|
+
}
|
|
1393
|
+
//#endregion
|
|
1394
|
+
//#region ../../plugins/vite/src/static-compose.ts
|
|
1395
|
+
/**
|
|
1396
|
+
* Compile-time static composition transform.
|
|
1397
|
+
*
|
|
1398
|
+
* For factory calls that have `precomputedClasses` injected (by classExtractPlugin),
|
|
1399
|
+
* replaces static JSX usage sites with direct element creation — bypassing the
|
|
1400
|
+
* runtime render pipeline entirely.
|
|
1401
|
+
*
|
|
1402
|
+
* Example:
|
|
1403
|
+
* // Source (same file defines Button and uses it)
|
|
1404
|
+
* const Button = createContractComponent({ tag: 'button', styling: { precomputedClasses: {...} } })
|
|
1405
|
+
* <Button size="lg">Click</Button>
|
|
1406
|
+
*
|
|
1407
|
+
* // Output
|
|
1408
|
+
* const Button = createContractComponent({ ... }) // unchanged — still exported
|
|
1409
|
+
* <button className="btn btn-lg">Click</button> // inlined!
|
|
1410
|
+
*
|
|
1411
|
+
* **Eligibility conditions** — a usage site is inlined only when:
|
|
1412
|
+
* 1. The component is defined in the same file or imported from a file already
|
|
1413
|
+
* transformed by this plugin, with `precomputedClasses` injected
|
|
1414
|
+
* 2. No `as`, `asChild`, `render`, or spread attributes at the usage site
|
|
1415
|
+
* 3. All variant props are static string literals
|
|
1416
|
+
* 4. `className` is absent or a static string literal
|
|
1417
|
+
* 5. The factory config has no top-level `defaults` and no `enforcement`
|
|
1418
|
+
* (either would require runtime prop normalization that inlining skips)
|
|
1419
|
+
*
|
|
1420
|
+
* The factory call itself is intentionally left in the output so the component
|
|
1421
|
+
* remains exportable for cross-file consumption that falls back to the runtime
|
|
1422
|
+
* path. Dead-code elimination at the bundler level can remove it when no
|
|
1423
|
+
* runtime path remains.
|
|
1424
|
+
*
|
|
1425
|
+
* **Cross-file known limitations:**
|
|
1426
|
+
* - Barrel re-exports are not resolved (import from index.ts re-exporting ./Button)
|
|
1427
|
+
* - Aliased imports are not matched (`export { Btn as Button }` won't be found)
|
|
1428
|
+
* - Dev-mode ordering: definition file may not yet be transformed when consumer runs
|
|
1429
|
+
* All three degrade gracefully to the runtime path — no error, no broken output.
|
|
1430
|
+
*/
|
|
1431
|
+
/** Returns the text of a string literal node, or undefined. */
|
|
1432
|
+
function asStringLiteral(node) {
|
|
1433
|
+
return node !== void 0 && ts.isStringLiteral(node) ? node.text : void 0;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Walks the source file and extracts metadata for each same-file factory call
|
|
1437
|
+
* that is eligible for static composition.
|
|
1438
|
+
*/
|
|
1439
|
+
function extractStaticComponents(source, calleeNames) {
|
|
1440
|
+
const result = /* @__PURE__ */ new Map();
|
|
1441
|
+
walkEach(source, (node) => {
|
|
1442
|
+
if (!ts.isVariableDeclaration(node)) return;
|
|
1443
|
+
if (!ts.isIdentifier(node.name)) return;
|
|
1444
|
+
const varName = node.name.text;
|
|
1445
|
+
const init = node.initializer;
|
|
1446
|
+
if (!init) return;
|
|
1447
|
+
let call;
|
|
1448
|
+
if (ts.isCallExpression(init) && isFactoryCall(init, calleeNames)) call = init;
|
|
1449
|
+
else if (ts.isAsExpression(init) && ts.isCallExpression(init.expression) && isFactoryCall(init.expression, calleeNames)) call = init.expression;
|
|
1450
|
+
if (!call) return;
|
|
1451
|
+
const arg = firstObjectArg(call);
|
|
1452
|
+
if (!arg) return;
|
|
1453
|
+
const defaultTag = asStringLiteral(getProperty(arg, "tag"));
|
|
1454
|
+
if (!defaultTag) return;
|
|
1455
|
+
const stylingObj = asObject(getProperty(arg, "styling"));
|
|
1456
|
+
if (!stylingObj) return;
|
|
1457
|
+
const precomputedNode = asObject(getProperty(stylingObj, "precomputedClasses"));
|
|
1458
|
+
if (!precomputedNode) return;
|
|
1459
|
+
const precomputedClasses = {};
|
|
1460
|
+
if (iterate.find(precomputedNode.properties, (prop) => {
|
|
1461
|
+
if (!ts.isPropertyAssignment(prop)) return true;
|
|
1462
|
+
const { initializer, name } = prop;
|
|
1463
|
+
const key = ts.isStringLiteral(name) ? name.text : void 0;
|
|
1464
|
+
const val = asStringLiteral(initializer);
|
|
1465
|
+
if (key === void 0 || val === void 0) return true;
|
|
1466
|
+
precomputedClasses[key] = val;
|
|
1467
|
+
return null;
|
|
1468
|
+
})) return;
|
|
1469
|
+
if (getProperty(arg, "defaults") !== void 0) return;
|
|
1470
|
+
if (getProperty(arg, "enforcement") !== void 0) return;
|
|
1471
|
+
const variantKeys = /* @__PURE__ */ new Set();
|
|
1472
|
+
const variantsObj = asObject(getProperty(stylingObj, "variants"));
|
|
1473
|
+
if (variantsObj) iterate.forEach(variantsObj.properties, (prop) => {
|
|
1474
|
+
if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) variantKeys.add(prop.name.text);
|
|
1475
|
+
});
|
|
1476
|
+
result.set(varName, {
|
|
1477
|
+
defaultTag,
|
|
1478
|
+
variantKeys,
|
|
1479
|
+
precomputedClasses
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
return result;
|
|
1483
|
+
}
|
|
1484
|
+
/** Reads the value of a named JSX attribute from the attribute list: absent, a static string, or dynamic. */
|
|
1485
|
+
function readAttrValue(attrs, name) {
|
|
1486
|
+
for (const attr of attrs) {
|
|
1487
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1488
|
+
if (!(ts.isIdentifier(attr.name) && attr.name.text === name)) continue;
|
|
1489
|
+
const init = attr.initializer;
|
|
1490
|
+
if (!init) return {
|
|
1491
|
+
kind: "string",
|
|
1492
|
+
value: ""
|
|
1493
|
+
};
|
|
1494
|
+
if (ts.isStringLiteral(init)) return {
|
|
1495
|
+
kind: "string",
|
|
1496
|
+
value: init.text
|
|
1497
|
+
};
|
|
1498
|
+
if (ts.isJsxExpression(init) && init.expression !== void 0) {
|
|
1499
|
+
if (ts.isStringLiteral(init.expression)) return {
|
|
1500
|
+
kind: "string",
|
|
1501
|
+
value: init.expression.text
|
|
1502
|
+
};
|
|
1503
|
+
return { kind: "dynamic" };
|
|
1504
|
+
}
|
|
1505
|
+
return { kind: "dynamic" };
|
|
1506
|
+
}
|
|
1507
|
+
return { kind: "absent" };
|
|
1508
|
+
}
|
|
1509
|
+
/** Returns a TS transformer that replaces eligible JSX usage sites with direct element creation. */
|
|
1510
|
+
function createStaticCompositionTransformer(factory, components, onInlined) {
|
|
1511
|
+
return (context) => {
|
|
1512
|
+
function visit(node) {
|
|
1513
|
+
const isSelfClose = ts.isJsxSelfClosingElement(node);
|
|
1514
|
+
const isOpen = ts.isJsxElement(node);
|
|
1515
|
+
if (!isSelfClose && !isOpen) return ts.visitEachChild(node, visit, context);
|
|
1516
|
+
const tagNode = isOpen ? node.openingElement.tagName : node.tagName;
|
|
1517
|
+
if (!ts.isIdentifier(tagNode)) return ts.visitEachChild(node, visit, context);
|
|
1518
|
+
const info = components.get(tagNode.text);
|
|
1519
|
+
if (!info) return ts.visitEachChild(node, visit, context);
|
|
1520
|
+
const attrList = isOpen ? node.openingElement.attributes.properties : node.attributes.properties;
|
|
1521
|
+
if (attrList.some(ts.isJsxSpreadAttribute)) return ts.visitEachChild(node, visit, context);
|
|
1522
|
+
if (readAttrValue(attrList, "asChild").kind !== "absent") return ts.visitEachChild(node, visit, context);
|
|
1523
|
+
if (readAttrValue(attrList, "render").kind !== "absent") return ts.visitEachChild(node, visit, context);
|
|
1524
|
+
const asVal = readAttrValue(attrList, "as");
|
|
1525
|
+
if (asVal.kind === "dynamic") return ts.visitEachChild(node, visit, context);
|
|
1526
|
+
const outputTag = asVal.kind === "string" ? asVal.value : info.defaultTag;
|
|
1527
|
+
const variantProps = {};
|
|
1528
|
+
for (const propName of info.variantKeys) {
|
|
1529
|
+
const val = readAttrValue(attrList, propName);
|
|
1530
|
+
if (val.kind === "absent") continue;
|
|
1531
|
+
if (val.kind === "string") {
|
|
1532
|
+
variantProps[propName] = val.value;
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
return ts.visitEachChild(node, visit, context);
|
|
1536
|
+
}
|
|
1537
|
+
const cacheKey = buildCacheKey(variantProps);
|
|
1538
|
+
const baseClass = info.precomputedClasses[cacheKey];
|
|
1539
|
+
if (baseClass === void 0) return ts.visitEachChild(node, visit, context);
|
|
1540
|
+
const clsVal = readAttrValue(attrList, "className");
|
|
1541
|
+
if (clsVal.kind === "dynamic") return ts.visitEachChild(node, visit, context);
|
|
1542
|
+
const finalClass = clsVal.kind === "string" && clsVal.value ? `${baseClass} ${clsVal.value}` : baseClass;
|
|
1543
|
+
const strip = /* @__PURE__ */ new Set([
|
|
1544
|
+
...info.variantKeys,
|
|
1545
|
+
"as",
|
|
1546
|
+
"asChild",
|
|
1547
|
+
"render",
|
|
1548
|
+
"className"
|
|
1549
|
+
]);
|
|
1550
|
+
const outputAttrs = [factory.createJsxAttribute(factory.createIdentifier("className"), factory.createStringLiteral(finalClass))];
|
|
1551
|
+
for (const attr of attrList) {
|
|
1552
|
+
if (ts.isJsxSpreadAttribute(attr)) continue;
|
|
1553
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
1554
|
+
const name = ts.isIdentifier(attr.name) ? attr.name.text : "";
|
|
1555
|
+
if (strip.has(name)) continue;
|
|
1556
|
+
outputAttrs.push(attr);
|
|
1557
|
+
}
|
|
1558
|
+
const newAttrs = factory.createJsxAttributes(outputAttrs);
|
|
1559
|
+
const outputTagIdent = factory.createIdentifier(outputTag);
|
|
1560
|
+
onInlined();
|
|
1561
|
+
if (isSelfClose) return factory.createJsxSelfClosingElement(outputTagIdent, void 0, newAttrs);
|
|
1562
|
+
const visitedChildren = node.children.map((c) => ts.visitNode(c, visit));
|
|
1563
|
+
return factory.createJsxElement(factory.createJsxOpeningElement(outputTagIdent, void 0, newAttrs), visitedChildren, factory.createJsxClosingElement(outputTagIdent));
|
|
1564
|
+
}
|
|
1565
|
+
return (sourceFile) => ts.visitEachChild(sourceFile, visit, context);
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Applies the static composition transform to the given source file.
|
|
1570
|
+
*
|
|
1571
|
+
* `importedComponents` contains cross-file metadata resolved by the plugin
|
|
1572
|
+
* registry — components defined in other files that were already transformed.
|
|
1573
|
+
* When empty (dev mode ordering race or barrel re-export), same-file components
|
|
1574
|
+
* still inline normally; cross-file sites fall through to the runtime path.
|
|
1575
|
+
*
|
|
1576
|
+
* Returns null when:
|
|
1577
|
+
* - No eligible factory calls or imported components are present
|
|
1578
|
+
* - No eligible usage sites are found after analysis
|
|
1579
|
+
*/
|
|
1580
|
+
function composeStatically(source, calleeNames, importedComponents = /* @__PURE__ */ new Map()) {
|
|
1581
|
+
const sameFile = extractStaticComponents(source, calleeNames);
|
|
1582
|
+
const components = importedComponents.size > 0 ? new Map([...importedComponents, ...sameFile]) : sameFile;
|
|
1583
|
+
if (components.size === 0) return null;
|
|
1584
|
+
const componentNames = new Set(components.keys());
|
|
1585
|
+
let hasEligibleTag = false;
|
|
1586
|
+
walkEach(source, (n) => {
|
|
1587
|
+
if (hasEligibleTag) return;
|
|
1588
|
+
const tagNode = ts.isJsxElement(n) ? n.openingElement.tagName : ts.isJsxSelfClosingElement(n) ? n.tagName : void 0;
|
|
1589
|
+
if (tagNode && ts.isIdentifier(tagNode) && componentNames.has(tagNode.text)) hasEligibleTag = true;
|
|
1590
|
+
});
|
|
1591
|
+
if (!hasEligibleTag) return null;
|
|
1592
|
+
let didInline = false;
|
|
1593
|
+
const transformResult = ts.transform(source, [createStaticCompositionTransformer(ts.factory, components, () => {
|
|
1594
|
+
didInline = true;
|
|
1595
|
+
})], {
|
|
1596
|
+
jsx: ts.JsxEmit.Preserve,
|
|
1597
|
+
target: ts.ScriptTarget.Latest
|
|
1598
|
+
});
|
|
1599
|
+
if (!didInline) {
|
|
1600
|
+
transformResult.dispose();
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
const output = ts.createPrinter({
|
|
1604
|
+
newLine: ts.NewLineKind.LineFeed,
|
|
1605
|
+
removeComments: false
|
|
1606
|
+
}).printFile(transformResult.transformed[0]);
|
|
1607
|
+
transformResult.dispose();
|
|
1608
|
+
return output;
|
|
1609
|
+
}
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region ../../lib/tailwind/src/layout-keys.ts
|
|
1612
|
+
/**
|
|
1613
|
+
* Canonical list of reserved layout prop names.
|
|
1614
|
+
*
|
|
1615
|
+
* This array is the single source of truth for every supported CSS `display`
|
|
1616
|
+
* value exposed as a boolean prop. The `LayoutKey` type is derived directly
|
|
1617
|
+
* from this list.
|
|
1618
|
+
*
|
|
1619
|
+
* To add a new display mode:
|
|
1620
|
+
* 1. Add the display value here.
|
|
1621
|
+
* 2. Register its layout family in `LAYOUT_FAMILY_MAP`.
|
|
1622
|
+
*
|
|
1623
|
+
* Prop names intentionally match the corresponding Tailwind/CSS display
|
|
1624
|
+
* utilities, so no additional prop-to-class mapping is required.
|
|
1625
|
+
*/
|
|
1626
|
+
const layoutKeys = [
|
|
1627
|
+
"flex",
|
|
1628
|
+
"inline-flex",
|
|
1629
|
+
"grid",
|
|
1630
|
+
"inline-grid",
|
|
1631
|
+
"block",
|
|
1632
|
+
"inline-block",
|
|
1633
|
+
"inline",
|
|
1634
|
+
"hidden",
|
|
1635
|
+
"contents",
|
|
1636
|
+
"flow-root",
|
|
1637
|
+
"list-item",
|
|
1638
|
+
"table",
|
|
1639
|
+
"inline-table",
|
|
1640
|
+
"table-caption",
|
|
1641
|
+
"table-cell",
|
|
1642
|
+
"table-column",
|
|
1643
|
+
"table-column-group",
|
|
1644
|
+
"table-footer-group",
|
|
1645
|
+
"table-header-group",
|
|
1646
|
+
"table-row-group",
|
|
1647
|
+
"table-row"
|
|
1648
|
+
];
|
|
1649
|
+
//#endregion
|
|
1650
|
+
//#region ../../plugins/vite/src/design-tokens.ts
|
|
1651
|
+
/**
|
|
1652
|
+
* Compile-time design token collection.
|
|
1653
|
+
*
|
|
1654
|
+
* Walks factory call ASTs and extracts every statically-declared class string
|
|
1655
|
+
* used in `styling.base`, `styling.variants`, `styling.compounds`, and
|
|
1656
|
+
* `styling.tags`. Collects them into a per-component manifest and writes a
|
|
1657
|
+
* JSON file on `writeBundle`.
|
|
1658
|
+
*
|
|
1659
|
+
* The primary use case is Tailwind's `safelist` — point the emitted file at
|
|
1660
|
+
* Tailwind's content so none of the variant classes are purged:
|
|
1661
|
+
*
|
|
1662
|
+
* // tailwind.config.js
|
|
1663
|
+
* export default { content: ['./src/**', './praxis-tokens.json'] }
|
|
1664
|
+
*
|
|
1665
|
+
* This is a Tailwind v3-style `content`-array mechanism: v4 does not scan
|
|
1666
|
+
* `tailwind.config.js` unless the app explicitly adds `@config` to its CSS,
|
|
1667
|
+
* which is not the standard v4 setup. On v4, display classes (flex, grid,
|
|
1668
|
+
* inline-block, etc.) are not covered by this manifest at all — those are
|
|
1669
|
+
* runtime-assembled and require importing `praxis-kit/tailwind.css` instead
|
|
1670
|
+
* (see lib/tailwind/src/tailwind-safelist.css).
|
|
1671
|
+
*
|
|
1672
|
+
* The JSON schema is intentionally stable and human-readable:
|
|
1673
|
+
*
|
|
1674
|
+
* {
|
|
1675
|
+
* "components": {
|
|
1676
|
+
* "Button": {
|
|
1677
|
+
* "base": "btn",
|
|
1678
|
+
* "variantClasses": ["btn-sm", "btn-md", "btn-lg"],
|
|
1679
|
+
* "compoundClasses": ["btn-compact"],
|
|
1680
|
+
* "tagClasses": ["link-style"]
|
|
1681
|
+
* }
|
|
1682
|
+
* },
|
|
1683
|
+
* "allClasses": ["btn", "btn-sm", ...]
|
|
1684
|
+
* }
|
|
1685
|
+
*/
|
|
1686
|
+
/** Recursively appends non-empty string literals from `node` into `out`. Handles string and array literals; ignores other shapes. */
|
|
1687
|
+
function collectStringValues(node, out) {
|
|
1688
|
+
if (!node) return;
|
|
1689
|
+
if (ts.isStringLiteral(node)) {
|
|
1690
|
+
if (node.text.trim()) out.push(node.text);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (ts.isArrayLiteralExpression(node)) iterate.forEach(node.elements, (elem) => {
|
|
1694
|
+
collectStringValues(elem, out);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
/** Extracts all statically-declared class strings from a `styling` object's `base`, `variants`, `compounds`, and `tags` fields. */
|
|
1698
|
+
function extractStylingTokens(stylingObj) {
|
|
1699
|
+
const base = [];
|
|
1700
|
+
const variantClasses = [];
|
|
1701
|
+
const compoundClasses = [];
|
|
1702
|
+
const tagClasses = [];
|
|
1703
|
+
collectStringValues(getProperty(stylingObj, "base"), base);
|
|
1704
|
+
const variantsObj = asObject(getProperty(stylingObj, "variants"));
|
|
1705
|
+
if (variantsObj) iterate.forEach(variantsObj.properties, (dimProp) => {
|
|
1706
|
+
if (!ts.isPropertyAssignment(dimProp)) return;
|
|
1707
|
+
const valuesObj = asObject(dimProp.initializer);
|
|
1708
|
+
if (!valuesObj) return;
|
|
1709
|
+
iterate.forEach(valuesObj.properties, (vp) => {
|
|
1710
|
+
if (!ts.isPropertyAssignment(vp)) return;
|
|
1711
|
+
collectStringValues(vp.initializer, variantClasses);
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
const compoundsArr = asArray(getProperty(stylingObj, "compounds"));
|
|
1715
|
+
if (compoundsArr) iterate.forEach(compoundsArr.elements, (elem) => {
|
|
1716
|
+
const obj = asObject(elem);
|
|
1717
|
+
if (!obj) return;
|
|
1718
|
+
collectStringValues(getProperty(obj, "class"), compoundClasses);
|
|
1719
|
+
});
|
|
1720
|
+
const tagsObj = asObject(getProperty(stylingObj, "tags"));
|
|
1721
|
+
if (tagsObj) iterate.forEach(tagsObj.properties, (tp) => {
|
|
1722
|
+
if (!ts.isPropertyAssignment(tp)) return;
|
|
1723
|
+
collectStringValues(tp.initializer, tagClasses);
|
|
1724
|
+
});
|
|
1725
|
+
return {
|
|
1726
|
+
base,
|
|
1727
|
+
variantClasses,
|
|
1728
|
+
compoundClasses,
|
|
1729
|
+
tagClasses
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
/**
|
|
1733
|
+
* Collects design tokens from all factory calls in a single source file.
|
|
1734
|
+
* Each entry in the returned map is keyed by the component variable name.
|
|
1735
|
+
*
|
|
1736
|
+
* Only `const X = factory(...)` declarations are handled; exported or
|
|
1737
|
+
* destructured patterns fall through (same scope as `collectConstraints`).
|
|
1738
|
+
*/
|
|
1739
|
+
function collectFileTokens(source, calleeNames) {
|
|
1740
|
+
const result = /* @__PURE__ */ new Map();
|
|
1741
|
+
ts.forEachChild(source, (stmt) => {
|
|
1742
|
+
if (!ts.isVariableStatement(stmt)) return;
|
|
1743
|
+
iterate.forEach(stmt.declarationList.declarations, (decl) => {
|
|
1744
|
+
const { initializer, name } = decl;
|
|
1745
|
+
if (!initializer || !ts.isCallExpression(initializer)) return;
|
|
1746
|
+
if (!isFactoryCall(initializer, calleeNames)) return;
|
|
1747
|
+
const arg = firstObjectArg(initializer);
|
|
1748
|
+
if (!arg) return;
|
|
1749
|
+
const stylingObj = asObject(getProperty(arg, "styling"));
|
|
1750
|
+
if (!stylingObj) return;
|
|
1751
|
+
const definedName = ts.isIdentifier(name) ? name.text : void 0;
|
|
1752
|
+
if (!definedName) return;
|
|
1753
|
+
result.set(definedName, extractStylingTokens(stylingObj));
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
return result;
|
|
1757
|
+
}
|
|
1758
|
+
/** Merges two ComponentTokens with deduplication per class list. Returns `incoming` when `existing` is undefined. */
|
|
1759
|
+
function mergeTokens(existing, incoming) {
|
|
1760
|
+
if (!existing) return incoming;
|
|
1761
|
+
return {
|
|
1762
|
+
base: [.../* @__PURE__ */ new Set([...existing.base, ...incoming.base])],
|
|
1763
|
+
variantClasses: [.../* @__PURE__ */ new Set([...existing.variantClasses, ...incoming.variantClasses])],
|
|
1764
|
+
compoundClasses: [.../* @__PURE__ */ new Set([...existing.compoundClasses, ...incoming.compoundClasses])],
|
|
1765
|
+
tagClasses: [.../* @__PURE__ */ new Set([...existing.tagClasses, ...incoming.tagClasses])]
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Builds a DesignTokenManifest from the accumulated per-component token maps,
|
|
1770
|
+
* including a flat sorted `allClasses` union.
|
|
1771
|
+
*
|
|
1772
|
+
* `allClasses` always includes every `layoutKeys` display value (`flex`,
|
|
1773
|
+
* `inline-flex`, `grid`, `hidden`, etc.) even when no scanned component uses
|
|
1774
|
+
* them literally. Those classes are only ever assembled at runtime by
|
|
1775
|
+
* `createTailwindPipeline` from boolean props, so they never appear as a
|
|
1776
|
+
* string literal for Tailwind's content scanner (or this manifest) to find
|
|
1777
|
+
* on its own — without this, Tailwind silently drops their generated CSS.
|
|
1778
|
+
*/
|
|
1779
|
+
function buildManifest(allTokens) {
|
|
1780
|
+
const components = {};
|
|
1781
|
+
const seen = new Set(layoutKeys);
|
|
1782
|
+
iterate.forEach(allTokens, ([name, tokens]) => {
|
|
1783
|
+
components[name] = tokens;
|
|
1784
|
+
iterate.forEach([
|
|
1785
|
+
...tokens.base,
|
|
1786
|
+
...tokens.variantClasses,
|
|
1787
|
+
...tokens.compoundClasses,
|
|
1788
|
+
...tokens.tagClasses
|
|
1789
|
+
], (cls) => {
|
|
1790
|
+
iterate.forEach(cls.split(/\s+/), (part) => {
|
|
1791
|
+
if (part) seen.add(part);
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
1794
|
+
});
|
|
1795
|
+
return {
|
|
1796
|
+
components,
|
|
1797
|
+
allClasses: [...seen].sort()
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
/**
|
|
1801
|
+
* Vite plugin that collects every statically-declared class string from
|
|
1802
|
+
* `createContractComponent` factory calls and writes a design token manifest
|
|
1803
|
+
* to a JSON file on each build.
|
|
1804
|
+
*
|
|
1805
|
+
* The manifest contains per-component class lists and a flat `allClasses`
|
|
1806
|
+
* union that can be used as a Tailwind content source to prevent purging of
|
|
1807
|
+
* variant classes.
|
|
1808
|
+
*
|
|
1809
|
+
* @example
|
|
1810
|
+
* // vite.config.ts
|
|
1811
|
+
* import { designTokensPlugin } from 'praxis-kit/vite-plugin'
|
|
1812
|
+
* export default { plugins: [designTokensPlugin({ outFile: 'praxis-tokens.json' })] }
|
|
1813
|
+
*
|
|
1814
|
+
* @example
|
|
1815
|
+
* // tailwind.config.js
|
|
1816
|
+
* export default { content: ['./src/**', './praxis-tokens.json'] }
|
|
1817
|
+
*/
|
|
1818
|
+
function designTokensPlugin(options) {
|
|
1819
|
+
const calleeNames = new Set(options?.calleeNames ?? DEFAULT_CALLEE_NAMES);
|
|
1820
|
+
const outFile = options?.outFile ?? "praxis-tokens.json";
|
|
1821
|
+
const accumulated = /* @__PURE__ */ new Map();
|
|
1822
|
+
let root = process.cwd();
|
|
1823
|
+
return {
|
|
1824
|
+
name: "praxis-kit:design-tokens",
|
|
1825
|
+
configResolved(config) {
|
|
1826
|
+
root = config.root;
|
|
1827
|
+
},
|
|
1828
|
+
buildStart() {
|
|
1829
|
+
accumulated.clear();
|
|
1830
|
+
},
|
|
1831
|
+
transform(code, id) {
|
|
1832
|
+
const ext = id.split(".").pop() ?? "";
|
|
1833
|
+
if (!ALL_EXTS.has(ext)) return null;
|
|
1834
|
+
const source = parseSource(id, code);
|
|
1835
|
+
iterate.forEach(collectFileTokens(source, calleeNames), ([name, tokens]) => {
|
|
1836
|
+
accumulated.set(name, mergeTokens(accumulated.get(name), tokens));
|
|
1837
|
+
});
|
|
1838
|
+
return null;
|
|
1839
|
+
},
|
|
1840
|
+
writeBundle() {
|
|
1841
|
+
if (accumulated.size === 0) return;
|
|
1842
|
+
const manifest = buildManifest(accumulated);
|
|
1843
|
+
writeFileSync(resolve(root, outFile), JSON.stringify(manifest, null, 2));
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1848
|
+
//#region ../../plugins/vite/src/index.ts
|
|
1849
|
+
/**
|
|
1850
|
+
* Vite plugin that performs static enforcement.children cardinality checks at
|
|
1851
|
+
* build time for components created with createContractComponent.
|
|
1852
|
+
*
|
|
1853
|
+
* **Single-file scope:** Components defined and used in the same `.tsx` / `.jsx`
|
|
1854
|
+
* file are validated during `transform`. JSX children containing expressions
|
|
1855
|
+
* (`{...}`) are skipped — their count is unknowable at compile time.
|
|
1856
|
+
*
|
|
1857
|
+
* **Cross-file scope:** Components imported from other files are validated in
|
|
1858
|
+
* `buildEnd` once the full constraint registry is populated. Only named imports
|
|
1859
|
+
* whose source file was also transformed by this plugin are checked.
|
|
1860
|
+
*
|
|
1861
|
+
* @example
|
|
1862
|
+
* // vite.config.ts
|
|
1863
|
+
* import { contractPlugin } from 'praxis-kit/vite-plugin'
|
|
1864
|
+
* export default { plugins: [contractPlugin()] }
|
|
1865
|
+
*/
|
|
1866
|
+
function contractPlugin(options) {
|
|
1867
|
+
const registry = new ConstraintRegistry();
|
|
1868
|
+
const calleeNames = new Set(options?.calleeNames ?? DEFAULT_CALLEE_NAMES);
|
|
1869
|
+
const severity = options?.severity ?? "warning";
|
|
1870
|
+
return {
|
|
1871
|
+
name: "praxis-kit:contract",
|
|
1872
|
+
async transform(code, id) {
|
|
1873
|
+
const ext = id.split(".").pop() ?? "";
|
|
1874
|
+
if (!JSX_EXTS.has(ext)) return null;
|
|
1875
|
+
const source = parseSource(id, code);
|
|
1876
|
+
const { constraints, importSpecifiers } = collectFileDeclarations(source, calleeNames);
|
|
1877
|
+
registry.registerConstraints(id, constraints);
|
|
1878
|
+
const { diagnostics, usages: allUsages } = analyzeJsxSites(source, constraints, severity);
|
|
1879
|
+
iterate.forEach(diagnostics, ({ col, diagnostic, line, severity }) => {
|
|
1880
|
+
const loc = {
|
|
1881
|
+
file: id,
|
|
1882
|
+
line,
|
|
1883
|
+
column: col
|
|
1884
|
+
};
|
|
1885
|
+
if (severity === "error") this.error({
|
|
1886
|
+
message: diagnostic.message,
|
|
1887
|
+
loc
|
|
1888
|
+
});
|
|
1889
|
+
else this.warn({
|
|
1890
|
+
message: diagnostic.message,
|
|
1891
|
+
loc
|
|
1892
|
+
});
|
|
1893
|
+
});
|
|
1894
|
+
const localNames = new Set(constraints.map((c) => c.name));
|
|
1895
|
+
const importedTagsInUse = new Set(allUsages.filter((u) => !localNames.has(u.tagName) && importSpecifiers.has(u.tagName)).map((u) => u.tagName));
|
|
1896
|
+
if (importedTagsInUse.size > 0) {
|
|
1897
|
+
const resolvedImports = /* @__PURE__ */ new Map();
|
|
1898
|
+
for (const [localName, { specifier }] of importSpecifiers) {
|
|
1899
|
+
if (!importedTagsInUse.has(localName)) continue;
|
|
1900
|
+
const resolved = await this.resolve(specifier, id);
|
|
1901
|
+
if (resolved) resolvedImports.set(localName, resolved.id);
|
|
1902
|
+
}
|
|
1903
|
+
registry.registerImports(id, resolvedImports);
|
|
1904
|
+
iterate.forEach(allUsages, (usage) => {
|
|
1905
|
+
if (importedTagsInUse.has(usage.tagName)) registry.addPendingUsage(id, usage);
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
buildEnd() {
|
|
1910
|
+
iterate.forEach(registry.diagnostics(severity), ({ col, diagnostic, fileId, line, severity }) => {
|
|
1911
|
+
const loc = {
|
|
1912
|
+
file: fileId,
|
|
1913
|
+
line,
|
|
1914
|
+
column: col
|
|
1915
|
+
};
|
|
1916
|
+
if (severity === "error") this.error({
|
|
1917
|
+
message: diagnostic.message,
|
|
1918
|
+
loc
|
|
1919
|
+
});
|
|
1920
|
+
else this.warn({
|
|
1921
|
+
message: diagnostic.message,
|
|
1922
|
+
loc
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Vite plugin that removes dead `styling.compounds` entries from factory calls
|
|
1930
|
+
* at build time, reducing bundle size and eliminating unreachable CVA compound
|
|
1931
|
+
* checks at runtime.
|
|
1932
|
+
*
|
|
1933
|
+
* A compound entry is dead when any of its conditions reference a variant key
|
|
1934
|
+
* that does not exist in `styling.variants`, or a value that is not valid for
|
|
1935
|
+
* that key. Only entries whose conditions are fully static (string/array
|
|
1936
|
+
* literals) are pruned — dynamic conditions are left unchanged.
|
|
1937
|
+
*
|
|
1938
|
+
* Place before `contractPlugin` so the pruned source is what gets analyzed.
|
|
1939
|
+
*
|
|
1940
|
+
* @example
|
|
1941
|
+
* // vite.config.ts
|
|
1942
|
+
* import { compoundPrunePlugin, contractPlugin } from 'praxis-kit/vite-plugin'
|
|
1943
|
+
* export default { plugins: [compoundPrunePlugin(), contractPlugin()] }
|
|
1944
|
+
*/
|
|
1945
|
+
function compoundPrunePlugin(options) {
|
|
1946
|
+
const calleeNames = new Set(options?.calleeNames ?? DEFAULT_CALLEE_NAMES);
|
|
1947
|
+
return {
|
|
1948
|
+
name: "praxis-kit:compound-prune",
|
|
1949
|
+
transform(code, id) {
|
|
1950
|
+
const ext = id.split(".").pop() ?? "";
|
|
1951
|
+
if (!ALL_EXTS.has(ext)) return null;
|
|
1952
|
+
const result = pruneDeadCompounds(parseSource(id, code), calleeNames);
|
|
1953
|
+
return result !== null ? { code: result } : null;
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Vite plugin that pre-computes variant class strings at build time and injects
|
|
1959
|
+
* them as a static `precomputedClasses` map into each factory call's `styling`
|
|
1960
|
+
* object.
|
|
1961
|
+
*
|
|
1962
|
+
* At runtime, `VariantClassResolver` checks this map before calling CVA — a
|
|
1963
|
+
* plain object lookup replaces a CVA invocation + LRU cache write for every
|
|
1964
|
+
* statically-known combination. Only combinations that appear in the map are
|
|
1965
|
+
* accelerated; invalid or dynamic variant values fall through to the existing
|
|
1966
|
+
* compute path unchanged.
|
|
1967
|
+
*
|
|
1968
|
+
* Injection is skipped when:
|
|
1969
|
+
* - `styling.variants` is absent or contains non-literal values
|
|
1970
|
+
* - `styling.compounds` contains non-literal conditions or classes
|
|
1971
|
+
* - The number of valid combinations exceeds 512
|
|
1972
|
+
*
|
|
1973
|
+
* Place after `compoundPrunePlugin` so the injected map reflects the live
|
|
1974
|
+
* compound set.
|
|
1975
|
+
*
|
|
1976
|
+
* @example
|
|
1977
|
+
* // vite.config.ts
|
|
1978
|
+
* import { compoundPrunePlugin, classExtractPlugin, contractPlugin } from 'praxis-kit/vite-plugin'
|
|
1979
|
+
* export default { plugins: [compoundPrunePlugin(), classExtractPlugin(), contractPlugin()] }
|
|
1980
|
+
*/
|
|
1981
|
+
function classExtractPlugin(options) {
|
|
1982
|
+
const calleeNames = new Set(options?.calleeNames ?? DEFAULT_CALLEE_NAMES);
|
|
1983
|
+
return {
|
|
1984
|
+
name: "praxis-kit:class-extract",
|
|
1985
|
+
transform(code, id) {
|
|
1986
|
+
const ext = id.split(".").pop() ?? "";
|
|
1987
|
+
if (!ALL_EXTS.has(ext)) return null;
|
|
1988
|
+
const result = injectPrecomputedClasses(parseSource(id, code), calleeNames);
|
|
1989
|
+
return result !== null ? { code: result } : null;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Vite plugin that transforms `asChild` JSX usage sites to the render-prop form
|
|
1995
|
+
* at build time, eliminating the Slot/cloneElement/mergeProps runtime path.
|
|
1996
|
+
*
|
|
1997
|
+
* Only transforms sites where the transform is semantically safe:
|
|
1998
|
+
* - Exactly one static child element
|
|
1999
|
+
* - Child has no `className`, `style`, or event handler props
|
|
2000
|
+
*
|
|
2001
|
+
* Complex asChild patterns (conflicting props, dynamic children, Slottable
|
|
2002
|
+
* siblings) are left unchanged and handled by the runtime Slot path.
|
|
2003
|
+
*
|
|
2004
|
+
* Place before `contractPlugin` so cardinality analysis sees the transformed
|
|
2005
|
+
* source.
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* // vite.config.ts
|
|
2009
|
+
* import { slotTransformPlugin, contractPlugin } from 'praxis-kit/vite-plugin'
|
|
2010
|
+
* export default { plugins: [slotTransformPlugin(), contractPlugin()] }
|
|
2011
|
+
*/
|
|
2012
|
+
function slotTransformPlugin() {
|
|
2013
|
+
return {
|
|
2014
|
+
name: "praxis-kit:slot-transform",
|
|
2015
|
+
transform(code, id) {
|
|
2016
|
+
const ext = id.split(".").pop() ?? "";
|
|
2017
|
+
if (!JSX_EXTS.has(ext)) return null;
|
|
2018
|
+
const result = transformAsChild(parseSource(id, code));
|
|
2019
|
+
return result !== null ? { code: result } : null;
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Vite plugin that replaces polymorphic component usage sites with direct
|
|
2025
|
+
* element creation at build time, eliminating the runtime render pipeline for
|
|
2026
|
+
* statically-analyzable usages.
|
|
2027
|
+
*
|
|
2028
|
+
* **Requires classExtractPlugin to run first** so that `precomputedClasses` is
|
|
2029
|
+
* present in the factory call before this plugin reads it. Place after
|
|
2030
|
+
* `classExtractPlugin` in the plugins array.
|
|
2031
|
+
*
|
|
2032
|
+
* A usage site is inlined when:
|
|
2033
|
+
* - The component is defined in the same file or imported from a file already
|
|
2034
|
+
* transformed by this plugin, with `precomputedClasses` injected
|
|
2035
|
+
* - No `as`, `asChild`, `render`, or spread attributes at the site
|
|
2036
|
+
* - All variant props are static string literals
|
|
2037
|
+
* - `className` is absent or a static string literal
|
|
2038
|
+
* - The factory config has no `defaults` or `enforcement`
|
|
2039
|
+
*
|
|
2040
|
+
* Cross-file inlining degrades gracefully when the definition file has not yet
|
|
2041
|
+
* been transformed (dev mode ordering, barrel re-exports, aliased imports).
|
|
2042
|
+
*
|
|
2043
|
+
* @example
|
|
2044
|
+
* // vite.config.ts
|
|
2045
|
+
* import { classExtractPlugin, staticCompositionPlugin } from 'praxis-kit/vite-plugin'
|
|
2046
|
+
* export default { plugins: [classExtractPlugin(), staticCompositionPlugin()] }
|
|
2047
|
+
*/
|
|
2048
|
+
function staticCompositionPlugin(options) {
|
|
2049
|
+
const calleeNames = new Set(options?.calleeNames ?? DEFAULT_CALLEE_NAMES);
|
|
2050
|
+
const registry = /* @__PURE__ */ new Map();
|
|
2051
|
+
return {
|
|
2052
|
+
name: "praxis-kit:static-compose",
|
|
2053
|
+
buildStart() {
|
|
2054
|
+
registry.clear();
|
|
2055
|
+
},
|
|
2056
|
+
async transform(code, id) {
|
|
2057
|
+
const ext = id.split(".").pop() ?? "";
|
|
2058
|
+
if (!JSX_EXTS.has(ext)) return null;
|
|
2059
|
+
const source = parseSource(id, code);
|
|
2060
|
+
const sameFile = extractStaticComponents(source, calleeNames);
|
|
2061
|
+
if (sameFile.size > 0) registry.set(id, sameFile);
|
|
2062
|
+
const importedComponents = /* @__PURE__ */ new Map();
|
|
2063
|
+
const importSpecifiers = extractImportSpecifiers(source);
|
|
2064
|
+
for (const [localName, { importedName, specifier }] of importSpecifiers) {
|
|
2065
|
+
const resolved = await this.resolve(specifier, id);
|
|
2066
|
+
if (!resolved) continue;
|
|
2067
|
+
const entry = registry.get(resolved.id);
|
|
2068
|
+
if (!entry) continue;
|
|
2069
|
+
const component = entry.get(importedName);
|
|
2070
|
+
if (component) importedComponents.set(localName, component);
|
|
2071
|
+
}
|
|
2072
|
+
const result = composeStatically(source, calleeNames, importedComponents);
|
|
2073
|
+
return result !== null ? { code: result } : null;
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Convenience plugin bundle that applies all three build-time rendering
|
|
2079
|
+
* optimisations in the correct dependency order:
|
|
2080
|
+
*
|
|
2081
|
+
* 1. `slotTransformPlugin` — rewrites `asChild` → render-prop form,
|
|
2082
|
+
* eliminating `cloneElement` for static sites
|
|
2083
|
+
* 2. `classExtractPlugin` — injects `precomputedClasses` into factory
|
|
2084
|
+
* calls for O(1) variant class resolution
|
|
2085
|
+
* 3. `staticCompositionPlugin` — inlines same-file static usages into direct
|
|
2086
|
+
* element creation, bypassing the runtime
|
|
2087
|
+
* pipeline entirely
|
|
2088
|
+
*
|
|
2089
|
+
* Place before `contractPlugin` so cardinality analysis sees the transformed
|
|
2090
|
+
* source. Especially effective for SSR builds where each component renders
|
|
2091
|
+
* exactly once per request and eliminates per-render pipeline overhead.
|
|
2092
|
+
*
|
|
2093
|
+
* @example
|
|
2094
|
+
* // vite.config.ts
|
|
2095
|
+
* import { ssrOptimizePlugin, contractPlugin } from 'praxis-kit/vite-plugin'
|
|
2096
|
+
* export default { plugins: [ssrOptimizePlugin(), contractPlugin()] }
|
|
2097
|
+
*/
|
|
2098
|
+
function ssrOptimizePlugin(options) {
|
|
2099
|
+
return [
|
|
2100
|
+
slotTransformPlugin(),
|
|
2101
|
+
classExtractPlugin(options),
|
|
2102
|
+
staticCompositionPlugin(options)
|
|
2103
|
+
];
|
|
2104
|
+
}
|
|
2105
|
+
//#endregion
|
|
2106
|
+
export { classExtractPlugin, compoundPrunePlugin, contractPlugin, designTokensPlugin, slotTransformPlugin, ssrOptimizePlugin, staticCompositionPlugin };
|