praxis-kit 0.1.0 → 1.0.2

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.
@@ -136,22 +136,12 @@ const iterate = Object.freeze({
136
136
  values
137
137
  });
138
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
- };
139
+ //#region ../../lib/foundation/src/type-guards.ts
140
+ function isDefined(value) {
141
+ return value !== void 0;
142
+ }
143
+ function isUndefined(value) {
144
+ return value === void 0;
155
145
  }
156
146
  //#endregion
157
147
  //#region ../../plugins/vite/src/ast.ts
@@ -868,6 +858,36 @@ const ALL_EXTS = /* @__PURE__ */ new Set([
868
858
  "jsx"
869
859
  ]);
870
860
  //#endregion
861
+ //#region ../../plugins/vite/src/jsx.ts
862
+ /**
863
+ * Small, shared JSX-AST vocabulary used by more than one transform/analysis file in this
864
+ * package (`diagnose.ts`, `slot-transform.ts`) — kept to genuinely-common helpers only, not a
865
+ * general dumping ground; see each function's own doc comment for exactly what it decides.
866
+ */
867
+ /**
868
+ * Returns a JSX tag name's identifier text, or `undefined` when the tag name isn't a plain
869
+ * identifier (e.g. a namespaced or member-access tag like `<foo.Bar />`).
870
+ *
871
+ * Deliberately narrow: only `ts.Identifier` is supported today. `ts.JsxTagNameExpression` also
872
+ * allows `PropertyAccessExpression`/`ThisExpression`/`JsxNamespacedName`, none of which any
873
+ * factory-based Praxis component can produce — add support only if the plugin actually needs to
874
+ * recognize a namespaced/member-access component tag, not speculatively.
875
+ */
876
+ function getJsxTagName(tagName) {
877
+ return ts.isIdentifier(tagName) ? tagName.text : void 0;
878
+ }
879
+ /**
880
+ * Returns true when `tagName` starts with an uppercase ASCII letter (A–Z) — JSX's own convention
881
+ * for "this is a component reference, not an intrinsic HTML tag" (`<Button>` vs `<button>`).
882
+ * Charcode comparison, not a `/^[A-Z]/` regex: this runs once per JSX site during a hot
883
+ * build-time AST walk, and a charcode check avoids the (admittedly small) regex engine overhead
884
+ * for a check this simple.
885
+ */
886
+ function isComponentTagName(tagName) {
887
+ const code = tagName.charCodeAt(0);
888
+ return code >= 65 && code <= 90;
889
+ }
890
+ //#endregion
871
891
  //#region ../../plugins/vite/src/vite-diagnostics.ts
872
892
  const ViteDiagnostics = {
873
893
  cardinalityViolation(name, totalMin, totalMax, receivedMin, receivedMax) {
@@ -890,78 +910,33 @@ const ViteDiagnostics = {
890
910
  };
891
911
  //#endregion
892
912
  //#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
- };
913
+ /** Extracts a {@link JsxSite} from a node, or `undefined` when the node isn't a JSX element/
914
+ * self-closing element, or its tag name isn't a plain identifier (e.g. `<foo.Bar />`). Centralizes
915
+ * the open-vs-self-closing branch every JSX-walking function in this file otherwise repeated. */
916
+ function getJsxSite(node) {
917
+ if (ts.isJsxElement(node)) {
918
+ const opening = node.openingElement;
919
+ const tagName = getJsxTagName(opening.tagName);
920
+ if (isUndefined(tagName)) return void 0;
921
+ const count = countJsxChildren(node.children);
922
+ return isUndefined(count) ? {
923
+ tagName,
924
+ attributes: opening.attributes
925
+ } : {
926
+ tagName,
927
+ attributes: opening.attributes,
928
+ count
929
+ };
930
+ }
931
+ if (ts.isJsxSelfClosingElement(node)) {
932
+ const tagName = getJsxTagName(node.tagName);
933
+ if (isUndefined(tagName)) return void 0;
934
+ return {
935
+ tagName,
936
+ attributes: node.attributes,
937
+ count: ZERO
938
+ };
939
+ }
965
940
  }
966
941
  const ZERO = {
967
942
  min: 0,
@@ -1014,7 +989,7 @@ function countExpression(node) {
1014
989
  const op = node.operatorToken.kind;
1015
990
  if (op === ts.SyntaxKind.AmpersandAmpersandToken) {
1016
991
  const right = countExpression(node.right);
1017
- if (!right) return void 0;
992
+ if (isUndefined(right)) return void 0;
1018
993
  return {
1019
994
  min: 0,
1020
995
  max: right.max
@@ -1023,7 +998,7 @@ function countExpression(node) {
1023
998
  if (op === ts.SyntaxKind.BarBarToken || op === ts.SyntaxKind.QuestionQuestionToken) {
1024
999
  const left = countExpression(node.left);
1025
1000
  const right = countExpression(node.right);
1026
- if (!left || !right) return void 0;
1001
+ if (isUndefined(left) || isUndefined(right)) return void 0;
1027
1002
  return {
1028
1003
  min: Math.min(left.min, right.min),
1029
1004
  max: Math.max(left.max, right.max)
@@ -1033,7 +1008,7 @@ function countExpression(node) {
1033
1008
  if (ts.isConditionalExpression(node)) {
1034
1009
  const whenTrue = countExpression(node.whenTrue);
1035
1010
  const whenFalse = countExpression(node.whenFalse);
1036
- if (!whenTrue || !whenFalse) return void 0;
1011
+ if (isUndefined(whenTrue) || isUndefined(whenFalse)) return void 0;
1037
1012
  return {
1038
1013
  min: Math.min(whenTrue.min, whenFalse.min),
1039
1014
  max: Math.max(whenTrue.max, whenFalse.max)
@@ -1050,7 +1025,7 @@ function countJsxChildren(children) {
1050
1025
  let max = 0;
1051
1026
  for (const child of children) {
1052
1027
  let c;
1053
- if (ts.isJsxExpression(child)) c = child.expression === void 0 ? ZERO : countExpression(child.expression);
1028
+ if (ts.isJsxExpression(child)) c = isUndefined(child.expression) ? ZERO : countExpression(child.expression);
1054
1029
  else if (ts.isJsxText(child)) c = child.text.trim().length > 0 ? ONE : ZERO;
1055
1030
  else if (ts.isJsxFragment(child)) c = countJsxChildren(child.children);
1056
1031
  else c = ONE;
@@ -1063,6 +1038,89 @@ function countJsxChildren(children) {
1063
1038
  max
1064
1039
  };
1065
1040
  }
1041
+ /** "Too many"/"too few" children for a component with a closed or minimum-bound children
1042
+ * contract. Only fires when `constraint` is present and `site.count` is statically known —
1043
+ * an unknowable count (`.map()`, a variable reference) is never treated as a violation. */
1044
+ function diagnoseCardinality(source, node, site, constraint, severity) {
1045
+ if (isUndefined(constraint) || isUndefined(site.count)) return void 0;
1046
+ const { count } = site;
1047
+ const { totalMin, totalMax, name, exclusiveChildren } = constraint;
1048
+ if (count.max < totalMin || exclusiveChildren && count.min > totalMax) {
1049
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
1050
+ return {
1051
+ diagnostic: ViteDiagnostics.cardinalityViolation(name, totalMin, totalMax, count.min, count.max),
1052
+ line: line + 1,
1053
+ col: character + 1,
1054
+ severity
1055
+ };
1056
+ }
1057
+ }
1058
+ /** A static `as="tag"` prop that overrides the default element on a component with ARIA
1059
+ * enforcement rules — not a hard error (the override may be intentional), just a site that
1060
+ * warrants review since it silently changes the implicit ARIA role. Only fires when
1061
+ * `constraint` is present; a dynamic/absent `as` is never flagged. */
1062
+ function diagnoseAriaOverride(source, node, site, constraint, severity) {
1063
+ if (!constraint) return [];
1064
+ const diagnostics = [];
1065
+ iterate.forEach(site.attributes.properties, (attr) => {
1066
+ if (!ts.isJsxAttribute(attr)) return;
1067
+ const { initializer, name } = attr;
1068
+ if ((ts.isIdentifier(name) ? name.text : void 0) !== "as" || isUndefined(initializer)) return;
1069
+ let asValue;
1070
+ if (ts.isStringLiteral(initializer)) asValue = initializer.text;
1071
+ else if (ts.isJsxExpression(initializer) && isDefined(initializer.expression) && ts.isStringLiteral(initializer.expression)) asValue = initializer.expression.text;
1072
+ if (isUndefined(asValue) || asValue === constraint.defaultTag) return;
1073
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
1074
+ diagnostics.push({
1075
+ diagnostic: ViteDiagnostics.ariaTagOverride(site.tagName, asValue, constraint.defaultTag),
1076
+ line: line + 1,
1077
+ col: character + 1,
1078
+ severity
1079
+ });
1080
+ });
1081
+ return diagnostics;
1082
+ }
1083
+ /**
1084
+ * Collects `site` as a `PendingUsage` for the cross-file validation queue, when its tag name
1085
+ * looks like a component reference (uppercase-led). Intentionally over-inclusive — it doesn't
1086
+ * check whether the tag resolves to an actual known Praxis component (that's exactly what
1087
+ * deferred cross-file validation at `buildEnd`, once the full constraint registry is
1088
+ * populated, still needs to determine); an intrinsic HTML tag (`<div>`) never reaches here.
1089
+ */
1090
+ function collectUsage(source, node, site) {
1091
+ if (!isComponentTagName(site.tagName)) return void 0;
1092
+ const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
1093
+ return {
1094
+ tagName: site.tagName,
1095
+ count: site.count,
1096
+ line: line + 1,
1097
+ col: character + 1
1098
+ };
1099
+ }
1100
+ /**
1101
+ * Single-pass variant of diagnoseUsages + diagnoseAriaTagOverrides + collectJsxUsages.
1102
+ * Visits each JSX node once and dispatches to all three checks, rather than walking
1103
+ * the source three times. For use in the hot Vite transform hook.
1104
+ */
1105
+ function analyzeJsxSites(source, constraints, severity) {
1106
+ const byName = new Map(constraints.filter((c) => c.rules.length > 0).map((c) => [c.name, c]));
1107
+ const byNameAria = new Map(constraints.filter((c) => c.hasAriaRules && isDefined(c.defaultTag)).map((c) => [c.name, c]));
1108
+ const diagnostics = [];
1109
+ const usages = [];
1110
+ walkEach(source, (node) => {
1111
+ const site = getJsxSite(node);
1112
+ if (!site) return;
1113
+ const cardinality = diagnoseCardinality(source, node, site, byName.get(site.tagName), severity);
1114
+ if (cardinality) diagnostics.push(cardinality);
1115
+ diagnostics.push(...diagnoseAriaOverride(source, node, site, byNameAria.get(site.tagName), severity));
1116
+ const usage = collectUsage(source, node, site);
1117
+ if (usage) usages.push(usage);
1118
+ });
1119
+ return {
1120
+ diagnostics,
1121
+ usages
1122
+ };
1123
+ }
1066
1124
  //#endregion
1067
1125
  //#region ../../plugins/vite/src/imports.ts
1068
1126
  /**
@@ -1188,9 +1246,15 @@ var ConstraintRegistry = class {
1188
1246
  * **Safety conditions** — the transform is skipped if any of these are true:
1189
1247
  * 1. The child has a dynamic `className` expression (cannot merge safely).
1190
1248
  * 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:
1249
+ * `className={_p.className ? `${_p.className} childCls` : 'childCls'}` — see
1250
+ * `buildClassNameMerge`'s own doc comment for why the guard is load-bearing, not defensive
1251
+ * styling.
1252
+ * 2. The child has a bare `style` or `on*` attribute without an initializer, or an `on*`
1253
+ * attribute whose value is provably not callable (`undefined`, `null`, `true`/`false`, a
1254
+ * numeric/string/bigint literal — see `isObviouslyNotCallable`'s own doc comment for why:
1255
+ * the composed handler calls the child's expression unconditionally, so `onClick={undefined}`
1256
+ * — valid, type-checked JSX — would otherwise throw `undefined is not a function` at
1257
+ * runtime). Static object-literal and expression-valued `style` props are merged:
1194
1258
  * `style={{..._p.style, ...childStyle}}`. Event handlers are composed:
1195
1259
  * `onClick={(_e) => { (childHandler)(_e); _p.onClick?.(_e); }}`.
1196
1260
  * 3. The component name starts with a lowercase letter (HTML intrinsic — not
@@ -1199,11 +1263,12 @@ var ConstraintRegistry = class {
1199
1263
  *
1200
1264
  * The transform is conservative: any condition that is not statically clear
1201
1265
  * causes the node to be left unchanged.
1266
+ *
1267
+ * The three generated-expression shapes above (`buildClassNameMerge`, `buildStyleMerge`,
1268
+ * `buildEventMerge`) are each their own named function, isolated from the JSX-parsing helpers
1269
+ * above them (`getStaticClassName`, `getStyleInfo`, `getEventHandlers`, …) — the parsing side
1270
+ * already had that boundary; the generation side didn't.
1202
1271
  */
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
1272
  /** Returns the attribute name string; empty string for namespaced names. */
1208
1273
  function jsxAttrName(attr) {
1209
1274
  return ts.isIdentifier(attr.name) ? attr.name.text : "";
@@ -1221,7 +1286,7 @@ function getStaticClassName(child) {
1221
1286
  absent: false,
1222
1287
  value: init.text
1223
1288
  };
1224
- if (ts.isJsxExpression(init) && init.expression !== void 0 && ts.isStringLiteral(init.expression)) return {
1289
+ if (ts.isJsxExpression(init) && isDefined(init.expression) && ts.isStringLiteral(init.expression)) return {
1225
1290
  absent: false,
1226
1291
  value: init.expression.text
1227
1292
  };
@@ -1235,7 +1300,7 @@ function getStyleInfo(child) {
1235
1300
  if (!ts.isJsxAttribute(attr) || jsxAttrName(attr) !== "style") continue;
1236
1301
  const init = attr.initializer;
1237
1302
  if (!init || ts.isStringLiteral(init)) return null;
1238
- if (ts.isJsxExpression(init) && init.expression !== void 0) return {
1303
+ if (ts.isJsxExpression(init) && isDefined(init.expression)) return {
1239
1304
  absent: false,
1240
1305
  expr: init.expression
1241
1306
  };
@@ -1243,6 +1308,27 @@ function getStyleInfo(child) {
1243
1308
  }
1244
1309
  return { absent: true };
1245
1310
  }
1311
+ /**
1312
+ * Whether `expr` is a literal/keyword that is *provably* not callable — `undefined`, `null`,
1313
+ * `true`/`false`, or a numeric/string/bigint literal. `buildEventMerge`'s generated code calls
1314
+ * the child's own handler expression unconditionally (`(childHandler)(_e)`), so a handler prop
1315
+ * whose value is one of these throws at runtime (`undefined is not a function`) even though
1316
+ * `onClick={undefined}` is itself perfectly valid, type-checked JSX (an optional handler prop
1317
+ * left unset via an explicit value rather than omitted). Deliberately narrow: an identifier,
1318
+ * member access, call expression, or arrow/function expression is left alone and treated as
1319
+ * callable — that's the overwhelming common case this transform exists to optimize, and this
1320
+ * transform has no type checker available to verify any of those further. This only catches the
1321
+ * subset of "not callable" that's provable from syntax alone.
1322
+ */
1323
+ function isObviouslyNotCallable(expr) {
1324
+ if (ts.isIdentifier(expr) && expr.text === "undefined") return true;
1325
+ switch (expr.kind) {
1326
+ case ts.SyntaxKind.NullKeyword:
1327
+ case ts.SyntaxKind.TrueKeyword:
1328
+ case ts.SyntaxKind.FalseKeyword: return true;
1329
+ default: return ts.isNumericLiteral(expr) || ts.isStringLiteral(expr) || ts.isBigIntLiteral(expr);
1330
+ }
1331
+ }
1246
1332
  function getEventHandlers(child) {
1247
1333
  const attrs = ts.isJsxElement(child) ? child.openingElement.attributes.properties : child.attributes.properties;
1248
1334
  const handlers = [];
@@ -1252,7 +1338,8 @@ function getEventHandlers(child) {
1252
1338
  if (!/^on[A-Z]/.test(name)) continue;
1253
1339
  const init = attr.initializer;
1254
1340
  if (!init) return null;
1255
- if (ts.isJsxExpression(init) && init.expression !== void 0) {
1341
+ if (ts.isJsxExpression(init) && isDefined(init.expression)) {
1342
+ if (isObviouslyNotCallable(init.expression)) return null;
1256
1343
  handlers.push({
1257
1344
  name,
1258
1345
  expr: init.expression
@@ -1268,8 +1355,8 @@ function hasAsChild(opening) {
1268
1355
  for (const attr of opening.attributes.properties) {
1269
1356
  if (!ts.isJsxAttribute(attr)) continue;
1270
1357
  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;
1358
+ if (isUndefined(attr.initializer)) return true;
1359
+ if (ts.isJsxExpression(attr.initializer) && isDefined(attr.initializer.expression) && attr.initializer.expression.kind === ts.SyntaxKind.TrueKeyword) return true;
1273
1360
  }
1274
1361
  return false;
1275
1362
  }
@@ -1296,13 +1383,49 @@ function getTagName(child) {
1296
1383
  return ts.isIdentifier(tag) ? tag.text : void 0;
1297
1384
  }
1298
1385
  /**
1386
+ * Builds the merged `className` expression.
1387
+ *
1388
+ * `_p.className` can genuinely be `undefined` at runtime — `resolveClasses`
1389
+ * (`lib/styling/src/create-class-pipeline.ts`) resolves to `undefined`, not `''`, for a component
1390
+ * with no base/variant classes and no caller-supplied `className` (confirmed by reading that
1391
+ * function directly, not assumed). An unconditional `_p.className + ' childCls'` would then
1392
+ * concatenate onto `undefined`, producing the literal string `"undefined childCls"` on the DOM —
1393
+ * a real bug this guard closes, not defensive styling. Generates
1394
+ * `_p.className ? `${_p.className} childCls` : 'childCls'`.
1395
+ */
1396
+ function buildClassNameMerge(factory, childClassName) {
1397
+ const pClassName = () => factory.createPropertyAccessExpression(factory.createIdentifier("_p"), "className");
1398
+ const whenTrue = factory.createTemplateExpression(factory.createTemplateHead(""), [factory.createTemplateSpan(pClassName(), factory.createTemplateTail(` ${childClassName}`))]);
1399
+ const whenFalse = factory.createStringLiteral(childClassName);
1400
+ return factory.createConditionalExpression(pClassName(), factory.createToken(ts.SyntaxKind.QuestionToken), whenTrue, factory.createToken(ts.SyntaxKind.ColonToken), whenFalse);
1401
+ }
1402
+ /**
1403
+ * Builds the merged `style` object expression: `{ ..._p.style, ...childStyle }` — the child's own
1404
+ * object-literal properties are inlined directly after the `_p.style` spread when the child's
1405
+ * style is itself an object literal (readable output, e.g. `{ ..._p.style, color: 'red' }`);
1406
+ * a non-literal style expression (a variable reference) is spread instead, since its own shape
1407
+ * isn't known statically.
1408
+ */
1409
+ function buildStyleMerge(factory, styleExpr) {
1410
+ const pStyleSpread = factory.createSpreadAssignment(factory.createPropertyAccessExpression(factory.createIdentifier("_p"), "style"));
1411
+ if (ts.isObjectLiteralExpression(styleExpr)) return factory.createObjectLiteralExpression([pStyleSpread, ...styleExpr.properties], false);
1412
+ return factory.createObjectLiteralExpression([pStyleSpread, factory.createSpreadAssignment(styleExpr)], false);
1413
+ }
1414
+ /** Builds one composed event-handler arrow function: `(_e) => { (childHandler)(_e); _p.name?.(_e); }` —
1415
+ * the child's own handler always runs, then the parent's (if any) via an optional-chain call, so
1416
+ * a parent that never set this handler doesn't throw. */
1417
+ function buildEventMerge(factory, name, childHandlerExpr) {
1418
+ const eParam = factory.createParameterDeclaration(void 0, void 0, "_e");
1419
+ const callChild = factory.createExpressionStatement(factory.createCallExpression(factory.createParenthesizedExpression(childHandlerExpr), void 0, [factory.createIdentifier("_e")]));
1420
+ const callParent = factory.createExpressionStatement(factory.createCallChain(factory.createPropertyAccessExpression(factory.createIdentifier("_p"), name), factory.createToken(ts.SyntaxKind.QuestionDotToken), void 0, [factory.createIdentifier("_e")]));
1421
+ return factory.createArrowFunction(void 0, void 0, [eParam], void 0, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([callChild, callParent], true));
1422
+ }
1423
+ /**
1299
1424
  * Builds the attribute list for the transformed parent, omitting `asChild`
1300
1425
  * and adding `render={(_p) => <childTag ...childAttrs {..._p} />}`.
1301
1426
  *
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); }}`
1427
+ * Merged props are placed after the `{..._p}` spread so they override _p — see
1428
+ * `buildClassNameMerge`/`buildStyleMerge`/`buildEventMerge` for each merge's own shape.
1306
1429
  */
1307
1430
  function buildTransformedAttributes(factory, original, child, tagName, clsResult, styleInfo, handlers) {
1308
1431
  const parentAttrs = original.attributes.properties.filter((attr) => !(ts.isJsxAttribute(attr) && jsxAttrName(attr) === "asChild"));
@@ -1313,25 +1436,9 @@ function buildTransformedAttributes(factory, original, child, tagName, clsResult
1313
1436
  const childContent = ts.isJsxElement(child) ? child.children : void 0;
1314
1437
  const spreadProp = factory.createJsxSpreadAttribute(factory.createIdentifier("_p"));
1315
1438
  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
- }
1439
+ if (hasStaticCls && clsResult.value !== "") extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier("className"), factory.createJsxExpression(void 0, buildClassNameMerge(factory, clsResult.value))));
1440
+ if (hasStyle) extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier("style"), factory.createJsxExpression(void 0, buildStyleMerge(factory, styleInfo.expr))));
1441
+ for (const { name, expr } of handlers) extraAttrs.push(factory.createJsxAttribute(factory.createIdentifier(name), factory.createJsxExpression(void 0, buildEventMerge(factory, name, expr))));
1335
1442
  const childAttrsWithSpread = factory.createJsxAttributes([
1336
1443
  ...childOpeningAttrs,
1337
1444
  spreadProp,
@@ -1351,8 +1458,8 @@ function createAsChildTransformer(factory) {
1351
1458
  function visit(node) {
1352
1459
  if (!ts.isJsxElement(node)) return ts.visitEachChild(node, visit, context);
1353
1460
  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);
1461
+ const tagName = getJsxTagName(opening.tagName);
1462
+ if (!tagName || !isComponentTagName(tagName)) return ts.visitEachChild(node, visit, context);
1356
1463
  if (!hasAsChild(opening)) return ts.visitEachChild(node, visit, context);
1357
1464
  const child = getSingleElementChild(node);
1358
1465
  if (!child) return ts.visitEachChild(node, visit, context);
@@ -1392,45 +1499,9 @@ function transformAsChild(source) {
1392
1499
  }
1393
1500
  //#endregion
1394
1501
  //#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
1502
  /** Returns the text of a string literal node, or undefined. */
1432
1503
  function asStringLiteral(node) {
1433
- return node !== void 0 && ts.isStringLiteral(node) ? node.text : void 0;
1504
+ return isDefined(node) && ts.isStringLiteral(node) ? node.text : void 0;
1434
1505
  }
1435
1506
  /**
1436
1507
  * Walks the source file and extracts metadata for each same-file factory call
@@ -1457,47 +1528,73 @@ function extractStaticComponents(source, calleeNames) {
1457
1528
  const precomputedNode = asObject(getProperty(stylingObj, "precomputedClasses"));
1458
1529
  if (!precomputedNode) return;
1459
1530
  const precomputedClasses = {};
1460
- if (iterate.find(precomputedNode.properties, (prop) => {
1461
- if (!ts.isPropertyAssignment(prop)) return true;
1531
+ let sawMalformedEntry = false;
1532
+ for (const prop of precomputedNode.properties) {
1533
+ if (!ts.isPropertyAssignment(prop)) {
1534
+ sawMalformedEntry = true;
1535
+ break;
1536
+ }
1462
1537
  const { initializer, name } = prop;
1463
1538
  const key = ts.isStringLiteral(name) ? name.text : void 0;
1464
1539
  const val = asStringLiteral(initializer);
1465
- if (key === void 0 || val === void 0) return true;
1540
+ if (isUndefined(key) || isUndefined(val)) {
1541
+ sawMalformedEntry = true;
1542
+ break;
1543
+ }
1466
1544
  precomputedClasses[key] = val;
1467
- return null;
1468
- })) return;
1469
- if (getProperty(arg, "defaults") !== void 0) return;
1470
- if (getProperty(arg, "enforcement") !== void 0) return;
1545
+ }
1546
+ if (sawMalformedEntry) return;
1547
+ if (isDefined(getProperty(arg, "defaults"))) return;
1548
+ if (isDefined(getProperty(arg, "enforcement"))) return;
1471
1549
  const variantKeys = /* @__PURE__ */ new Set();
1472
1550
  const variantsObj = asObject(getProperty(stylingObj, "variants"));
1473
1551
  if (variantsObj) iterate.forEach(variantsObj.properties, (prop) => {
1474
1552
  if (ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))) variantKeys.add(prop.name.text);
1475
1553
  });
1554
+ const strippedProps = /* @__PURE__ */ new Set([
1555
+ ...variantKeys,
1556
+ "as",
1557
+ "asChild",
1558
+ "render",
1559
+ "className"
1560
+ ]);
1476
1561
  result.set(varName, {
1477
1562
  defaultTag,
1478
1563
  variantKeys,
1479
- precomputedClasses
1564
+ precomputedClasses,
1565
+ strippedProps
1480
1566
  });
1481
1567
  });
1482
1568
  return result;
1483
1569
  }
1484
- /** Reads the value of a named JSX attribute from the attribute list: absent, a static string, or dynamic. */
1485
- function readAttrValue(attrs, name) {
1570
+ /**
1571
+ * Reads the value of a named JSX attribute from the attribute list: absent, a static string, or
1572
+ * dynamic.
1573
+ *
1574
+ * A bare shorthand attribute (`<Button size />`) is `'dynamic'`, not `'static'` with an empty
1575
+ * string: JSX gives a bare attribute the runtime value `true` (boolean), never `''`. Every
1576
+ * consumer of this function that reasons about a variant/tag/className value only ever expects a
1577
+ * *string* — `true` can never satisfy a `size:s:sm`-style precomputed-class cache key or a real
1578
+ * variant's string-keyed value map, so treating it as truly unknown (rather than silently
1579
+ * collapsing it to `''`) is both the more accurate representation of what the runtime actually
1580
+ * sees and the conservative choice: it bails to the runtime path instead of risking a wrong
1581
+ * inlined class string for a `variants: { size: { '': ... } }`-shaped config, where the old `''`
1582
+ * representation could have coincidentally matched a real (if unusual) empty-string variant
1583
+ * value.
1584
+ */
1585
+ function readStaticAttribute(attrs, name) {
1486
1586
  for (const attr of attrs) {
1487
1587
  if (!ts.isJsxAttribute(attr)) continue;
1488
1588
  if (!(ts.isIdentifier(attr.name) && attr.name.text === name)) continue;
1489
1589
  const init = attr.initializer;
1490
- if (!init) return {
1491
- kind: "string",
1492
- value: ""
1493
- };
1590
+ if (!init) return { kind: "dynamic" };
1494
1591
  if (ts.isStringLiteral(init)) return {
1495
- kind: "string",
1592
+ kind: "static",
1496
1593
  value: init.text
1497
1594
  };
1498
1595
  if (ts.isJsxExpression(init) && init.expression !== void 0) {
1499
1596
  if (ts.isStringLiteral(init.expression)) return {
1500
- kind: "string",
1597
+ kind: "static",
1501
1598
  value: init.expression.text
1502
1599
  };
1503
1600
  return { kind: "dynamic" };
@@ -1519,16 +1616,16 @@ function createStaticCompositionTransformer(factory, components, onInlined) {
1519
1616
  if (!info) return ts.visitEachChild(node, visit, context);
1520
1617
  const attrList = isOpen ? node.openingElement.attributes.properties : node.attributes.properties;
1521
1618
  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");
1619
+ if (readStaticAttribute(attrList, "asChild").kind !== "absent") return ts.visitEachChild(node, visit, context);
1620
+ if (readStaticAttribute(attrList, "render").kind !== "absent") return ts.visitEachChild(node, visit, context);
1621
+ const asVal = readStaticAttribute(attrList, "as");
1525
1622
  if (asVal.kind === "dynamic") return ts.visitEachChild(node, visit, context);
1526
- const outputTag = asVal.kind === "string" ? asVal.value : info.defaultTag;
1623
+ const outputTag = asVal.kind === "static" ? asVal.value : info.defaultTag;
1527
1624
  const variantProps = {};
1528
1625
  for (const propName of info.variantKeys) {
1529
- const val = readAttrValue(attrList, propName);
1626
+ const val = readStaticAttribute(attrList, propName);
1530
1627
  if (val.kind === "absent") continue;
1531
- if (val.kind === "string") {
1628
+ if (val.kind === "static") {
1532
1629
  variantProps[propName] = val.value;
1533
1630
  continue;
1534
1631
  }
@@ -1536,23 +1633,16 @@ function createStaticCompositionTransformer(factory, components, onInlined) {
1536
1633
  }
1537
1634
  const cacheKey = buildCacheKey(variantProps);
1538
1635
  const baseClass = info.precomputedClasses[cacheKey];
1539
- if (baseClass === void 0) return ts.visitEachChild(node, visit, context);
1540
- const clsVal = readAttrValue(attrList, "className");
1636
+ if (isUndefined(baseClass)) return ts.visitEachChild(node, visit, context);
1637
+ const clsVal = readStaticAttribute(attrList, "className");
1541
1638
  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
- ]);
1639
+ const finalClass = clsVal.kind === "static" && clsVal.value ? `${baseClass} ${clsVal.value}` : baseClass;
1550
1640
  const outputAttrs = [factory.createJsxAttribute(factory.createIdentifier("className"), factory.createStringLiteral(finalClass))];
1551
1641
  for (const attr of attrList) {
1552
1642
  if (ts.isJsxSpreadAttribute(attr)) continue;
1553
1643
  if (!ts.isJsxAttribute(attr)) continue;
1554
1644
  const name = ts.isIdentifier(attr.name) ? attr.name.text : "";
1555
- if (strip.has(name)) continue;
1645
+ if (info.strippedProps.has(name)) continue;
1556
1646
  outputAttrs.push(attr);
1557
1647
  }
1558
1648
  const newAttrs = factory.createJsxAttributes(outputAttrs);