praxis-kit 0.1.1 → 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.
- package/dist/{build-runtime-CJ_nQEaZ.js → build-runtime-cNkW-D62.js} +331 -207
- package/dist/contract/index.d.ts +396 -14
- package/dist/contract/index.js +66 -1
- package/dist/eslint/index.js +11 -5
- package/dist/{index-DiPzmjWW.d.ts → index-zJDSXTxL.d.ts} +342 -66
- package/dist/lit/index.d.ts +251 -55
- package/dist/lit/index.js +275 -215
- package/dist/preact/index.d.ts +296 -52
- package/dist/preact/index.js +277 -210
- package/dist/react/index.d.ts +13 -4
- package/dist/react/index.js +22 -72
- package/dist/react/legacy.d.ts +9 -4
- package/dist/react/legacy.js +14 -2
- package/dist/solid/index.d.ts +253 -40
- package/dist/solid/index.js +271 -210
- package/dist/svelte/index.d.ts +203 -33
- package/dist/svelte/index.js +270 -207
- package/dist/tailwind/index.d.ts +12 -1
- package/dist/vite-plugin/index.js +289 -199
- package/dist/vue/index.d.ts +252 -41
- package/dist/vue/index.js +277 -210
- package/dist/web/index.d.ts +271 -53
- package/dist/web/index.js +275 -215
- package/package.json +5 -5
|
@@ -136,22 +136,12 @@ const iterate = Object.freeze({
|
|
|
136
136
|
values
|
|
137
137
|
});
|
|
138
138
|
//#endregion
|
|
139
|
-
//#region ../../lib/
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
*
|
|
895
|
-
*
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
* `{
|
|
1192
|
-
*
|
|
1193
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1272
|
-
if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression
|
|
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
|
-
*
|
|
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
|
-
|
|
1318
|
-
|
|
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 =
|
|
1355
|
-
if (!tagName || !
|
|
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
|
|
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
|
-
|
|
1461
|
-
|
|
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
|
|
1540
|
+
if (isUndefined(key) || isUndefined(val)) {
|
|
1541
|
+
sawMalformedEntry = true;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1466
1544
|
precomputedClasses[key] = val;
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (getProperty(arg, "defaults")
|
|
1470
|
-
if (getProperty(arg, "enforcement")
|
|
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
|
-
/**
|
|
1485
|
-
|
|
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: "
|
|
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: "
|
|
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 (
|
|
1523
|
-
if (
|
|
1524
|
-
const asVal =
|
|
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 === "
|
|
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 =
|
|
1626
|
+
const val = readStaticAttribute(attrList, propName);
|
|
1530
1627
|
if (val.kind === "absent") continue;
|
|
1531
|
-
if (val.kind === "
|
|
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
|
|
1540
|
-
const clsVal =
|
|
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 === "
|
|
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 (
|
|
1645
|
+
if (info.strippedProps.has(name)) continue;
|
|
1556
1646
|
outputAttrs.push(attr);
|
|
1557
1647
|
}
|
|
1558
1648
|
const newAttrs = factory.createJsxAttributes(outputAttrs);
|