@barefootjs/test 0.26.1 → 0.26.3

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/index.js CHANGED
@@ -187301,6 +187301,7 @@ var UNSUPPORTED_METHODS = new Set([
187301
187301
  "some",
187302
187302
  "forEach",
187303
187303
  "flatMap",
187304
+ "fill",
187304
187305
  "charAt",
187305
187306
  "charCodeAt",
187306
187307
  "codePointAt",
@@ -189191,6 +189192,16 @@ function computeFileScope(entryPath) {
189191
189192
 
189192
189193
  // ../jsx/src/ir-to-client-js/csr-substitute.ts
189193
189194
  var import_typescript4 = __toESM(require_typescript(), 1);
189195
+ function extractFreeIdentifiersFromText(text) {
189196
+ if (!text || text.trim().length === 0)
189197
+ return new Set;
189198
+ const sf = import_typescript4.default.createSourceFile("__free_ids__.ts", `(${text});`, import_typescript4.default.ScriptTarget.Latest, true, import_typescript4.default.ScriptKind.TS);
189199
+ const stmt = sf.statements[0];
189200
+ if (!stmt || !import_typescript4.default.isExpressionStatement(stmt))
189201
+ return new Set;
189202
+ const expr = import_typescript4.default.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
189203
+ return extractFreeIdentifiersFromNode(expr);
189204
+ }
189194
189205
 
189195
189206
  // ../jsx/src/ir-to-client-js/html-template.ts
189196
189207
  function splitTemplateInterpolations(inner) {
@@ -189412,6 +189423,39 @@ function reconstructWithoutTypes(node, sourceFile, ranges) {
189412
189423
  }
189413
189424
  return result;
189414
189425
  }
189426
+ function reconstructAsSegments(node, sourceFile, ranges, markers) {
189427
+ const nodeStart = node.getStart(sourceFile);
189428
+ const nodeEnd = node.getEnd();
189429
+ const fullText = sourceFile.text;
189430
+ const edits = [];
189431
+ for (const r of ranges) {
189432
+ if (r.end <= nodeStart || r.start >= nodeEnd)
189433
+ continue;
189434
+ edits.push({ start: r.start, end: r.end, marker: null });
189435
+ }
189436
+ markers.forEach((m, i2) => edits.push({ start: m.start, end: m.end, marker: i2 }));
189437
+ edits.sort((a, b) => a.start - b.start || b.end - a.end);
189438
+ const segments = [];
189439
+ let jsBuf = "";
189440
+ let pos = nodeStart;
189441
+ for (const edit of edits) {
189442
+ if (edit.start < pos)
189443
+ continue;
189444
+ jsBuf += fullText.slice(pos, edit.start);
189445
+ if (edit.marker !== null) {
189446
+ if (jsBuf)
189447
+ segments.push({ js: jsBuf });
189448
+ jsBuf = "";
189449
+ segments.push({ marker: edit.marker });
189450
+ }
189451
+ pos = edit.end;
189452
+ }
189453
+ if (pos < nodeEnd)
189454
+ jsBuf += fullText.slice(pos, nodeEnd);
189455
+ if (jsBuf)
189456
+ segments.push({ js: jsBuf });
189457
+ return segments;
189458
+ }
189415
189459
  function mergeRanges(ranges) {
189416
189460
  if (ranges.length === 0)
189417
189461
  return [];
@@ -189557,10 +189601,11 @@ function findAngleBracketAfter(lastTypeArg, fullText) {
189557
189601
  }
189558
189602
 
189559
189603
  // ../jsx/src/analyzer-context.ts
189560
- function createAnalyzerContext(sourceFile, filePath) {
189604
+ function createAnalyzerContext(sourceFile, filePath, acceptsCallbackBody) {
189561
189605
  return {
189562
189606
  sourceFile,
189563
189607
  filePath,
189608
+ acceptsCallbackBody,
189564
189609
  componentName: null,
189565
189610
  componentNode: null,
189566
189611
  hasDefaultExport: false,
@@ -189606,6 +189651,9 @@ function createAnalyzerContext(sourceFile, filePath) {
189606
189651
  } catch {
189607
189652
  ownSourceFile = undefined;
189608
189653
  }
189654
+ if (process.env.BF_ASSERT_NO_JSX_IN_GETJS === "1" && this.errors.length === 0 && nodeContainsJsx(node)) {
189655
+ throw new Error("getJS() called on a JSX-bearing node — raw JSX must never be spliced " + "into emitted output. Carry mixed content as structured segments " + "(MapCallbackPreamble / FlatMapCallback) instead.");
189656
+ }
189609
189657
  if (ownSourceFile && ownSourceFile !== sourceFile) {
189610
189658
  return node.getText(ownSourceFile);
189611
189659
  }
@@ -189613,6 +189661,11 @@ function createAnalyzerContext(sourceFile, filePath) {
189613
189661
  }
189614
189662
  };
189615
189663
  }
189664
+ function nodeContainsJsx(node) {
189665
+ if (import_typescript7.default.isJsxElement(node) || import_typescript7.default.isJsxSelfClosingElement(node) || import_typescript7.default.isJsxFragment(node))
189666
+ return true;
189667
+ return import_typescript7.default.forEachChild(node, nodeContainsJsx) ?? false;
189668
+ }
189616
189669
  function getSourceLocation(node, sourceFile, filePath) {
189617
189670
  const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
189618
189671
  const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
@@ -190038,7 +190091,7 @@ function createProgramForFile(source, filePath) {
190038
190091
  return null;
190039
190092
  }
190040
190093
  }
190041
- function analyzeComponent(source, filePath, targetComponentName, program) {
190094
+ function analyzeComponent(source, filePath, targetComponentName, program, acceptsCallbackBody) {
190042
190095
  incrementCounter("filesAnalyzed");
190043
190096
  const hadSharedProgram = program !== undefined;
190044
190097
  const prescan = prescanReactiveFactoriesInSource(source, filePath);
@@ -190071,7 +190124,7 @@ function analyzeComponent(source, filePath, targetComponentName, program) {
190071
190124
  checker = result.checker;
190072
190125
  }
190073
190126
  }
190074
- const ctx = createAnalyzerContext(sourceFile, filePath);
190127
+ const ctx = createAnalyzerContext(sourceFile, filePath, acceptsCallbackBody);
190075
190128
  ctx.checker = checker;
190076
190129
  ctx.reactiveFactories = prescan.factories;
190077
190130
  ctx.declinedReactiveFactories = prescan.declined;
@@ -191123,9 +191176,15 @@ function extractSingleJsxReturn(body) {
191123
191176
  return null;
191124
191177
  return jsxReturn;
191125
191178
  }
191126
- function extractMultiReturnJsxBranches(body) {
191179
+ function preambleUnsafe(preamble, branches, fallback) {
191180
+ if (preamble.length === 0)
191181
+ return false;
191182
+ return fallback === null || branches.some((b) => b.jsxReturn === null);
191183
+ }
191184
+ function extractMultiReturnJsxBranches(body, allowPreamble = false) {
191127
191185
  const branches = [];
191128
191186
  let fallback = null;
191187
+ const preamble = [];
191129
191188
  const stmts = body.statements;
191130
191189
  for (let i2 = 0;i2 < stmts.length; i2++) {
191131
191190
  const stmt = stmts[i2];
@@ -191155,7 +191214,9 @@ function extractMultiReturnJsxBranches(body) {
191155
191214
  }
191156
191215
  if (branches.length === 0)
191157
191216
  return null;
191158
- return { branches, fallback };
191217
+ if (preambleUnsafe(preamble, branches, fallback))
191218
+ return null;
191219
+ return { branches, fallback, preamble };
191159
191220
  }
191160
191221
  break;
191161
191222
  }
@@ -191170,23 +191231,38 @@ function extractMultiReturnJsxBranches(body) {
191170
191231
  const hasDefault = stmt.caseBlock.clauses.some((c) => import_typescript8.default.isDefaultClause(c));
191171
191232
  if (!hasDefault)
191172
191233
  return null;
191234
+ let pendingCases = [];
191173
191235
  for (const clause of stmt.caseBlock.clauses) {
191236
+ if (import_typescript8.default.isCaseClause(clause) && clause.statements.length === 0) {
191237
+ pendingCases.push(clause.expression);
191238
+ continue;
191239
+ }
191174
191240
  const jsxReturn = findJsxReturnInCaseClause(clause);
191175
191241
  const nullReturn = findNullReturnInCaseClause(clause);
191176
191242
  if (!jsxReturn && !nullReturn)
191177
191243
  return null;
191244
+ if (!caseClauseIsDirectReturn(clause))
191245
+ return null;
191178
191246
  if (import_typescript8.default.isCaseClause(clause)) {
191179
191247
  branches.push({
191180
191248
  condition: clause.expression,
191181
- jsxReturn: jsxReturn ?? null
191249
+ jsxReturn: jsxReturn ?? null,
191250
+ extraCaseConditions: pendingCases.length > 0 ? pendingCases : undefined
191182
191251
  });
191252
+ pendingCases = [];
191183
191253
  } else {
191254
+ if (pendingCases.length > 0)
191255
+ return null;
191184
191256
  fallback = jsxReturn ?? null;
191185
191257
  }
191186
191258
  }
191259
+ if (pendingCases.length > 0)
191260
+ return null;
191187
191261
  if (branches.length === 0)
191188
191262
  return null;
191189
- return { branches, fallback, switchDiscriminant: stmt.expression };
191263
+ if (preambleUnsafe(preamble, branches, fallback))
191264
+ return null;
191265
+ return { branches, fallback, switchDiscriminant: stmt.expression, preamble };
191190
191266
  }
191191
191267
  if (import_typescript8.default.isReturnStatement(stmt) && stmt.expression) {
191192
191268
  const expr = unwrapJsxTransparent(stmt.expression);
@@ -191197,13 +191273,22 @@ function extractMultiReturnJsxBranches(body) {
191197
191273
  }
191198
191274
  continue;
191199
191275
  }
191200
- if (import_typescript8.default.isVariableStatement(stmt))
191276
+ if (import_typescript8.default.isVariableStatement(stmt)) {
191277
+ const declFlags = stmt.declarationList.flags;
191278
+ const isConstOrLet = (declFlags & import_typescript8.default.NodeFlags.Const) !== 0 || (declFlags & import_typescript8.default.NodeFlags.Let) !== 0;
191279
+ if (allowPreamble && isConstOrLet && branches.length === 0 && fallback === null) {
191280
+ preamble.push(stmt);
191281
+ continue;
191282
+ }
191201
191283
  return null;
191284
+ }
191202
191285
  return null;
191203
191286
  }
191204
191287
  if (branches.length === 0)
191205
191288
  return null;
191206
- return { branches, fallback };
191289
+ if (preambleUnsafe(preamble, branches, fallback))
191290
+ return null;
191291
+ return { branches, fallback, preamble };
191207
191292
  }
191208
191293
  function isDirectReturnBlock(node) {
191209
191294
  if (import_typescript8.default.isReturnStatement(node))
@@ -191213,9 +191298,9 @@ function isDirectReturnBlock(node) {
191213
191298
  for (const stmt of node.statements) {
191214
191299
  if (import_typescript8.default.isReturnStatement(stmt)) {
191215
191300
  returnCount++;
191216
- } else if (import_typescript8.default.isIfStatement(stmt) || import_typescript8.default.isSwitchStatement(stmt) || import_typescript8.default.isForStatement(stmt) || import_typescript8.default.isForOfStatement(stmt) || import_typescript8.default.isForInStatement(stmt) || import_typescript8.default.isWhileStatement(stmt) || import_typescript8.default.isDoStatement(stmt) || import_typescript8.default.isTryStatement(stmt)) {
191217
- return false;
191301
+ continue;
191218
191302
  }
191303
+ return false;
191219
191304
  }
191220
191305
  return returnCount === 1;
191221
191306
  }
@@ -191246,6 +191331,24 @@ function findJsxReturnInCaseClause(clause) {
191246
191331
  }
191247
191332
  return null;
191248
191333
  }
191334
+ function caseClauseIsDirectReturn(clause) {
191335
+ let returnCount = 0;
191336
+ let seenReturn = false;
191337
+ for (const stmt of clause.statements) {
191338
+ if (import_typescript8.default.isReturnStatement(stmt)) {
191339
+ returnCount++;
191340
+ seenReturn = true;
191341
+ continue;
191342
+ }
191343
+ if (import_typescript8.default.isBreakStatement(stmt)) {
191344
+ if (!seenReturn)
191345
+ return false;
191346
+ continue;
191347
+ }
191348
+ return false;
191349
+ }
191350
+ return returnCount === 1;
191351
+ }
191249
191352
  function findNullReturnInCaseClause(clause) {
191250
191353
  for (const stmt of clause.statements) {
191251
191354
  if (import_typescript8.default.isReturnStatement(stmt) && stmt.expression) {
@@ -193261,6 +193364,9 @@ var REACTIVE_BINDING_KINDS = new Set([
193261
193364
  function isReactiveOrigin(origin) {
193262
193365
  return origin.freeRefs?.some((r) => REACTIVE_BINDING_KINDS.has(r.kind)) ?? false;
193263
193366
  }
193367
+ function tsxSourceText(raw) {
193368
+ return raw;
193369
+ }
193264
193370
  var AttrValueOf = {
193265
193371
  literal(value) {
193266
193372
  return { kind: "literal", value };
@@ -194391,8 +194497,13 @@ function attachParsedExpressions(node, analyzer, bound = EMPTY_BOUND) {
194391
194497
  for (const child of nested.children)
194392
194498
  attachParsedExpressions(child, analyzer, loopBound);
194393
194499
  }
194394
- for (const frag of node.flatMapCallback?.fragments ?? []) {
194395
- attachParsedExpressions(frag.ir, analyzer, loopBound);
194500
+ for (const seg of node.flatMapCallback?.segments ?? []) {
194501
+ if (seg.kind === "jsx")
194502
+ attachParsedExpressions(seg.ir, analyzer, loopBound);
194503
+ }
194504
+ for (const seg of node.preamble?.segments ?? []) {
194505
+ if (seg.kind === "jsx")
194506
+ attachParsedExpressions(seg.ir, analyzer, loopBound);
194396
194507
  }
194397
194508
  break;
194398
194509
  }
@@ -195027,74 +195138,78 @@ function transformMultiReturnJsxFunctionCall(callExpr, info, ctx) {
195027
195138
  ctx.analyzer.getJS = substitutedGetJS;
195028
195139
  try {
195029
195140
  const loc = getSourceLocation(callExpr, ctx.sourceFile, ctx.filePath);
195030
- const nullExpr = {
195031
- type: "expression",
195032
- expr: "null",
195033
- typeInfo: { kind: "primitive", raw: "null", primitive: "null" },
195034
- reactive: false,
195035
- slotId: null,
195036
- loc,
195037
- origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
195038
- };
195039
- let result = info.fallback ? transformNode(info.fallback, ctx) ?? nullExpr : nullExpr;
195040
- for (let i2 = info.branches.length - 1;i2 >= 0; i2--) {
195041
- const branch = info.branches[i2];
195042
- let conditionText;
195043
- if (info.switchDiscriminant) {
195044
- const discText = substitutedGetJS(info.switchDiscriminant);
195045
- const caseText = substitutedGetJS(branch.condition);
195046
- conditionText = `${discText} === ${caseText}`;
195047
- } else {
195048
- conditionText = substitutedGetJS(branch.condition);
195049
- }
195050
- const env = makeBindingEnv(ctx);
195051
- const caseFreeRefs = resolveFreeRefs(branch.condition, env);
195052
- const discFreeRefs = info.switchDiscriminant ? resolveFreeRefs(info.switchDiscriminant, env) : [];
195053
- const conditionOrigin = {
195054
- phase: "tick",
195055
- scope: "template",
195056
- effect: "pure",
195057
- freeRefs: [...discFreeRefs, ...caseFreeRefs]
195058
- };
195059
- const reactive = isReactiveExpression(conditionText, ctx, branch.condition) || isReactiveOrigin(conditionOrigin);
195060
- const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx);
195061
- const callsReactive = exprCallsReactiveGetters(branch.condition, ctx) || (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false);
195062
- const hasCalls = exprHasFunctionCalls(branch.condition) || (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false);
195063
- const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls;
195064
- const slotId = needsSlot ? generateSlotId(ctx) : null;
195065
- const whenTrue = branch.jsxReturn ? transformNode(branch.jsxReturn, ctx) ?? nullExpr : nullExpr;
195066
- let templateCondition;
195067
- if (info.switchDiscriminant) {
195068
- const discRewritten = rewriteBarePropRefs2(substitutedGetJS(info.switchDiscriminant), info.switchDiscriminant, ctx);
195069
- const caseRewritten = rewriteBarePropRefs2(substitutedGetJS(branch.condition), branch.condition, ctx);
195070
- const discPart = discRewritten ?? substitutedGetJS(info.switchDiscriminant);
195071
- const casePart = caseRewritten ?? substitutedGetJS(branch.condition);
195072
- templateCondition = `${discPart} === ${casePart}`;
195073
- } else {
195074
- templateCondition = rewriteBarePropRefs2(conditionText, branch.condition, ctx);
195075
- }
195076
- const conditional = {
195077
- type: "conditional",
195078
- condition: conditionText,
195079
- templateCondition,
195080
- conditionType: null,
195081
- reactive,
195082
- whenTrue,
195083
- whenFalse: result,
195084
- slotId,
195085
- callsReactiveGetters: callsReactive || undefined,
195086
- hasFunctionCalls: hasCalls || undefined,
195087
- loc,
195088
- origin: conditionOrigin
195089
- };
195090
- result = conditional;
195091
- }
195092
- return result;
195141
+ return foldMultiReturnBranches(info, ctx, loc, substitutedGetJS);
195093
195142
  } finally {
195094
195143
  ctx.getJS = originalCtxGetJS;
195095
195144
  ctx.analyzer.getJS = originalAnalyzerGetJS;
195096
195145
  }
195097
195146
  }
195147
+ function foldMultiReturnBranches(info, ctx, loc, getText) {
195148
+ const nullExpr = {
195149
+ type: "expression",
195150
+ expr: "null",
195151
+ typeInfo: { kind: "primitive", raw: "null", primitive: "null" },
195152
+ reactive: false,
195153
+ slotId: null,
195154
+ loc,
195155
+ origin: { phase: "tick", scope: "template", effect: "pure", freeRefs: [] }
195156
+ };
195157
+ let result = info.fallback ? transformNode(info.fallback, ctx) ?? nullExpr : nullExpr;
195158
+ for (let i2 = info.branches.length - 1;i2 >= 0; i2--) {
195159
+ const branch = info.branches[i2];
195160
+ const caseConds = info.switchDiscriminant ? [branch.condition, ...branch.extraCaseConditions ?? []] : [branch.condition];
195161
+ let conditionText;
195162
+ if (info.switchDiscriminant) {
195163
+ const discText = getText(info.switchDiscriminant);
195164
+ conditionText = caseConds.map((c) => `(${discText}) === (${getText(c)})`).join(" || ");
195165
+ } else {
195166
+ conditionText = getText(branch.condition);
195167
+ }
195168
+ const env = makeBindingEnv(ctx);
195169
+ const caseFreeRefs = caseConds.flatMap((c) => resolveFreeRefs(c, env));
195170
+ const discFreeRefs = info.switchDiscriminant ? resolveFreeRefs(info.switchDiscriminant, env) : [];
195171
+ const conditionOrigin = {
195172
+ phase: "tick",
195173
+ scope: "template",
195174
+ effect: "pure",
195175
+ freeRefs: [...discFreeRefs, ...caseFreeRefs]
195176
+ };
195177
+ const reactive = isReactiveExpression(conditionText, ctx, branch.condition) || isReactiveOrigin(conditionOrigin);
195178
+ const loopParamReactive = !reactive && referencesLoopParam(conditionText, ctx);
195179
+ const callsReactive = caseConds.some((c) => exprCallsReactiveGetters(c, ctx)) || (info.switchDiscriminant ? exprCallsReactiveGetters(info.switchDiscriminant, ctx) : false);
195180
+ const hasCalls = caseConds.some((c) => exprHasFunctionCalls(c)) || (info.switchDiscriminant ? exprHasFunctionCalls(info.switchDiscriminant) : false);
195181
+ const needsSlot = reactive || loopParamReactive || callsReactive || hasCalls;
195182
+ const slotId = needsSlot ? generateSlotId(ctx) : null;
195183
+ const whenTrue = branch.jsxReturn ? transformNode(branch.jsxReturn, ctx) ?? nullExpr : nullExpr;
195184
+ let templateCondition;
195185
+ if (info.switchDiscriminant) {
195186
+ const discRewritten = rewriteBarePropRefs2(getText(info.switchDiscriminant), info.switchDiscriminant, ctx);
195187
+ const discPart = discRewritten ?? getText(info.switchDiscriminant);
195188
+ templateCondition = caseConds.map((c) => {
195189
+ const casePart = rewriteBarePropRefs2(getText(c), c, ctx) ?? getText(c);
195190
+ return `(${discPart}) === (${casePart})`;
195191
+ }).join(" || ");
195192
+ } else {
195193
+ templateCondition = rewriteBarePropRefs2(conditionText, branch.condition, ctx);
195194
+ }
195195
+ const conditional = {
195196
+ type: "conditional",
195197
+ condition: conditionText,
195198
+ templateCondition,
195199
+ conditionType: null,
195200
+ reactive,
195201
+ whenTrue,
195202
+ whenFalse: result,
195203
+ slotId,
195204
+ callsReactiveGetters: callsReactive || undefined,
195205
+ hasFunctionCalls: hasCalls || undefined,
195206
+ loc,
195207
+ origin: conditionOrigin
195208
+ };
195209
+ result = conditional;
195210
+ }
195211
+ return result;
195212
+ }
195098
195213
  function transformConditional(node, ctx) {
195099
195214
  const condition = ctx.getJS(node.condition);
195100
195215
  const conditionOrigin = {
@@ -195945,9 +196060,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
195945
196060
  let filterPredicate;
195946
196061
  let sortComparator;
195947
196062
  let chainOrder;
195948
- let mapPreamble;
195949
- let templateMapPreamble;
195950
- let typedMapPreamble;
196063
+ let preamble;
195951
196064
  let iterationShape;
195952
196065
  let objectIteration;
195953
196066
  const setArray = (node2) => {
@@ -195977,7 +196090,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
195977
196090
  const innerFilter = isFilterCall(sortInfo.array);
195978
196091
  const sortExtraction = extractSortComparator(sortInfo.callback, sortInfo.method, ctx);
195979
196092
  if (isClientOnly || !sortExtraction.result) {
195980
- if (!isClientOnly && sortExtraction.unsupportedReason) {
196093
+ if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("sort") ?? false)) {
195981
196094
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(sortInfo.callback, ctx.sourceFile, ctx.filePath), {
195982
196095
  message: `Expression cannot be compiled to marked template: ${sortExtraction.unsupportedReason}`,
195983
196096
  suggestion: {
@@ -195992,7 +196105,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
195992
196105
  chainOrder = "filter-sort";
195993
196106
  const filterExtraction = extractFilterPredicate(innerFilter.callback, ctx);
195994
196107
  if (isClientOnly || !filterExtraction.result) {
195995
- if (!isClientOnly && filterExtraction.unsupportedReason) {
196108
+ if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("filter") ?? false)) {
195996
196109
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(innerFilter.callback, ctx.sourceFile, ctx.filePath), {
195997
196110
  message: `Expression cannot be compiled to marked template: ${filterExtraction.unsupportedReason}`,
195998
196111
  suggestion: {
@@ -196015,7 +196128,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196015
196128
  const innerSort = isSortCall(filterInfo.array);
196016
196129
  const filterExtraction = extractFilterPredicate(filterInfo.callback, ctx);
196017
196130
  if (isClientOnly || !filterExtraction.result) {
196018
- if (!isClientOnly && filterExtraction.unsupportedReason) {
196131
+ if (!isClientOnly && filterExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("filter") ?? false)) {
196019
196132
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(filterInfo.callback, ctx.sourceFile, ctx.filePath), {
196020
196133
  message: `Expression cannot be compiled to marked template: ${filterExtraction.unsupportedReason}`,
196021
196134
  suggestion: {
@@ -196030,7 +196143,7 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196030
196143
  chainOrder = "sort-filter";
196031
196144
  const sortExtraction = extractSortComparator(innerSort.callback, innerSort.method, ctx);
196032
196145
  if (isClientOnly || !sortExtraction.result) {
196033
- if (!isClientOnly && sortExtraction.unsupportedReason) {
196146
+ if (!isClientOnly && sortExtraction.unsupportedReason && !(ctx.analyzer.acceptsCallbackBody?.("sort") ?? false)) {
196034
196147
  ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(innerSort.callback, ctx.sourceFile, ctx.filePath), {
196035
196148
  message: `Expression cannot be compiled to marked template: ${sortExtraction.unsupportedReason}`,
196036
196149
  suggestion: {
@@ -196145,7 +196258,24 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196145
196258
  } else if (method === "flatMap" && import_typescript11.default.isArrayLiteralExpression(body)) {
196146
196259
  children = transformArrayLiteralChildren(body, ctx);
196147
196260
  } else if (import_typescript11.default.isBlock(body)) {
196148
- const returnStmt = body.statements.find((s) => import_typescript11.default.isReturnStatement(s) && s.expression != null);
196261
+ const multiReturn = method !== "flatMap" ? extractMultiReturnJsxBranches(body, true) : null;
196262
+ if (multiReturn && multiReturn.branches.length > 0) {
196263
+ const loc = getSourceLocation(body, ctx.sourceFile, ctx.filePath);
196264
+ children = [foldMultiReturnBranches(multiReturn, ctx, loc, ctx.getJS)];
196265
+ const pre = multiReturn.preamble ?? [];
196266
+ if (pre.length > 0) {
196267
+ preamble = preambleFromValueStatements(pre, ctx);
196268
+ if (!isClientOnly && !(ctx.analyzer.acceptsCallbackBody?.("map") ?? false)) {
196269
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, loc, {
196270
+ message: "A .map() callback body with a `const`/`let` preamble before its " + "branches cannot be lowered to a template: the loop-local binding " + "cannot be carried into a conditional branch on this backend.",
196271
+ suggestion: {
196272
+ message: "Add /* @client */ to evaluate this expression on the client only"
196273
+ }
196274
+ }));
196275
+ }
196276
+ }
196277
+ }
196278
+ const returnStmt = children.length === 0 ? body.statements.find((s) => import_typescript11.default.isReturnStatement(s) && s.expression != null) : undefined;
196149
196279
  if (returnStmt && returnStmt.expression) {
196150
196280
  let returnExpr = returnStmt.expression;
196151
196281
  while (import_typescript11.default.isParenthesizedExpression(returnExpr)) {
@@ -196157,32 +196287,53 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196157
196287
  children = [transformed];
196158
196288
  }
196159
196289
  }
196160
- const preambleStmts = [];
196161
- const templatePreambleStmts = [];
196162
- const typedPreambleStmts = [];
196163
- let hasTypeDiff = false;
196164
- let hasTemplateDiff = false;
196290
+ let jsxPreambleStmt;
196165
196291
  for (const stmt of body.statements) {
196166
196292
  if (stmt === returnStmt)
196167
196293
  break;
196168
- const js = ctx.getJS(stmt);
196169
- const tjs = ctx.getTemplateJS(stmt);
196170
- const ts12 = stmt.getText(ctx.sourceFile);
196171
- preambleStmts.push(js.endsWith(";") ? js : js + ";");
196172
- templatePreambleStmts.push(tjs.endsWith(";") ? tjs : tjs + ";");
196173
- typedPreambleStmts.push(ts12.endsWith(";") ? ts12 : ts12 + ";");
196174
- if (js !== ts12)
196175
- hasTypeDiff = true;
196176
- if (js !== tjs)
196177
- hasTemplateDiff = true;
196294
+ if (containsJsxInExpression(stmt)) {
196295
+ jsxPreambleStmt = stmt;
196296
+ break;
196297
+ }
196178
196298
  }
196179
- if (preambleStmts.length > 0) {
196180
- mapPreamble = preambleStmts.join(" ");
196181
- if (hasTemplateDiff) {
196182
- templateMapPreamble = templatePreambleStmts.join(" ");
196299
+ if (jsxPreambleStmt) {
196300
+ const jsRuntime = isClientOnly || (ctx.analyzer.acceptsCallbackBody?.("map") ?? false);
196301
+ if (children.length === 0) {
196302
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath), {
196303
+ message: "A .map() callback that builds JSX in a statement before its " + "`return` must return a single JSX element that embeds the " + "built array — this return shape has no element root to host it.",
196304
+ suggestion: {
196305
+ message: "Wrap the result in one element root, e.g. `return <tr key={item.id}>{out}</tr>`."
196306
+ }
196307
+ }));
196308
+ } else if (!jsRuntime) {
196309
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(jsxPreambleStmt, ctx.sourceFile, ctx.filePath), {
196310
+ message: "A .map() callback that builds JSX in a statement before its " + "`return` (for example pushing elements into an array in a loop) " + "cannot be lowered to a template on this backend.",
196311
+ suggestion: {
196312
+ message: "Add /* @client */ to render this loop on the client only"
196313
+ }
196314
+ }));
196315
+ } else {
196316
+ const collected = buildPreambleSegments(body.statements, returnStmt, ctx);
196317
+ if (collected.refusalNode) {
196318
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(collected.refusalNode, ctx.sourceFile, ctx.filePath), {
196319
+ message: "A JSX element built in a .map() callback preamble cannot carry " + "event handlers, components, nested loops, or reactive expressions, " + "and cannot sit inside a template literal — the verbatim render " + "path builds it once, with no reactive wiring.",
196320
+ suggestion: {
196321
+ message: "Return the element directly (not through a preamble variable), " + "or add /* @client */ to render the loop on the client only."
196322
+ }
196323
+ }));
196324
+ } else {
196325
+ preamble = collected.preamble;
196326
+ }
196327
+ }
196328
+ } else {
196329
+ const valueStmts = [];
196330
+ for (const stmt of body.statements) {
196331
+ if (stmt === returnStmt)
196332
+ break;
196333
+ valueStmts.push(stmt);
196183
196334
  }
196184
- if (hasTypeDiff) {
196185
- typedMapPreamble = typedPreambleStmts.join(" ");
196335
+ if (valueStmts.length > 0) {
196336
+ preamble = preambleFromValueStatements(valueStmts, ctx);
196186
196337
  }
196187
196338
  }
196188
196339
  }
@@ -196211,6 +196362,31 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196211
196362
  const itemConditional = children.length > 0 ? loopBodyItemConditional(children) : null;
196212
196363
  const bodyIsItemConditional = itemConditional !== null;
196213
196364
  const key = bodyIsItemConditional ? extractItemConditionalKey(itemConditional) : children.length > 0 ? extractLoopKey(children[0]) : null;
196365
+ const declaredNameSet = preamble && preamble.declaredNames.length > 0 ? new Set(preamble.declaredNames) : undefined;
196366
+ if (key && declaredNameSet) {
196367
+ const keyRefs = extractFreeIdentifiersFromText(key);
196368
+ const usesLocal = [...keyRefs].some((r) => declaredNameSet.has(r));
196369
+ if (usesLocal) {
196370
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
196371
+ message: "A .map() loop key must be derivable from the loop item — it is " + "evaluated before the callback body runs, so it cannot reference a " + "value computed in the callback preamble.",
196372
+ suggestion: {
196373
+ message: "Derive the key directly from the loop item (e.g. key={item.id})."
196374
+ }
196375
+ }));
196376
+ }
196377
+ }
196378
+ if (preamble && preamble.builderNames.length > 0) {
196379
+ flagArrayChildExpressions(children, new Set(preamble.builderNames));
196380
+ }
196381
+ if (preamble && preamble.builderNames.length > 0 && children.length === 1 && children[0].type === "component") {
196382
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(node, ctx.sourceFile, ctx.filePath), {
196383
+ message: "A .map() callback that builds JSX in its preamble cannot return a " + "component: the built elements would reach the component as raw HTML " + "strings on the client but as JSX elements at SSR (a silent divergence).",
196384
+ suggestion: {
196385
+ message: "Return a plain element root that embeds the array, or move the " + "building logic inside the component."
196386
+ }
196387
+ }));
196388
+ preamble = undefined;
196389
+ }
196214
196390
  let childComponent;
196215
196391
  if (children.length === 1 && children[0].type === "component") {
196216
196392
  const comp = children[0];
@@ -196259,11 +196435,9 @@ function transformMapCall(node, ctx, isClientOnly = false, method = "map") {
196259
196435
  objectIteration,
196260
196436
  depth,
196261
196437
  clientOnly: isClientOnly || undefined,
196262
- mapPreamble,
196263
- templateMapPreamble,
196438
+ preamble,
196264
196439
  paramType,
196265
196440
  indexType,
196266
- typedMapPreamble,
196267
196441
  paramBindings,
196268
196442
  arrayFreeIdentifiers: extractFreeIdentifiersFromNode(arrayExpr),
196269
196443
  flatMapCallback,
@@ -196299,47 +196473,206 @@ function containsJsx(node) {
196299
196473
  function buildFlatMapCallback(callback, body, ctx) {
196300
196474
  if (!containsJsx(body))
196301
196475
  return;
196302
- const fragments = [];
196303
- const sourceText = ctx.sourceFile.text;
196304
- const bodyStart = body.getStart(ctx.sourceFile);
196305
- const bodyEnd = body.getEnd();
196306
- const bodyText = sourceText.slice(bodyStart, bodyEnd);
196307
- const jsxNodes = [];
196308
- function collectJsx(n) {
196476
+ const leafSpans = [];
196477
+ const leafIrs = [];
196478
+ let refusalNode;
196479
+ const collectJsx = (n, underTemplate) => {
196309
196480
  if (import_typescript11.default.isJsxElement(n) || import_typescript11.default.isJsxSelfClosingElement(n) || import_typescript11.default.isJsxFragment(n)) {
196310
- jsxNodes.push({
196311
- node: n,
196312
- start: n.getStart(ctx.sourceFile) - bodyStart,
196313
- end: n.getEnd() - bodyStart
196314
- });
196481
+ if (underTemplate)
196482
+ refusalNode ??= n;
196483
+ leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() });
196484
+ const ir = transformNode(n, ctx);
196485
+ leafIrs.push(ir ?? { type: "text", value: "", loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) });
196315
196486
  return;
196316
196487
  }
196317
- n.forEachChild(collectJsx);
196318
- }
196319
- collectJsx(body);
196320
- if (jsxNodes.length === 0)
196488
+ const inTemplate = underTemplate || import_typescript11.default.isTemplateExpression(n) || import_typescript11.default.isTaggedTemplateExpression(n);
196489
+ n.forEachChild((c) => collectJsx(c, inTemplate));
196490
+ };
196491
+ collectJsx(body, false);
196492
+ if (leafSpans.length === 0)
196493
+ return;
196494
+ if (refusalNode) {
196495
+ ctx.analyzer.errors.push(createError(ErrorCodes.UNSUPPORTED_JSX_PATTERN, getSourceLocation(refusalNode, ctx.sourceFile, ctx.filePath), {
196496
+ message: "A JSX element inside a template literal in a .flatMap() callback " + "body cannot be compiled.",
196497
+ suggestion: { message: "Build the element outside the template literal." }
196498
+ }));
196321
196499
  return;
196322
- let compiledBody = "";
196323
- let lastEnd = 0;
196324
- for (let i2 = 0;i2 < jsxNodes.length; i2++) {
196325
- const { node, start, end } = jsxNodes[i2];
196326
- const placeholder = `__BF_JSX_${i2}__`;
196327
- compiledBody += bodyText.slice(lastEnd, start) + placeholder;
196328
- lastEnd = end;
196329
- const ir = transformNode(node, ctx);
196330
- fragments.push({
196331
- placeholder,
196332
- ir: ir ?? { type: "text", value: "", loc: getSourceLocation(node, ctx.sourceFile, ctx.filePath) }
196333
- });
196334
196500
  }
196335
- compiledBody += bodyText.slice(lastEnd);
196501
+ const pieces = reconstructAsSegments(body, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans);
196502
+ const segments = pieces.map((piece) => {
196503
+ if ("marker" in piece)
196504
+ return { kind: "jsx", ir: leafIrs[piece.marker] };
196505
+ const tpl = rewriteBarePropRefs2(piece.js, body, ctx);
196506
+ return tpl !== undefined && tpl !== piece.js ? { kind: "js", text: piece.js, templateText: tpl } : { kind: "js", text: piece.js };
196507
+ });
196336
196508
  const paramsText = callback.parameters.map((p) => p.getText(ctx.sourceFile)).join(", ");
196337
196509
  return {
196338
196510
  params: `(${paramsText})`,
196339
- body: compiledBody,
196340
- templateBody: compiledBody,
196341
- rawBody: bodyText,
196342
- fragments
196511
+ segments,
196512
+ rawBody: tsxSourceText(body.getText(ctx.sourceFile))
196513
+ };
196514
+ }
196515
+ function preambleFragmentNeedsWiring(ir) {
196516
+ switch (ir.type) {
196517
+ case "component":
196518
+ case "loop":
196519
+ return true;
196520
+ case "element":
196521
+ if (ir.events.length > 0)
196522
+ return true;
196523
+ if (ir.attrs.some((a) => a.name.startsWith("...")))
196524
+ return true;
196525
+ return ir.children.some(preambleFragmentNeedsWiring);
196526
+ case "expression":
196527
+ return ir.reactive === true;
196528
+ case "conditional":
196529
+ return preambleFragmentNeedsWiring(ir.whenTrue) || (ir.whenFalse ? preambleFragmentNeedsWiring(ir.whenFalse) : false);
196530
+ case "fragment":
196531
+ return ir.children.some(preambleFragmentNeedsWiring);
196532
+ default:
196533
+ return false;
196534
+ }
196535
+ }
196536
+ function flagArrayChildExpressions(nodes, declared) {
196537
+ for (const node of nodes) {
196538
+ switch (node.type) {
196539
+ case "expression": {
196540
+ const name = node.expr.trim();
196541
+ const refs = extractFreeIdentifiersFromText(node.expr);
196542
+ if (refs.size === 1 && refs.has(name) && declared.has(name)) {
196543
+ node.joinArrayChild = true;
196544
+ }
196545
+ break;
196546
+ }
196547
+ case "element":
196548
+ case "fragment":
196549
+ flagArrayChildExpressions(node.children, declared);
196550
+ break;
196551
+ case "conditional":
196552
+ flagArrayChildExpressions([node.whenTrue, ...node.whenFalse ? [node.whenFalse] : []], declared);
196553
+ break;
196554
+ }
196555
+ }
196556
+ }
196557
+ function collectBindingNames(name, out) {
196558
+ if (import_typescript11.default.isIdentifier(name)) {
196559
+ out.add(name.text);
196560
+ return;
196561
+ }
196562
+ for (const el of name.elements) {
196563
+ if (import_typescript11.default.isBindingElement(el))
196564
+ collectBindingNames(el.name, out);
196565
+ }
196566
+ }
196567
+ function collectPreambleDeclaredNames(stmt, out) {
196568
+ if (import_typescript11.default.isVariableStatement(stmt)) {
196569
+ for (const decl of stmt.declarationList.declarations) {
196570
+ collectBindingNames(decl.name, out);
196571
+ }
196572
+ } else if (import_typescript11.default.isFunctionDeclaration(stmt) && stmt.name) {
196573
+ out.add(stmt.name.text);
196574
+ }
196575
+ }
196576
+ function preambleFromValueStatements(statements, ctx) {
196577
+ const segments = [];
196578
+ const typedParts = [];
196579
+ const declared = new Set;
196580
+ for (const stmt of statements) {
196581
+ collectPreambleDeclaredNames(stmt, declared);
196582
+ const js0 = ctx.getJS(stmt);
196583
+ const tjs0 = ctx.getTemplateJS(stmt);
196584
+ const raw0 = stmt.getText(ctx.sourceFile);
196585
+ const js = (js0.endsWith(";") ? js0 : js0 + ";") + " ";
196586
+ const tjs = (tjs0.endsWith(";") ? tjs0 : tjs0 + ";") + " ";
196587
+ typedParts.push(raw0.endsWith(";") ? raw0 : raw0 + ";");
196588
+ segments.push(tjs !== js ? { kind: "js", text: js, templateText: tjs } : { kind: "js", text: js });
196589
+ }
196590
+ return {
196591
+ segments: trimPreambleSegments(segments),
196592
+ ssrText: tsxSourceText(typedParts.join(" ")),
196593
+ declaredNames: [...declared],
196594
+ builderNames: []
196595
+ };
196596
+ }
196597
+ function trimPreambleSegments(segments) {
196598
+ const last = segments[segments.length - 1];
196599
+ if (last?.kind === "js") {
196600
+ const text = last.text.trimEnd();
196601
+ const templateText = last.templateText?.trimEnd();
196602
+ segments[segments.length - 1] = templateText !== undefined ? { kind: "js", text, templateText } : { kind: "js", text };
196603
+ }
196604
+ return segments;
196605
+ }
196606
+ function buildPreambleSegments(statements, returnStmt, ctx) {
196607
+ const segments = [];
196608
+ const typedParts = [];
196609
+ const declared = new Set;
196610
+ const builders = new Set;
196611
+ let refusalNode;
196612
+ const recordBuilderTarget = (leaf, stmt) => {
196613
+ for (let n = leaf.parent;n && n !== stmt.parent; n = n.parent) {
196614
+ if (import_typescript11.default.isCallExpression(n) && import_typescript11.default.isPropertyAccessExpression(n.expression) && (n.expression.name.text === "push" || n.expression.name.text === "unshift") && import_typescript11.default.isIdentifier(n.expression.expression)) {
196615
+ builders.add(n.expression.expression.text);
196616
+ return;
196617
+ }
196618
+ if (import_typescript11.default.isVariableDeclaration(n) && import_typescript11.default.isIdentifier(n.name)) {
196619
+ builders.add(n.name.text);
196620
+ return;
196621
+ }
196622
+ }
196623
+ };
196624
+ for (const stmt of statements) {
196625
+ if (stmt === returnStmt)
196626
+ break;
196627
+ collectPreambleDeclaredNames(stmt, declared);
196628
+ const leafSpans = [];
196629
+ const leafIrs = [];
196630
+ const collect = (n, underTemplate) => {
196631
+ if (import_typescript11.default.isJsxElement(n) || import_typescript11.default.isJsxSelfClosingElement(n) || import_typescript11.default.isJsxFragment(n)) {
196632
+ if (underTemplate)
196633
+ refusalNode ??= n;
196634
+ recordBuilderTarget(n, stmt);
196635
+ leafSpans.push({ start: n.getStart(ctx.sourceFile), end: n.getEnd() });
196636
+ const ir = transformNode(n, ctx);
196637
+ if (ir && preambleFragmentNeedsWiring(ir))
196638
+ refusalNode ??= n;
196639
+ leafIrs.push(ir ?? { type: "text", value: "", loc: getSourceLocation(n, ctx.sourceFile, ctx.filePath) });
196640
+ return;
196641
+ }
196642
+ const inTemplate = underTemplate || import_typescript11.default.isTemplateExpression(n) || import_typescript11.default.isTaggedTemplateExpression(n);
196643
+ n.forEachChild((c) => collect(c, inTemplate));
196644
+ };
196645
+ collect(stmt, false);
196646
+ const raw0 = stmt.getText(ctx.sourceFile);
196647
+ typedParts.push(raw0.endsWith(";") ? raw0 : raw0 + ";");
196648
+ if (leafSpans.length === 0) {
196649
+ const js0 = ctx.getJS(stmt);
196650
+ const tjs0 = ctx.getTemplateJS(stmt);
196651
+ const js = (js0.endsWith(";") ? js0 : js0 + ";") + " ";
196652
+ const tjs = (tjs0.endsWith(";") ? tjs0 : tjs0 + ";") + " ";
196653
+ segments.push(tjs !== js ? { kind: "js", text: js, templateText: tjs } : { kind: "js", text: js });
196654
+ continue;
196655
+ }
196656
+ const pieces = reconstructAsSegments(stmt, ctx.sourceFile, ctx.analyzer.typeExcludeRanges, leafSpans);
196657
+ for (const piece of pieces) {
196658
+ if ("marker" in piece) {
196659
+ segments.push({ kind: "jsx", ir: leafIrs[piece.marker] });
196660
+ } else {
196661
+ const tpl = rewriteBarePropRefs2(piece.js, stmt, ctx);
196662
+ segments.push(tpl !== undefined && tpl !== piece.js ? { kind: "js", text: piece.js, templateText: tpl } : { kind: "js", text: piece.js });
196663
+ }
196664
+ }
196665
+ const sep2 = raw0.endsWith(";") ? " " : "; ";
196666
+ segments.push({ kind: "js", text: sep2 });
196667
+ }
196668
+ return {
196669
+ preamble: {
196670
+ segments: trimPreambleSegments(segments),
196671
+ ssrText: tsxSourceText(typedParts.join(" ")),
196672
+ declaredNames: [...declared],
196673
+ builderNames: [...builders]
196674
+ },
196675
+ refusalNode
196343
196676
  };
196344
196677
  }
196345
196678
  function collectNestedComponents(nodes) {
@@ -197726,6 +198059,7 @@ class BaseAdapter {
197726
198059
  // ../jsx/src/adapters/jsx-adapter.ts
197727
198060
  class JsxAdapter extends BaseAdapter {
197728
198061
  componentName = "";
198062
+ acceptsCallbackBody = () => true;
197729
198063
  formatImportSpecifiers(specifiers) {
197730
198064
  const defaultSpec = specifiers.find((s) => s.isDefault);
197731
198065
  const namespaceSpec = specifiers.find((s) => s.isNamespace);
@@ -198071,6 +198405,10 @@ export default ${this.componentName}` : "";
198071
198405
  const indexParam = loop.index ? `, ${loop.index}` : "";
198072
198406
  const children = this.renderChildren(loop.children);
198073
198407
  const safeChildren = children.startsWith("{") ? `<>${children}</>` : children;
198408
+ const preamble = loop.preamble?.ssrText;
198409
+ if (preamble) {
198410
+ return `{${loop.array}.map((${loop.param}${indexParam}) => { ${preamble} return ${safeChildren} })}`;
198411
+ }
198074
198412
  return `{${loop.array}.map((${loop.param}${indexParam}) => ${safeChildren})}`;
198075
198413
  }
198076
198414
  renderComponent(comp) {
@@ -198880,9 +199218,9 @@ function resolveConstants(constants3) {
198880
199218
  for (const c of constants3) {
198881
199219
  if (c.parsed?.kind === "object-literal") {
198882
199220
  for (const prop of c.parsed.properties) {
198883
- if (prop.value.kind === "literal" && prop.value.literalType === "string") {
198884
- resolved.set(`${c.name}.${prop.key}`, String(prop.value.value));
198885
- }
199221
+ const value2 = resolveObjectPropValue(prop.value, resolved);
199222
+ if (value2 !== null)
199223
+ resolved.set(`${c.name}.${prop.key}`, value2);
198886
199224
  }
198887
199225
  continue;
198888
199226
  }
@@ -198910,6 +199248,31 @@ function resolveConstants(constants3) {
198910
199248
  }
198911
199249
  return resolved;
198912
199250
  }
199251
+ function resolveObjectPropValue(expr, resolved) {
199252
+ if (expr.kind === "literal" && expr.literalType === "string") {
199253
+ return String(expr.value);
199254
+ }
199255
+ if (expr.kind === "identifier") {
199256
+ return resolved.get(expr.name) ?? null;
199257
+ }
199258
+ if (expr.kind === "template-literal") {
199259
+ const strs = [];
199260
+ for (const part of expr.parts) {
199261
+ if (part.type === "string") {
199262
+ strs.push(part.value);
199263
+ continue;
199264
+ }
199265
+ if (part.expr.kind !== "identifier")
199266
+ return null;
199267
+ const value = resolved.get(part.expr.name);
199268
+ if (value === undefined)
199269
+ return null;
199270
+ strs.push(value);
199271
+ }
199272
+ return strs.join("");
199273
+ }
199274
+ return null;
199275
+ }
198913
199276
  function tryResolve(raw, resolved) {
198914
199277
  const value = raw.trim();
198915
199278
  if (value.startsWith("'") && value.endsWith("'") && !value.slice(1, -1).includes("'")) {
@@ -13,12 +13,14 @@ import type { ParsedExpr } from '@barefootjs/jsx';
13
13
  * Resolves string literals, template literals, array.join() patterns,
14
14
  * and plain identifier references. When `valueBranches` is present
15
15
  * (from ternary initializers), each branch is resolved and merged
16
- * with union semantics. An object-literal const with string-valued
17
- * properties (`const rowClass = { active: 'row row-active', plain: 'row' }`)
18
- * additionally seeds member-path keys (`rowClass.active` `'row row-active'`)
19
- * so a member-access className (`className={rowClass.active}`, or a
20
- * ternary arm) resolves through the same lookup (#2354). Function
21
- * expressions and other complex values are skipped.
16
+ * with union semantics. An object-literal const with string-literal,
17
+ * template-literal, or bare-identifier-valued properties (`const rowClass
18
+ * = { active: \`${base} row-active\`, plain: base }`) additionally seeds
19
+ * member-path
20
+ * keys (`rowClass.active` `'row row-active'`) so a member-access
21
+ * className (`className={rowClass.active}`, or a ternary arm) resolves
22
+ * through the same lookup (#2354, template-literal values #2360).
23
+ * Function expressions and other complex property values are skipped.
22
24
  */
23
25
  export declare function resolveConstants(constants: Array<{
24
26
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-constants.d.ts","sourceRoot":"","sources":["../src/resolve-constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAE,CAAC,GAChG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAuCrB"}
1
+ {"version":3,"file":"resolve-constants.d.ts","sourceRoot":"","sources":["../src/resolve-constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAE,CAAC,GAChG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA0CrB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/test",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Test utilities for BarefootJS - IR-based component testing without a browser",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "directory": "packages/test"
40
40
  },
41
41
  "dependencies": {
42
- "@barefootjs/jsx": "0.26.1"
42
+ "@barefootjs/jsx": "0.26.3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.0.0"
@@ -15,12 +15,14 @@ import type { ParsedExpr } from '@barefootjs/jsx'
15
15
  * Resolves string literals, template literals, array.join() patterns,
16
16
  * and plain identifier references. When `valueBranches` is present
17
17
  * (from ternary initializers), each branch is resolved and merged
18
- * with union semantics. An object-literal const with string-valued
19
- * properties (`const rowClass = { active: 'row row-active', plain: 'row' }`)
20
- * additionally seeds member-path keys (`rowClass.active` `'row row-active'`)
21
- * so a member-access className (`className={rowClass.active}`, or a
22
- * ternary arm) resolves through the same lookup (#2354). Function
23
- * expressions and other complex values are skipped.
18
+ * with union semantics. An object-literal const with string-literal,
19
+ * template-literal, or bare-identifier-valued properties (`const rowClass
20
+ * = { active: \`${base} row-active\`, plain: base }`) additionally seeds
21
+ * member-path
22
+ * keys (`rowClass.active` `'row row-active'`) so a member-access
23
+ * className (`className={rowClass.active}`, or a ternary arm) resolves
24
+ * through the same lookup (#2354, template-literal values #2360).
25
+ * Function expressions and other complex property values are skipped.
24
26
  */
25
27
  export function resolveConstants(
26
28
  constants: Array<{ name: string; value?: string; valueBranches?: string[]; parsed?: ParsedExpr }>
@@ -28,14 +30,17 @@ export function resolveConstants(
28
30
  const resolved = new Map<string, string>()
29
31
 
30
32
  for (const c of constants) {
31
- // Object-literal const: expose each string-valued property as a
32
- // `name.key` member-path key. The bare identifier stays unresolved
33
- // (an object is not a class string), matching prior behavior.
33
+ // Object-literal const: expose each resolvable property as a
34
+ // `name.key` member-path key. The object's OWN bare identifier
35
+ // (`c.name` referenced directly, e.g. `className={rowClass}`) stays
36
+ // unresolved — an object is not a class string — matching prior
37
+ // behavior; this is unrelated to a resolvable identifier-valued
38
+ // PROPERTY (`{ plain: base }`), handled by resolveObjectPropValue
39
+ // below (Copilot review, PR #2361).
34
40
  if (c.parsed?.kind === 'object-literal') {
35
41
  for (const prop of c.parsed.properties) {
36
- if (prop.value.kind === 'literal' && prop.value.literalType === 'string') {
37
- resolved.set(`${c.name}.${prop.key}`, String(prop.value.value))
38
- }
42
+ const value = resolveObjectPropValue(prop.value, resolved)
43
+ if (value !== null) resolved.set(`${c.name}.${prop.key}`, value)
39
44
  }
40
45
  continue
41
46
  }
@@ -65,6 +70,56 @@ export function resolveConstants(
65
70
  return resolved
66
71
  }
67
72
 
73
+ /**
74
+ * Resolve one object-literal property's value to a string, for the
75
+ * `name.key` member-path seeding above. Supports the same shapes as
76
+ * `tryResolve` — string literal, template literal, plain identifier
77
+ * reference — but walks the already-structured `ParsedExpr` (available
78
+ * here, unlike `tryResolve`'s raw-string input) instead of re-parsing
79
+ * with a regex, and is stricter about "unresolvable": `tryResolve`'s
80
+ * template-literal branch folds an unresolved `${...}` to `''` (its
81
+ * caller only ever uses that for the whole-constant case, where a
82
+ * partial string is an acceptable degrade), but a property value here
83
+ * returns `null` for the whole property instead — seeding `name.key`
84
+ * with a misleadingly partial class string would be worse than not
85
+ * seeding it at all. A bare identifier property (`{ plain: base }`) and
86
+ * an interpolated identifier inside a template literal both resolve
87
+ * against `resolved` — safe because `resolveConstants` processes
88
+ * `constants` in declaration order, so a module-scope const referenced
89
+ * inside a later object literal's property is already in the map
90
+ * (#2360). Anything else (a nested object/array, a call, a binary
91
+ * expression, ...) is left unresolved, same posture as `tryResolve`
92
+ * skipping "complex values".
93
+ */
94
+ function resolveObjectPropValue(expr: ParsedExpr, resolved: Map<string, string>): string | null {
95
+ if (expr.kind === 'literal' && expr.literalType === 'string') {
96
+ return String(expr.value)
97
+ }
98
+ if (expr.kind === 'identifier') {
99
+ return resolved.get(expr.name) ?? null
100
+ }
101
+ if (expr.kind === 'template-literal') {
102
+ const strs: string[] = []
103
+ for (const part of expr.parts) {
104
+ if (part.type === 'string') {
105
+ strs.push(part.value)
106
+ continue
107
+ }
108
+ // An interpolation that isn't a plain identifier reference (a
109
+ // conditional, a member access, ...), or one that IS an identifier
110
+ // but doesn't resolve, makes the whole template unresolved — don't
111
+ // fold it to '' and seed a partial, misleadingly "resolved" string
112
+ // (Copilot review, PR #2361).
113
+ if (part.expr.kind !== 'identifier') return null
114
+ const value = resolved.get(part.expr.name)
115
+ if (value === undefined) return null
116
+ strs.push(value)
117
+ }
118
+ return strs.join('')
119
+ }
120
+ return null
121
+ }
122
+
68
123
  function tryResolve(raw: string, resolved: Map<string, string>): string | null {
69
124
  const value = raw.trim()
70
125