@generaltranslation/vue-extractor 0.0.0 → 0.1.0-iris.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.
Files changed (87) hide show
  1. package/LICENSE.md +18 -102
  2. package/dist/detect.d.ts +8 -0
  3. package/dist/detect.js +2 -0
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +2 -1
  6. package/dist/inspect.d.ts +9 -0
  7. package/dist/inspect.js +2 -0
  8. package/dist/integration.d.ts +69 -0
  9. package/dist/integration.js +162 -0
  10. package/dist/integration.js.map +1 -0
  11. package/dist/internal/config/configFiles.d.ts +6 -0
  12. package/dist/internal/config/configFiles.js +34 -0
  13. package/dist/internal/config/configFiles.js.map +1 -0
  14. package/dist/internal/config/resolveVueCompilerOptions.d.ts +6 -0
  15. package/dist/internal/config/resolveVueCompilerOptions.js +74 -56
  16. package/dist/internal/config/resolveVueCompilerOptions.js.map +1 -1
  17. package/dist/internal/extractFromVueSource.d.ts +4 -3
  18. package/dist/internal/extractFromVueSource.js +255 -12
  19. package/dist/internal/extractFromVueSource.js.map +1 -1
  20. package/dist/internal/project/consumerUsage.d.ts +34 -0
  21. package/dist/internal/project/consumerUsage.js +1220 -0
  22. package/dist/internal/project/consumerUsage.js.map +1 -0
  23. package/dist/internal/project/detectVueProject.d.ts +13 -0
  24. package/dist/internal/project/detectVueProject.js +65 -0
  25. package/dist/internal/project/detectVueProject.js.map +1 -0
  26. package/dist/internal/project/extractFromVueProject.d.ts +10 -0
  27. package/dist/internal/project/extractFromVueProject.js +579 -0
  28. package/dist/internal/project/extractFromVueProject.js.map +1 -0
  29. package/dist/internal/project/inspectVueProject.d.ts +48 -0
  30. package/dist/internal/project/inspectVueProject.js +143 -0
  31. package/dist/internal/project/inspectVueProject.js.map +1 -0
  32. package/dist/internal/project/manifest.d.ts +64 -0
  33. package/dist/internal/project/manifest.js +270 -0
  34. package/dist/internal/project/manifest.js.map +1 -0
  35. package/dist/internal/project/mergeVueProjectExtraction.d.ts +18 -0
  36. package/dist/internal/project/mergeVueProjectExtraction.js +79 -0
  37. package/dist/internal/project/mergeVueProjectExtraction.js.map +1 -0
  38. package/dist/internal/project/moduleResolver.d.ts +18 -0
  39. package/dist/internal/project/moduleResolver.js +325 -0
  40. package/dist/internal/project/moduleResolver.js.map +1 -0
  41. package/dist/internal/project/scopes.d.ts +31 -0
  42. package/dist/internal/project/scopes.js +182 -0
  43. package/dist/internal/project/scopes.js.map +1 -0
  44. package/dist/internal/project/sourcePatterns.d.ts +4 -0
  45. package/dist/internal/project/sourcePatterns.js +32 -0
  46. package/dist/internal/project/sourcePatterns.js.map +1 -0
  47. package/dist/internal/project/viteAliases.d.ts +42 -0
  48. package/dist/internal/project/viteAliases.js +1020 -0
  49. package/dist/internal/project/viteAliases.js.map +1 -0
  50. package/dist/internal/project/vueSourceClassification.d.ts +13 -0
  51. package/dist/internal/project/vueSourceClassification.js +146 -0
  52. package/dist/internal/project/vueSourceClassification.js.map +1 -0
  53. package/dist/internal/project/workspaces.d.ts +31 -0
  54. package/dist/internal/project/workspaces.js +326 -0
  55. package/dist/internal/project/workspaces.js.map +1 -0
  56. package/dist/internal/project/wrapperProvenance.d.ts +21 -0
  57. package/dist/internal/project/wrapperProvenance.js +287 -0
  58. package/dist/internal/project/wrapperProvenance.js.map +1 -0
  59. package/dist/internal/script/analyze.js +252 -85
  60. package/dist/internal/script/analyze.js.map +1 -1
  61. package/dist/internal/script/jsx.d.ts +7 -4
  62. package/dist/internal/script/jsx.js +351 -79
  63. package/dist/internal/script/jsx.js.map +1 -1
  64. package/dist/internal/script/localModules.d.ts +23 -2
  65. package/dist/internal/script/localModules.js +533 -20
  66. package/dist/internal/script/localModules.js.map +1 -1
  67. package/dist/internal/script/model.d.ts +9 -2
  68. package/dist/internal/script/parser.js +2 -1
  69. package/dist/internal/script/parser.js.map +1 -1
  70. package/dist/internal/script/runtimeModules.d.ts +10 -0
  71. package/dist/internal/script/runtimeModules.js +27 -0
  72. package/dist/internal/script/runtimeModules.js.map +1 -0
  73. package/dist/internal/stringCalls.js +1 -1
  74. package/dist/internal/stringCalls.js.map +1 -1
  75. package/dist/internal/template.js +263 -141
  76. package/dist/internal/template.js.map +1 -1
  77. package/dist/internal/types.d.ts +4 -1
  78. package/dist/internal/utils.d.ts +14 -3
  79. package/dist/internal/utils.js +63 -3
  80. package/dist/internal/utils.js.map +1 -1
  81. package/dist/internal/vueCompiler.d.ts +9 -1
  82. package/dist/internal/vueCompiler.js +37 -14
  83. package/dist/internal/vueCompiler.js.map +1 -1
  84. package/dist/project.d.ts +11 -0
  85. package/dist/project.js +3 -0
  86. package/dist/types.d.ts +95 -4
  87. package/package.json +30 -4
@@ -1,9 +1,11 @@
1
- import { addVueError, babelLocation, createInlineMetadata, unwrapExpression } from "../utils.js";
1
+ import { addVueError, applyStaticTMetadataValue, babelLocation, createInlineMetadata, normalizeTMetadataKey, unwrapExpression } from "../utils.js";
2
2
  import { isAcceptedPluralForm } from "generaltranslation/internal";
3
3
  import * as babel from "@babel/types";
4
4
  import { HTML_CONTENT_PROPS } from "@generaltranslation/format/types";
5
5
  import { isHTMLTag, isSVGTag } from "@vue/shared";
6
6
  //#region src/internal/script/jsx.ts
7
+ /** Prevents source strings on opposite sides of a JSX comment from merging. */
8
+ const jsxTextBoundary = Symbol("gt-vue-jsx-text-boundary");
7
9
  const RESERVED_T_PROPS = new Set(["key", "ref"]);
8
10
  const FORMAT_COMPONENTS = new Set([
9
11
  "Currency",
@@ -28,10 +30,13 @@ const NON_BRANCH_PROP_NAMES = new Set([
28
30
  /**
29
31
  * Extracts one statically identified gt-vue `T` JSX element.
30
32
  *
31
- * This serializer follows the VNodes emitted by `@vue/babel-plugin-jsx`:
32
- * fragments are transparent, JSX text uses the plugin's normalization, and
33
- * arbitrary component slots remain opaque because gt-vue does not execute
34
- * user slots while calculating a source hash.
33
+ * This serializer follows the VNodes emitted by `@vue/babel-plugin-jsx` while
34
+ * preserving React's authoritative rich-source contract: authored Fragments
35
+ * are semantic elements, explicit arrays retain array cardinality, JSX text
36
+ * uses the plugin's normalization, and static custom-component default slots
37
+ * participate in the same source tree as React component children. Runtime
38
+ * values remain explicit variable boundaries and named component slots remain
39
+ * outside the outer translation.
35
40
  */
36
41
  function extractVueJSXTranslation(path, context, analysis) {
37
42
  if (analysis.customPragma) {
@@ -39,13 +44,13 @@ function extractVueJSXTranslation(path, context, analysis) {
39
44
  return;
40
45
  }
41
46
  const errorCount = context.errors.length;
42
- const translationContext = readTranslationContext(path.node.openingElement, path.scope, context, analysis);
43
- const source = collapseChildren(serializeChildren(path.node.children, { value: 0 }, path.scope, context, analysis));
47
+ const translationMetadata = readTranslationMetadata(path.node.openingElement, path.scope, context, analysis);
48
+ const source = collapseRootChildren(serializeChildren(path.node.children, { value: 0 }, path.scope, context, analysis));
44
49
  if (context.errors.length !== errorCount) return;
45
50
  context.results.push({
46
51
  dataFormat: "JSX",
47
52
  source,
48
- metadata: createInlineMetadata(context, babelLocation(path.node.loc), translationContext)
53
+ metadata: createInlineMetadata(context, babelLocation(path.node.loc), translationMetadata)
49
54
  });
50
55
  }
51
56
  /** Returns whether the visitor is already covered by an outer serialized T. */
@@ -56,13 +61,26 @@ function isNestedInVueJSXTranslation(path, analysis) {
56
61
  const element = current.node;
57
62
  const identity = resolveElementIdentity(element.openingElement.name, current.scope, analysis);
58
63
  const containingAttribute = findContainingJSXAttribute(path, current);
59
- if (containingAttribute && !(identity?.type === "component" && (identity.name === "Branch" || identity.name === "Plural") && isSlotsAttribute(containingAttribute.node))) return false;
64
+ if (containingAttribute) {
65
+ if (!isSlotsAttribute(containingAttribute.node)) return false;
66
+ if (identity?.type === "component" && (identity.name === "Branch" || identity.name === "Plural")) {
67
+ current = current.parentPath;
68
+ continue;
69
+ }
70
+ if (identity?.type === "component" && (identity.name === "Var" || FORMAT_COMPONENTS.has(identity.name))) return false;
71
+ const slotName = readContainingJSXSlotName(path, containingAttribute, analysis);
72
+ if (slotName !== "default") return slotName === void 0;
73
+ }
74
+ if (!containingAttribute && identity?.type !== "component" && (identity?.type !== "vue-builtin" || usesFragmentComponentSlots(element.openingElement.name, identity))) {
75
+ if ((identity ? { component: true } : resolveOrdinaryElementShape(element.openingElement.name, current.scope, analysis, /* @__PURE__ */ new Set()))?.component) {
76
+ const objectSlot = readContainingJSXObjectSlotName(path, current, analysis);
77
+ if (objectSlot.present && objectSlot.name !== "default") return objectSlot.name === void 0;
78
+ }
79
+ }
60
80
  if (identity?.type === "component") {
61
81
  if (identity.name === "T") return true;
62
82
  if (identity.name === "Var" || FORMAT_COMPONENTS.has(identity.name)) return false;
63
- } else if (identity?.type === "vue-builtin" && (identity.name !== "Fragment" || isTransparentFragment(element.openingElement.name, identity))) {
64
- if (identity.name === "Suspense" && containingAttribute && isSlotsAttribute(containingAttribute.node)) return false;
65
- } else if (!isLiteralFragment(element.openingElement.name) && !isNativeElement(element.openingElement.name)) return false;
83
+ } else if (identity?.type === "vue-builtin" && (identity.name !== "Fragment" || isAuthoredFragment(element.openingElement.name, identity))) {}
66
84
  }
67
85
  current = current.parentPath;
68
86
  }
@@ -80,6 +98,33 @@ function isSlotsAttribute(attribute) {
80
98
  const name = readAttributeName(attribute.name);
81
99
  return name === "v-slots" || name === "vSlots";
82
100
  }
101
+ /** Returns the direct static slot key containing one nested JSX element. */
102
+ function readContainingJSXSlotName(path, attribute, analysis) {
103
+ const expression = attribute.node.value?.type === "JSXExpressionContainer" && attribute.node.value.expression.type !== "JSXEmptyExpression" ? unwrapExpression(attribute.node.value.expression) : void 0;
104
+ if (expression?.type !== "ObjectExpression") return void 0;
105
+ let current = path.parentPath;
106
+ while (current && current.node !== expression) {
107
+ if (current.parentPath?.node === expression && (current.isObjectMethod() || current.isObjectProperty())) return readStaticObjectKey(current.node, attribute.scope, analysis);
108
+ current = current.parentPath;
109
+ }
110
+ }
111
+ /** Classifies a nested element inside Vue's object-child slot spelling. */
112
+ function readContainingJSXObjectSlotName(path, element, analysis) {
113
+ const meaningfulChildren = element.node.children.filter(isMeaningfulJSXChild);
114
+ const child = meaningfulChildren[0];
115
+ if (meaningfulChildren.length !== 1 || child?.type !== "JSXExpressionContainer" || child.expression.type === "JSXEmptyExpression") return { present: false };
116
+ const object = unwrapExpression(child.expression);
117
+ if (object?.type !== "ObjectExpression") return { present: false };
118
+ let current = path.parentPath;
119
+ while (current && current.node !== object) {
120
+ if (current.parentPath?.node === object && (current.isObjectMethod() || current.isObjectProperty())) return {
121
+ name: readStaticObjectKey(current.node, element.scope, analysis),
122
+ present: true
123
+ };
124
+ current = current.parentPath;
125
+ }
126
+ return { present: false };
127
+ }
83
128
  /** Resolves a JSX tag to the identity tracked by the script analyzer. */
84
129
  function resolveVueJSXElementIdentity(name, scope, analysis) {
85
130
  return resolveElementIdentity(name, scope, analysis);
@@ -88,63 +133,81 @@ function resolveVueJSXElementIdentity(name, scope, analysis) {
88
133
  function validateVueJSXVariableComponent(element, component, context) {
89
134
  validateVariableElement(element, component, context);
90
135
  }
91
- function readTranslationContext(element, scope, context, analysis) {
92
- let translationContext;
93
- let hasContext = false;
136
+ function readTranslationMetadata(element, scope, context, analysis) {
137
+ const metadata = {};
138
+ const seen = /* @__PURE__ */ new Set();
94
139
  for (const attribute of element.attributes) {
95
140
  if (attribute.type === "JSXSpreadAttribute") {
96
- addVueError(context, babelLocation(attribute.loc), "Found a spread prop on a gt-vue <T> component in JSX", "Pass context as one explicit static context or $context prop");
141
+ addVueError(context, babelLocation(attribute.loc), "Found a spread prop on a gt-vue <T> component in JSX", "Pass each metadata value through an explicit static prop");
97
142
  continue;
98
143
  }
99
144
  const name = readAttributeName(attribute.name);
100
145
  if (RESERVED_T_PROPS.has(name)) continue;
101
- if (name !== "context" && name !== "$context") {
102
- addVueError(context, babelLocation(attribute.loc), `Found unsupported prop "${name}" on a gt-vue <T> component`, "gt-vue <T> currently supports only context");
146
+ const key = normalizeTMetadataKey(name);
147
+ if (!key) {
148
+ addVueError(context, babelLocation(attribute.loc), `Found unsupported prop "${name}" on a gt-vue <T> component`, "Use only static context, id, maxChars, or requiresReview metadata");
103
149
  continue;
104
150
  }
105
- if (hasContext) {
106
- addVueError(context, babelLocation(attribute.loc), "Found duplicate context props on a gt-vue <T> component", "Pass only one context prop");
107
- continue;
108
- }
109
- hasContext = true;
151
+ if (!registerJSXTMetadataKey(key, seen, babelLocation(attribute.loc), context)) continue;
110
152
  const contextExpression = attribute.value?.type === "JSXExpressionContainer" && attribute.value.expression.type !== "JSXEmptyExpression" ? unwrapExpression(attribute.value.expression) : void 0;
111
- if (contextExpression?.type === "ConditionalExpression" || contextExpression?.type === "LogicalExpression") {
153
+ if (key === "context" && (contextExpression?.type === "ConditionalExpression" || contextExpression?.type === "LogicalExpression")) {
112
154
  addVueError(context, babelLocation(attribute.loc), "Found conditional context on a gt-vue <T> component", "Use one invariant static context string for this translation");
113
155
  continue;
114
156
  }
115
157
  const value = readJSXAttributePrimitive(attribute, scope, analysis);
116
- if (!value.ok || typeof value.value !== "string") {
117
- addVueError(context, babelLocation(attribute.loc), "Found a dynamic context on a gt-vue <T> component", "Use a string literal or an immutable static string");
158
+ if (!value.ok || !applyStaticTMetadataValue(metadata, key, value.value)) {
159
+ addInvalidJSXTMetadataValueError(key, babelLocation(attribute.loc), context);
118
160
  continue;
119
161
  }
120
- translationContext = value.value;
121
162
  }
122
- return translationContext;
163
+ return metadata;
164
+ }
165
+ function registerJSXTMetadataKey(key, seen, location, context) {
166
+ if (!seen.has(key)) {
167
+ seen.add(key);
168
+ return true;
169
+ }
170
+ addVueError(context, location, `Found duplicate ${key} props on a gt-vue <T> component`, `Pass only one ${key} prop or its $-prefixed alias`);
171
+ return false;
172
+ }
173
+ function addInvalidJSXTMetadataValueError(key, location, context) {
174
+ const expected = key === "context" || key === "id" ? "a static string" : key === "maxChars" ? "a static integer expression" : "a static boolean or a bare requiresReview prop";
175
+ addVueError(context, location, `Found an invalid or dynamic ${key} on a gt-vue <T> component`, `Use ${expected}`);
123
176
  }
124
177
  function serializeChildren(children, counter, scope, context, analysis) {
125
178
  const result = [];
126
- for (const child of children) for (const serialized of serializeChild(child, counter, scope, context, analysis)) appendSerializedChild(result, serialized);
127
- return result;
179
+ const runtimeArgumentCount = children.filter(isJSXRuntimeChildArgument).length;
180
+ const preserveScalarWireLiteral = runtimeArgumentCount === 1;
181
+ for (const child of children) for (const serialized of serializeChild(child, counter, scope, context, analysis, preserveScalarWireLiteral)) appendSerializedChild(result, serialized);
182
+ return runtimeArgumentCount > 1 ? [{
183
+ kind: "array",
184
+ children: result
185
+ }] : result;
186
+ }
187
+ /** Whether the Vue JSX transform emits this authored node as a child value. */
188
+ function isJSXRuntimeChildArgument(child) {
189
+ if (child.type === "JSXText") return normalizeJSXText(child.value) !== "";
190
+ return child.type !== "JSXExpressionContainer" || child.expression.type !== "JSXEmptyExpression";
128
191
  }
129
- function serializeChild(child, counter, scope, context, analysis) {
192
+ function serializeChild(child, counter, scope, context, analysis, preserveScalarWireLiteral) {
130
193
  if (child.type === "JSXText") {
131
194
  const text = normalizeJSXText(child.value);
132
195
  return text ? [text] : [];
133
196
  }
134
197
  if (child.type === "JSXElement") return serializeElement(child, counter, scope, context, analysis);
135
- if (child.type === "JSXFragment") return serializeChildren(child.children, counter, scope, context, analysis);
198
+ if (child.type === "JSXFragment") return [serializeJSXFragment(child, counter, scope, context, analysis)];
136
199
  if (child.type === "JSXSpreadChild") {
137
200
  addVueError(context, babelLocation(child.loc), "Found a spread child inside a gt-vue <T> component in JSX", "Use static JSX children and wrap one runtime value in a gt-vue variable component");
138
201
  return [];
139
202
  }
140
- if (child.expression.type === "JSXEmptyExpression") return [];
141
- return serializeExpression(child.expression, counter, scope, context, analysis);
203
+ if (child.expression.type === "JSXEmptyExpression") return [jsxTextBoundary];
204
+ return serializeExpression(child.expression, counter, scope, context, analysis, preserveScalarWireLiteral);
142
205
  }
143
- function serializeExpression(input, counter, scope, context, analysis) {
206
+ function serializeExpression(input, counter, scope, context, analysis, preserveScalarWireLiteral = true) {
144
207
  const expression = unwrapExpression(input);
145
208
  if (!expression) return [];
146
209
  if (expression.type === "JSXElement") return serializeElement(expression, counter, scope, context, analysis);
147
- if (expression.type === "JSXFragment") return serializeChildren(expression.children, counter, scope, context, analysis);
210
+ if (expression.type === "JSXFragment") return [serializeJSXFragment(expression, counter, scope, context, analysis)];
148
211
  if (expression.type === "ArrayExpression") {
149
212
  const result = [];
150
213
  for (const item of expression.elements) {
@@ -153,9 +216,12 @@ function serializeExpression(input, counter, scope, context, analysis) {
153
216
  addVueError(context, babelLocation(item.loc), "Found a spread array child inside a gt-vue <T> component in JSX", "List each static child explicitly");
154
217
  continue;
155
218
  }
156
- for (const serialized of serializeExpression(item, counter, scope, context, analysis)) appendSerializedChild(result, serialized);
219
+ for (const serialized of serializeExpression(item, counter, scope, context, analysis, false)) appendSerializedChild(result, serialized);
157
220
  }
158
- return result;
221
+ return [{
222
+ kind: "array",
223
+ children: result
224
+ }];
159
225
  }
160
226
  if (expression.type === "ConditionalExpression" || expression.type === "LogicalExpression") {
161
227
  addVueError(context, babelLocation(expression.loc), "Found conditional JSX content inside a gt-vue <T> component", "Move conditional content outside <T>, or use a static Branch or Plural component");
@@ -166,12 +232,13 @@ function serializeExpression(input, counter, scope, context, analysis) {
166
232
  addVueError(context, babelLocation(expression.loc), "Found dynamic JSX content inside a gt-vue <T> component", "Wrap runtime values in <Var>, <Num>, <DateTime>, or <Currency>");
167
233
  return [];
168
234
  }
169
- return value.value == null || typeof value.value === "boolean" ? [] : [String(value.value)];
235
+ if (value.value === null || typeof value.value === "boolean") return preserveScalarWireLiteral ? [value.value] : [];
236
+ return value.value === void 0 ? [] : [String(value.value)];
170
237
  }
171
238
  function serializeElement(element, counter, scope, context, analysis) {
172
239
  const name = element.openingElement.name;
173
240
  const identity = resolveElementIdentity(name, scope, analysis);
174
- if (isTransparentFragment(name, identity)) return serializeChildren(element.children, counter, scope, context, analysis);
241
+ if (isAuthoredFragment(name, identity)) return serializeSemanticFragment(element, counter, scope, context, analysis, usesFragmentComponentSlots(name, identity));
175
242
  counter.value += 1;
176
243
  const id = counter.value;
177
244
  const component = identity?.type === "component" ? identity.name : void 0;
@@ -203,19 +270,11 @@ function serializeElement(element, counter, scope, context, analysis) {
203
270
  }
204
271
  if (component === "Branch" || component === "Plural") return [serializeBranchElement(element, id, component, counter, scope, context, analysis)];
205
272
  if (identity?.type === "vue-builtin" && identity.name === "Suspense") return [serializeSuspenseElement(element, id, counter, scope, context, analysis)];
206
- if (identity?.type === "vue-builtin" && identity.name === "Fragment") {
207
- addVueError(context, babelLocation(element.loc), "Found a renamed Vue Fragment binding inside a gt-vue JSX translation", "Use <>, literal <Fragment>, or a Vue namespace member such as <Vue.Fragment>; the Vue JSX transform compiles renamed Fragment bindings as component slots");
208
- return [];
209
- }
210
- if (identity?.type === "vue-builtin") {
211
- addVueError(context, babelLocation(element.loc), `Found unsupported Vue <${identity.name}> content inside a gt-vue JSX translation`, "Move this Vue builtin outside <T>");
212
- return [];
213
- }
214
273
  if (!identity && isUnboundNonNativeElement(name, scope)) {
215
274
  addVueError(context, babelLocation(element.loc), `Could not determine whether JSX tag <${readElementDisplayName(name) ?? "unknown"}> is a component or custom element`, "Import or locally bind the component, or move the configured custom element outside <T>");
216
275
  return [];
217
276
  }
218
- const ordinaryShape = identity ? void 0 : resolveOrdinaryElementShape(name, scope, analysis, /* @__PURE__ */ new Set());
277
+ const ordinaryShape = identity ? { component: true } : resolveOrdinaryElementShape(name, scope, analysis, /* @__PURE__ */ new Set());
219
278
  if (!identity && !ordinaryShape) {
220
279
  addVueError(context, babelLocation(element.loc), `Could not statically resolve whether JSX tag <${readElementDisplayName(name) ?? "unknown"}> renders an element or component`, "Use a native tag, imported component, defineComponent result, or an immutable tag expression that cannot switch between element and component shapes");
221
280
  return [];
@@ -226,19 +285,115 @@ function serializeElement(element, counter, scope, context, analysis) {
226
285
  addVueError(context, babelLocation(element.loc), "Found unsupported namespaced JSX syntax inside a gt-vue <T> component", "Use a native element or a statically imported component");
227
286
  return [];
228
287
  }
229
- if (ordinaryShape?.opaque) return [{
288
+ const childResult = ordinaryShape?.component ? serializeOrdinaryComponentDefault(element, counter, scope, context, analysis) : { children: serializeChildren(element.children, counter, scope, context, analysis) };
289
+ return [{
230
290
  t: tag,
231
291
  i: id,
232
- ...Object.keys(data).length > 0 && { d: data }
292
+ ...Object.keys(data).length > 0 && { d: data },
293
+ ...shouldIncludeElementChildren(element, childResult.children, scope, analysis, childResult.slotTruthy) && { c: collapseChildren(childResult.children) }
233
294
  }];
234
- const children = serializeChildren(element.children, counter, scope, context, analysis);
295
+ }
296
+ /** Serializes the shorthand `<>...</>` spelling as React.Fragment semantics. */
297
+ function serializeJSXFragment(fragment, counter, scope, context, analysis) {
298
+ counter.value += 1;
299
+ const id = counter.value;
300
+ const children = serializeChildren(fragment.children, counter, scope, context, analysis);
301
+ return {
302
+ t: `C${id}`,
303
+ i: id,
304
+ ...hasTruthyJSXChildren(fragment.children, scope, analysis) && { c: collapseChildren(children) }
305
+ };
306
+ }
307
+ /** Serializes a Fragment alias that Vue JSX represents as a component slot. */
308
+ function serializeSemanticFragment(element, counter, scope, context, analysis, usesComponentSlots) {
309
+ counter.value += 1;
310
+ const id = counter.value;
311
+ validateElementAttributes(element.openingElement, scope, context, analysis);
312
+ const data = readContentProps(element.openingElement, scope, context, analysis);
313
+ const childResult = usesComponentSlots ? serializeOrdinaryComponentDefault(element, counter, scope, context, analysis) : { children: serializeChildren(element.children, counter, scope, context, analysis) };
235
314
  return [{
236
- t: tag,
315
+ t: `C${id}`,
237
316
  i: id,
238
317
  ...Object.keys(data).length > 0 && { d: data },
239
- ...children.length > 0 && { c: collapseChildren(children) }
318
+ ...shouldIncludeElementChildren(element, childResult.children, scope, analysis, childResult.slotTruthy) && { c: collapseChildren(childResult.children) }
240
319
  }];
241
320
  }
321
+ /** Serializes the authored default slot of an ordinary component VNode. */
322
+ function serializeOrdinaryComponentDefault(element, counter, scope, context, analysis) {
323
+ const slotAttributes = element.openingElement.attributes.filter((attribute) => attribute.type === "JSXAttribute" && isSlotsAttribute(attribute));
324
+ if (slotAttributes.length > 1) addVueError(context, babelLocation(slotAttributes[1].loc), "Found more than one v-slots prop on a component inside a gt-vue JSX translation", "Pass one static slots object");
325
+ const meaningfulChildren = element.children.filter(isMeaningfulJSXChild);
326
+ const objectSlotChild = slotAttributes.length === 0 && meaningfulChildren.length === 1 && meaningfulChildren[0]?.type === "JSXExpressionContainer" && meaningfulChildren[0].expression.type !== "JSXEmptyExpression" ? analysis.resolveStaticObject(meaningfulChildren[0].expression, scope) : void 0;
327
+ const slotsObject = slotAttributes[0] ? readSlotsAttributeObject(slotAttributes[0], scope, context, analysis, "a component inside a gt-vue JSX translation") : objectSlotChild;
328
+ const defaultSlot = slotsObject ? readOrdinaryDefaultSlot(slotsObject, context, analysis) : { present: false };
329
+ if (defaultSlot.present) return defaultSlot.slot ? serializeSlotFunction(defaultSlot.slot, counter, context, analysis) : {
330
+ children: [],
331
+ slotTruthy: false
332
+ };
333
+ const directSlot = readDirectOrdinaryDefaultSlot(element, scope, context, analysis);
334
+ if (directSlot.present) return directSlot.slot ? serializeSlotFunction(directSlot.slot, counter, context, analysis) : {
335
+ children: [],
336
+ slotTruthy: false
337
+ };
338
+ if (objectSlotChild) return {
339
+ children: [],
340
+ slotTruthy: false
341
+ };
342
+ return { children: serializeChildren(element.children, counter, scope, context, analysis) };
343
+ }
344
+ /** Reads the one-function child Vue JSX compiles as a direct default slot. */
345
+ function readDirectOrdinaryDefaultSlot(element, scope, context, analysis) {
346
+ const children = element.children.filter(isMeaningfulJSXChild);
347
+ if (children.length !== 1 || children[0]?.type !== "JSXExpressionContainer" || children[0].expression.type === "JSXEmptyExpression") return { present: false };
348
+ const value = unwrapExpression(children[0].expression);
349
+ if (!value || value.type !== "ArrowFunctionExpression" && value.type !== "FunctionExpression") return { present: false };
350
+ if (value.params.length > 0 || value.async || value.generator) {
351
+ addVueError(context, babelLocation(value.loc), "Found a dynamic or scoped direct default slot on a component inside a gt-vue JSX translation", "Use a synchronous zero-argument function with static returned content");
352
+ return { present: true };
353
+ }
354
+ return {
355
+ present: true,
356
+ slot: {
357
+ node: value,
358
+ scope: analysis.scopeForNode(value, scope)
359
+ }
360
+ };
361
+ }
362
+ /** Reads only the default entry; named slots remain runtime-owned and opaque. */
363
+ function readOrdinaryDefaultSlot(object, context, analysis) {
364
+ let present = false;
365
+ let slot;
366
+ for (const property of object.node.properties) {
367
+ if (property.type === "SpreadElement") {
368
+ addVueError(context, babelLocation(property.loc), "Found a spread in a component slots object inside a gt-vue JSX translation", "List the static default slot explicitly so a spread cannot add or replace it");
369
+ continue;
370
+ }
371
+ const name = readStaticObjectKey(property, object.scope, analysis);
372
+ if (name === void 0) {
373
+ addVueError(context, babelLocation(property.loc), "Found a dynamic slot name on a component inside a gt-vue JSX translation", "Use a static named slot or an explicit default key");
374
+ continue;
375
+ }
376
+ if (name !== "default") continue;
377
+ if (present) {
378
+ addVueError(context, babelLocation(property.loc), "Found duplicate default slots on a component inside a gt-vue JSX translation", "Define the default slot once");
379
+ continue;
380
+ }
381
+ present = true;
382
+ const value = property.type === "ObjectMethod" && property.kind === "method" ? property : property.type === "ObjectProperty" ? unwrapExpression(property.value) : void 0;
383
+ if (!value || value.type !== "ArrowFunctionExpression" && value.type !== "FunctionExpression" && value.type !== "ObjectMethod" || value.params.length > 0 || value.async || value.generator) {
384
+ addVueError(context, babelLocation(property.loc), "Found a dynamic or scoped default slot on a component inside a gt-vue JSX translation", "Use a synchronous zero-argument function with static returned content");
385
+ continue;
386
+ }
387
+ slot = {
388
+ node: value,
389
+ scope: analysis.scopeForNode(value, object.scope)
390
+ };
391
+ }
392
+ return {
393
+ present,
394
+ ...slot && { slot }
395
+ };
396
+ }
242
397
  /**
243
398
  * Serializes Vue Suspense's normalized default content without evaluating its
244
399
  * fallback. Vue requires that default slot to resolve to one VNode root; an
@@ -257,13 +412,13 @@ function serializeSuspenseElement(element, id, counter, scope, context, analysis
257
412
  }
258
413
  if (slots.defaultSlot) validateSuspenseSlotRoot(slots.defaultSlot, element, context);
259
414
  else validateSuspenseImplicitRoot(slots.defaultChildren, element, context);
260
- const children = slots.defaultSlot ? serializeSlotFunction(slots.defaultSlot, counter, context, analysis) : serializeChildren(slots.defaultChildren, counter, scope, context, analysis);
415
+ const childResult = slots.defaultSlot ? serializeSlotFunction(slots.defaultSlot, counter, context, analysis) : { children: serializeChildren(slots.defaultChildren, counter, scope, context, analysis) };
261
416
  const data = readContentProps(element.openingElement, scope, context, analysis);
262
417
  return {
263
418
  t: "Suspense",
264
419
  i: id,
265
420
  ...Object.keys(data).length > 0 && { d: data },
266
- ...children.length > 0 && { c: collapseChildren(children) }
421
+ ...shouldIncludeElementChildren(element, childResult.children, scope, analysis, childResult.slotTruthy) && { c: collapseChildren(childResult.children) }
267
422
  };
268
423
  }
269
424
  /** Reads the unambiguous one-function JSX spelling for a default slot. */
@@ -314,12 +469,14 @@ function addSuspenseRootError(element, context) {
314
469
  function serializeBranchElement(element, id, component, counter, scope, context, analysis) {
315
470
  const slots = readJSXSlotLayout(element, scope, context, analysis);
316
471
  if (component === "Plural" && !hasExplicitAttribute(element.openingElement, "n")) addVueError(context, babelLocation(element.openingElement.loc), "Found a gt-vue <Plural> component without an n prop", "Pass the numeric selector through <Plural n={count} />");
317
- const children = slots.defaultSlot ? serializeSlotFunction(slots.defaultSlot, counter, context, analysis) : serializeChildren(slots.defaultChildren, counter, scope, context, analysis);
472
+ const childResult = slots.defaultSlot ? serializeSlotFunction(slots.defaultSlot, counter, context, analysis) : { children: serializeChildren(slots.defaultChildren, counter, scope, context, analysis) };
318
473
  const branches = {};
319
474
  for (const [name, slot] of slots.namedSlots) {
320
475
  if (name.startsWith("_")) continue;
321
476
  if (component === "Plural" && !isAcceptedPluralForm(name)) continue;
322
- branches[name] = collapseChildren(serializeSlotFunction(slot, { value: id }, context, analysis));
477
+ const result = serializeSlotFunction(slot, { value: id }, context, analysis);
478
+ if (result.slotAbsent) continue;
479
+ branches[name] = collapseChildren(result.children);
323
480
  }
324
481
  readBranchAttributeSources(element.openingElement, component, branches, scope, context, analysis);
325
482
  const data = readContentProps(element.openingElement, scope, context, analysis);
@@ -331,7 +488,7 @@ function serializeBranchElement(element, id, component, counter, scope, context,
331
488
  t: component,
332
489
  i: id,
333
490
  ...Object.keys(data).length > 0 && { d: data },
334
- ...children.length > 0 && { c: collapseChildren(children) }
491
+ ...shouldIncludeElementChildren(element, childResult.children, scope, analysis, childResult.slotTruthy) && { c: collapseChildren(childResult.children) }
335
492
  };
336
493
  }
337
494
  /** Reconstructs the slots object emitted by the Vue JSX transform. */
@@ -392,9 +549,53 @@ function serializeSlotFunction(slot, counter, context, analysis) {
392
549
  const returned = readStaticSlotReturn(slot);
393
550
  if (!returned) {
394
551
  addVueError(context, babelLocation(slot.node.loc), "Found a JSX slot with a dynamic function body in a gt-vue translation", "Return static slot content directly from the zero-argument function");
395
- return [];
552
+ return {
553
+ children: [],
554
+ slotTruthy: false
555
+ };
556
+ }
557
+ const value = unwrapExpression(returned);
558
+ if (!value) return {
559
+ children: [],
560
+ slotTruthy: false
561
+ };
562
+ if (value.type !== "ArrayExpression") return {
563
+ children: serializeExpression(value, counter, slot.scope, context, analysis, true),
564
+ ...isUndefinedSlotChild(value, slot.scope, analysis) && { slotAbsent: true },
565
+ slotTruthy: isTruthySlotChild(value, slot.scope, analysis)
566
+ };
567
+ const children = [];
568
+ const entries = value.elements.filter((entry) => entry !== null);
569
+ for (const entry of entries) {
570
+ if (entry.type === "SpreadElement") {
571
+ addVueError(context, babelLocation(entry.loc), "Found a spread in a JSX slot return inside a gt-vue translation", "Return a static list of slot children");
572
+ continue;
573
+ }
574
+ for (const child of serializeExpression(entry, counter, slot.scope, context, analysis, entries.length === 1)) appendSerializedChild(children, child);
396
575
  }
397
- return serializeExpression(returned, counter, slot.scope, context, analysis);
576
+ return {
577
+ children: entries.length > 1 ? [{
578
+ kind: "array",
579
+ children
580
+ }] : children,
581
+ ...entries.length === 1 && entries[0].type !== "SpreadElement" && isUndefinedSlotChild(entries[0], slot.scope, analysis) && { slotAbsent: true },
582
+ slotTruthy: entries.length > 1 || entries.length === 1 && entries[0].type !== "SpreadElement" && isTruthySlotChild(entries[0], slot.scope, analysis)
583
+ };
584
+ }
585
+ /** Matches Vue's absent named-slot result without collapsing an authored array. */
586
+ function isUndefinedSlotChild(input, scope, analysis) {
587
+ const expression = unwrapExpression(input);
588
+ if (!expression || expression.type === "ArrayExpression") return false;
589
+ const primitive = analysis.readStaticPrimitive(expression, scope);
590
+ return primitive.ok && primitive.value === void 0;
591
+ }
592
+ /** Mirrors React's `props.children` truthiness for one normalized slot entry. */
593
+ function isTruthySlotChild(input, scope, analysis) {
594
+ const expression = unwrapExpression(input);
595
+ if (!expression) return false;
596
+ if (expression.type === "ArrayExpression" || expression.type === "JSXElement" || expression.type === "JSXFragment") return true;
597
+ const primitive = analysis.readStaticPrimitive(expression, scope);
598
+ return primitive.ok ? Boolean(primitive.value) : true;
398
599
  }
399
600
  /** Returns the expression from a slot body that contains only one return. */
400
601
  function readStaticSlotReturn(slot) {
@@ -430,9 +631,23 @@ function readBranchAttributeSources(element, component, branches, scope, context
430
631
  addVueError(context, babelLocation(attribute.loc), `Found dynamic branch prop "${name}" on a gt-vue <${component}> component`, "Use a static primitive branch prop or a static named slot");
431
632
  continue;
432
633
  }
433
- branches[name] = value.value == null || typeof value.value === "boolean" ? [] : String(value.value);
634
+ if (value.value === void 0) continue;
635
+ branches[name] = serializeBranchAttributePrimitive(value.value);
434
636
  }
435
637
  }
638
+ /**
639
+ * Serializes one direct branch prop without applying Vue child normalization.
640
+ *
641
+ * Vue renders boolean and null VNode children empty, but React and gt-vue
642
+ * preserve those direct prop values in the persisted branch wire format. The
643
+ * shared `JsxChildren` type predates those values, so the cast stays confined
644
+ * to this exact catalog boundary. Other primitives retain the established
645
+ * string representation.
646
+ */
647
+ function serializeBranchAttributePrimitive(value) {
648
+ if (value === null || typeof value === "boolean") return value;
649
+ return String(value);
650
+ }
436
651
  function isBranchPropName(name) {
437
652
  return !(NON_BRANCH_PROP_NAMES.has(name) || name.startsWith("aria-") || name.startsWith("data-") || /^on[^a-z]/.test(name));
438
653
  }
@@ -465,6 +680,7 @@ function validateVariableElement(element, component, context) {
465
680
  return;
466
681
  }
467
682
  if (!explicitProps.has("value")) addVueError(context, babelLocation(element.openingElement.loc), `Found a gt-vue <${component}> component without a value prop`, `Pass the runtime value through <${component} value={value} />`);
683
+ if (explicitProps.has("name")) addVueError(context, babelLocation(element.openingElement.loc), `Found unsupported name prop on a gt-vue <${component}> component`, "Remove name; named variables are not supported by gt-vue yet");
468
684
  if (hasMeaningfulChildren(element.children)) addVueError(context, babelLocation(element.loc), `Found children on a gt-vue <${component}> component`, `Use only the required value prop: <${component} value={value} />`);
469
685
  }
470
686
  function hasMeaningfulChildren(children) {
@@ -528,6 +744,7 @@ function readJSXAttributePrimitive(attribute, scope, analysis) {
528
744
  return analysis.readStaticPrimitive(attribute.value.expression, scope);
529
745
  }
530
746
  function resolveElementIdentity(name, scope, analysis) {
747
+ if (isNativeElement(name)) return void 0;
531
748
  const expression = jsxNameToExpression(name);
532
749
  return expression ? analysis.resolveKnownValue(expression, scope) : void 0;
533
750
  }
@@ -545,9 +762,13 @@ function jsxMemberObjectToExpression(object) {
545
762
  function isLiteralFragment(name) {
546
763
  return name.type === "JSXIdentifier" && name.name === "Fragment";
547
764
  }
548
- /** Matches only Fragment spellings the Vue JSX transform keeps transparent. */
549
- function isTransparentFragment(name, identity) {
550
- return isLiteralFragment(name) || identity?.type === "vue-builtin" && identity.name === "Fragment" && name.type === "JSXMemberExpression" && name.property.name === "Fragment";
765
+ /** Matches Fragment spellings that create a semantic authored boundary. */
766
+ function isAuthoredFragment(name, identity) {
767
+ return isLiteralFragment(name) || identity?.type === "vue-builtin" && identity.name === "Fragment";
768
+ }
769
+ /** Detects Fragment aliases the JSX transform compiles through component slots. */
770
+ function usesFragmentComponentSlots(name, identity) {
771
+ return identity?.type === "vue-builtin" && identity.name === "Fragment" && !isLiteralFragment(name) && !(name.type === "JSXMemberExpression" && name.property.name === "Fragment");
551
772
  }
552
773
  /** Detects tags whose VNode type can change with plugin `isCustomElement`. */
553
774
  function isUnboundNonNativeElement(name, scope) {
@@ -560,10 +781,10 @@ function isUnboundNonNativeElement(name, scope) {
560
781
  function isNativeElement(name) {
561
782
  return name.type === "JSXIdentifier" && (isHTMLTag(name.name) || isSVGTag(name.name));
562
783
  }
563
- /** Proves whether a non-GT JSX tag is traversable or slot-opaque. */
784
+ /** Proves whether a non-GT JSX tag is an element or component VNode. */
564
785
  function resolveOrdinaryElementShape(name, scope, analysis, seen) {
565
786
  if (isNativeElement(name)) return {
566
- opaque: false,
787
+ component: false,
567
788
  tag: name.name
568
789
  };
569
790
  if (name.type === "JSXNamespacedName") return void 0;
@@ -571,11 +792,11 @@ function resolveOrdinaryElementShape(name, scope, analysis, seen) {
571
792
  let root = name.object;
572
793
  while (root.type === "JSXMemberExpression") root = root.object;
573
794
  const binding = scope.getBinding(root.name);
574
- return binding?.path.isImportSpecifier() || binding?.path.isImportDefaultSpecifier() || binding?.path.isImportNamespaceSpecifier() ? { opaque: true } : void 0;
795
+ return binding?.path.isImportSpecifier() || binding?.path.isImportDefaultSpecifier() || binding?.path.isImportNamespaceSpecifier() ? { component: true } : void 0;
575
796
  }
576
797
  const binding = scope.getBinding(name.name);
577
798
  if (!binding) return void 0;
578
- if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier() || binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) return { opaque: true };
799
+ if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier() || binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) return { component: true };
579
800
  const declaration = binding.path.node;
580
801
  return declaration.type === "VariableDeclarator" && declaration.init ? resolveOrdinaryElementExpression(declaration.init, binding.path.scope, analysis, seen) : void 0;
581
802
  }
@@ -587,21 +808,21 @@ function resolveOrdinaryElementExpression(input, scope, analysis, seen) {
587
808
  nextSeen.add(expression);
588
809
  const primitive = analysis.readStaticPrimitive(expression, scope);
589
810
  if (primitive.ok) return typeof primitive.value === "string" ? {
590
- opaque: false,
811
+ component: false,
591
812
  tag: primitive.value
592
813
  } : void 0;
593
- if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression" || expression.type === "ClassExpression" || expression.type === "ObjectExpression") return { opaque: true };
814
+ if (expression.type === "ArrowFunctionExpression" || expression.type === "FunctionExpression" || expression.type === "ClassExpression" || expression.type === "ObjectExpression") return { component: true };
594
815
  if (expression.type === "Identifier") {
595
816
  const binding = scope.getBinding(expression.name);
596
817
  if (!binding) return void 0;
597
- if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier() || binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) return { opaque: true };
818
+ if (binding.path.isImportSpecifier() || binding.path.isImportDefaultSpecifier() || binding.path.isImportNamespaceSpecifier() || binding.path.isFunctionDeclaration() || binding.path.isClassDeclaration()) return { component: true };
598
819
  const declaration = binding.path.node;
599
820
  return declaration.type === "VariableDeclarator" && declaration.init ? resolveOrdinaryElementExpression(declaration.init, binding.path.scope, analysis, nextSeen) : void 0;
600
821
  }
601
822
  if (expression.type === "CallExpression" || expression.type === "OptionalCallExpression") {
602
823
  if (expression.callee.type === "V8IntrinsicIdentifier") return void 0;
603
824
  const callee = analysis.resolveKnownValue(expression.callee, scope);
604
- if (callee?.type === "defineComponent") return { opaque: true };
825
+ if (callee?.type === "defineComponent") return { component: true };
605
826
  const argument = expression.arguments[0];
606
827
  return callee?.type === "identity" && expression.arguments.length === 1 && argument && argument.type !== "SpreadElement" && argument.type !== "ArgumentPlaceholder" ? resolveOrdinaryElementExpression(argument, scope, analysis, nextSeen) : void 0;
607
828
  }
@@ -614,9 +835,9 @@ function resolveOrdinaryElementExpression(input, scope, analysis, seen) {
614
835
  }
615
836
  /** Combines alternate tags only when they preserve traversal semantics. */
616
837
  function mergeOrdinaryElementShapes(left, right) {
617
- if (!left || !right || left.opaque !== right.opaque) return void 0;
838
+ if (!left || !right || left.component !== right.component) return void 0;
618
839
  return {
619
- opaque: left.opaque,
840
+ component: left.component,
620
841
  ...left.tag === right.tag && left.tag !== void 0 && { tag: left.tag }
621
842
  };
622
843
  }
@@ -654,12 +875,63 @@ function normalizeJSXText(text) {
654
875
  return result;
655
876
  }
656
877
  function appendSerializedChild(result, child) {
657
- const previous = result.at(-1);
658
- if (typeof previous === "string" && typeof child === "string") result[result.length - 1] = previous + child;
659
- else result.push(child);
878
+ if (child === jsxTextBoundary) {
879
+ if (result.at(-1) !== jsxTextBoundary) result.push(child);
880
+ return;
881
+ }
882
+ result.push(child);
883
+ }
884
+ /**
885
+ * Mirrors the truthiness guard in React's `addGTIdentifier()`.
886
+ *
887
+ * JSX emits a scalar for one authored child and an array for multiple children.
888
+ * Arrays are truthy even when React.Children later removes all of their values;
889
+ * a sole primitive instead controls the guard with its original runtime value.
890
+ */
891
+ function hasTruthyJSXChildren(children, scope, analysis) {
892
+ const runtimeChildren = children.filter(isJSXRuntimeChildArgument);
893
+ if (runtimeChildren.length !== 1) return runtimeChildren.length > 1;
894
+ const child = runtimeChildren[0];
895
+ if (child.type === "JSXText") return Boolean(normalizeJSXText(child.value));
896
+ if (child.type === "JSXElement" || child.type === "JSXFragment" || child.type === "JSXSpreadChild") return true;
897
+ if (child.expression.type === "JSXEmptyExpression") return false;
898
+ const expression = unwrapExpression(child.expression);
899
+ if (!expression) return false;
900
+ if (expression.type === "ArrayExpression") return true;
901
+ if (expression.type === "JSXElement" || expression.type === "JSXFragment") return true;
902
+ const primitive = analysis.readStaticPrimitive(expression, scope);
903
+ return primitive.ok ? Boolean(primitive.value) : true;
904
+ }
905
+ /** Includes direct children or a statically selected Vue default slot. */
906
+ function shouldIncludeElementChildren(element, children, scope, analysis, slotTruthy) {
907
+ if (slotTruthy !== void 0) return slotTruthy;
908
+ return element.children.some(isJSXRuntimeChildArgument) ? hasTruthyJSXChildren(element.children, scope, analysis) : children.some(hasSerializedValue);
909
+ }
910
+ function hasSerializedValue(child) {
911
+ return child !== jsxTextBoundary;
912
+ }
913
+ function isSerializedJSXArray(child) {
914
+ return child !== jsxTextBoundary && typeof child === "object" && child !== null && "kind" in child && child.kind === "array";
915
+ }
916
+ /** Flattens explicit nested arrays exactly as React.Children.map does. */
917
+ function flattenSerializedChildren(children) {
918
+ const flattened = [];
919
+ for (const child of children) {
920
+ if (child === jsxTextBoundary) continue;
921
+ if (isSerializedJSXArray(child)) flattened.push(...flattenSerializedChildren(child.children));
922
+ else flattened.push(child);
923
+ }
924
+ return flattened;
660
925
  }
661
926
  function collapseChildren(children) {
662
- return children.length === 1 ? children[0] : children;
927
+ const structuralChildren = children.filter((child) => child !== jsxTextBoundary);
928
+ if (structuralChildren.length === 1 && isSerializedJSXArray(structuralChildren[0])) return flattenSerializedChildren(structuralChildren[0].children);
929
+ const visibleChildren = flattenSerializedChildren(children);
930
+ return visibleChildren.length === 1 ? visibleChildren[0] : visibleChildren;
931
+ }
932
+ /** Distinguishes a missing T slot from an explicitly authored empty array. */
933
+ function collapseRootChildren(children) {
934
+ return children.some(hasSerializedValue) ? collapseChildren(children) : void 0;
663
935
  }
664
936
  //#endregion
665
937
  export { extractVueJSXTranslation, isNestedInVueJSXTranslation, resolveVueJSXElementIdentity, validateVueJSXVariableComponent };