@barefootjs/jsx 0.24.1 → 0.26.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 (46) hide show
  1. package/dist/adapters/dangerous-inner-html.d.ts +52 -24
  2. package/dist/adapters/dangerous-inner-html.d.ts.map +1 -1
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/errors.d.ts +1 -0
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/index.js +473 -165
  7. package/dist/ir-to-client-js/collect-elements.d.ts.map +1 -1
  8. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts +15 -1
  9. package/dist/ir-to-client-js/control-flow/plan/build-loop-child-arm.d.ts.map +1 -1
  10. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts +9 -6
  11. package/dist/ir-to-client-js/control-flow/plan/build-reactive-effects.d.ts.map +1 -1
  12. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts +11 -5
  13. package/dist/ir-to-client-js/control-flow/plan/loop-child-arm.d.ts.map +1 -1
  14. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts +27 -1
  15. package/dist/ir-to-client-js/control-flow/stringify/loop-child-arm.d.ts.map +1 -1
  16. package/dist/ir-to-client-js/control-flow/stringify/reactive-effects.d.ts.map +1 -1
  17. package/dist/ir-to-client-js/reactivity.d.ts +24 -2
  18. package/dist/ir-to-client-js/reactivity.d.ts.map +1 -1
  19. package/dist/ir-to-client-js/types.d.ts +13 -0
  20. package/dist/ir-to-client-js/types.d.ts.map +1 -1
  21. package/dist/to-locale-date-lowering.d.ts +31 -10
  22. package/dist/to-locale-date-lowering.d.ts.map +1 -1
  23. package/dist/types.d.ts +29 -5
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +2 -2
  26. package/src/__tests__/dangerous-inner-html-resolver.test.ts +27 -7
  27. package/src/__tests__/nested-loop-conditional.test.ts +133 -0
  28. package/src/__tests__/profile-nested-binding-ids.test.ts +5 -2
  29. package/src/__tests__/reactive-factory-cross-file.test.ts +833 -0
  30. package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
  31. package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
  32. package/src/__tests__/to-locale-date-lowering.test.ts +27 -2
  33. package/src/adapters/dangerous-inner-html.ts +101 -48
  34. package/src/analyzer.ts +607 -142
  35. package/src/errors.ts +4 -0
  36. package/src/ir-to-client-js/collect-elements.ts +28 -2
  37. package/src/ir-to-client-js/control-flow/plan/build-loop-child-arm.ts +56 -2
  38. package/src/ir-to-client-js/control-flow/plan/build-reactive-effects.ts +30 -54
  39. package/src/ir-to-client-js/control-flow/plan/loop-child-arm.ts +11 -5
  40. package/src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts +78 -4
  41. package/src/ir-to-client-js/control-flow/stringify/reactive-effects.ts +3 -25
  42. package/src/ir-to-client-js/reactivity.ts +61 -7
  43. package/src/ir-to-client-js/types.ts +13 -0
  44. package/src/rich-type-refusal.ts +3 -2
  45. package/src/to-locale-date-lowering.ts +50 -13
  46. package/src/types.ts +30 -5
package/dist/index.js CHANGED
@@ -5244,7 +5244,8 @@ var ErrorCodes = {
5244
5244
  UNRECOGNIZED_REACTIVE_FACTORY: "BF110",
5245
5245
  REACTIVE_FACTORY_RENAME_UNSUPPORTED: "BF111",
5246
5246
  REACTIVE_FACTORY_MODULE_CAPTURE: "BF112",
5247
- REACTIVE_FACTORY_IMPORT_COLLISION: "BF113"
5247
+ REACTIVE_FACTORY_IMPORT_COLLISION: "BF113",
5248
+ REACTIVE_FACTORY_PARAM_SHADOWED: "BF114"
5248
5249
  };
5249
5250
  var errorMessages = {
5250
5251
  [ErrorCodes.MISSING_USE_CLIENT]: "'use client' directive required for components with createSignal or event handlers",
@@ -5271,7 +5272,8 @@ var errorMessages = {
5271
5272
  [ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY]: "Tuple destructuring of a non-reactive factory call. The compiler only recognizes createSignal / createMemo calls and same-file helpers that wrap them with a single `return [a, b]` exit.",
5272
5273
  [ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED]: "Reactive factory object return/destructure must use shorthand properties only. " + "Property renames (`{ lists: myLists }`), defaults, and rest elements are not " + "supported — destructure with the factory's own property names.",
5273
5274
  [ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE]: "Imported reactive factory references bindings from its own module scope, so its " + "body cannot be inlined into the component file. Move those helpers into the " + "component file, pass them to the factory as parameters, or define the factory " + "in the component file.",
5274
- [ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION]: "Inlining an imported reactive factory requires re-importing one of its helper " + "imports into this file, but that name is already bound here to something else. " + "Rename the conflicting binding in this file, or alias the import in the factory's own file."
5275
+ [ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION]: "Inlining an imported reactive factory requires re-importing one of its helper " + "imports into this file, but that name is already bound here to something else. " + "Rename the conflicting binding in this file, or alias the import in the factory's own file.",
5276
+ [ErrorCodes.REACTIVE_FACTORY_PARAM_SHADOWED]: "Reactive factory parameter is shadowed by a nested declaration inside the factory body, so argument substitution at the inline site would be ambiguous. Rename the inner binding so it does not collide with the parameter."
5275
5277
  };
5276
5278
  function createError(code, loc, options) {
5277
5279
  if (code === undefined || !(code in errorMessages)) {
@@ -6954,6 +6956,37 @@ function extractFreeIdentifiersFromNode(node) {
6954
6956
  visit2(node);
6955
6957
  return ids;
6956
6958
  }
6959
+ function extractFreeTypeIdentifiersFromNode(node) {
6960
+ const ids = new Set;
6961
+ const boundTypeParams = new Set;
6962
+ function rootName(name) {
6963
+ return ts8.isQualifiedName(name) ? rootName(name.left) : name;
6964
+ }
6965
+ function visit2(n) {
6966
+ if (ts8.isTypeReferenceNode(n)) {
6967
+ const name = rootName(n.typeName).text;
6968
+ if (!boundTypeParams.has(name))
6969
+ ids.add(name);
6970
+ }
6971
+ if (ts8.isTypeQueryNode(n)) {
6972
+ const name = rootName(n.exprName).text;
6973
+ if (!boundTypeParams.has(name))
6974
+ ids.add(name);
6975
+ }
6976
+ if (ts8.isFunctionLike(n) && n.typeParameters && n.typeParameters.length > 0) {
6977
+ const names = n.typeParameters.map((p) => p.name.text);
6978
+ for (const p of names)
6979
+ boundTypeParams.add(p);
6980
+ ts8.forEachChild(n, visit2);
6981
+ for (const p of names)
6982
+ boundTypeParams.delete(p);
6983
+ return;
6984
+ }
6985
+ ts8.forEachChild(n, visit2);
6986
+ }
6987
+ visit2(node);
6988
+ return ids;
6989
+ }
6957
6990
  function initializerShapeContainsJsx(node) {
6958
6991
  let found = false;
6959
6992
  function visit2(n) {
@@ -7798,16 +7831,17 @@ function buildEntryImportIndex(sf, filePath) {
7798
7831
  continue;
7799
7832
  if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7800
7833
  continue;
7801
- if (stmt.importClause?.isTypeOnly)
7802
- continue;
7803
7834
  const src = stmt.moduleSpecifier.text;
7804
7835
  const targetKey = src.startsWith("./") || src.startsWith("../") ? resolveRelativeImportToFile(src, filePath) ?? "unresolved:" + src : src;
7836
+ const wholeTypeOnly = stmt.importClause?.isTypeOnly === true;
7805
7837
  const namedBindings = stmt.importClause?.namedBindings;
7806
7838
  if (namedBindings && ts8.isNamedImports(namedBindings)) {
7807
7839
  for (const el of namedBindings.elements) {
7808
- if (el.isTypeOnly)
7809
- continue;
7810
- index.set(el.name.text, { targetKey, exportedName: (el.propertyName ?? el.name).text });
7840
+ index.set(el.name.text, {
7841
+ targetKey,
7842
+ exportedName: (el.propertyName ?? el.name).text,
7843
+ isTypeOnly: wholeTypeOnly || el.isTypeOnly
7844
+ });
7811
7845
  }
7812
7846
  }
7813
7847
  }
@@ -7837,6 +7871,9 @@ function collectEntryBindingNames(sf) {
7837
7871
  if ((ts8.isFunctionDeclaration(node) || ts8.isClassDeclaration(node) || ts8.isEnumDeclaration(node)) && node.name) {
7838
7872
  names.add(node.name.text);
7839
7873
  }
7874
+ if ((ts8.isTypeAliasDeclaration(node) || ts8.isInterfaceDeclaration(node)) && node.name) {
7875
+ names.add(node.name.text);
7876
+ }
7840
7877
  if (ts8.isFunctionLike(node)) {
7841
7878
  for (const p of node.parameters) {
7842
7879
  const out = [];
@@ -7850,6 +7887,7 @@ function collectEntryBindingNames(sf) {
7850
7887
  visit2(sf);
7851
7888
  return names;
7852
7889
  }
7890
+ var MAX_REEXPORT_HOPS = 1;
7853
7891
  function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7854
7892
  const candidateCallees = new Set;
7855
7893
  function collectCandidates(node) {
@@ -7893,29 +7931,28 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7893
7931
  const entryBindingNames = collectEntryBindingNames(entrySourceFile);
7894
7932
  const entryImportIndex = buildEntryImportIndex(entrySourceFile, filePath);
7895
7933
  const plannedInjections = new Map;
7896
- for (const { src, specs } of importsToCheck) {
7897
- const resolved = resolveRelativeImportToFile(src, filePath);
7898
- if (!resolved)
7899
- continue;
7934
+ const helperCache = new Map;
7935
+ function loadHelperFile(abs) {
7936
+ const cached = helperCache.get(abs);
7937
+ if (cached !== undefined)
7938
+ return cached;
7900
7939
  let content;
7901
7940
  try {
7902
- content = fs.readFileSync(resolved, "utf8");
7941
+ content = fs.readFileSync(abs, "utf8");
7903
7942
  } catch {
7904
- continue;
7943
+ helperCache.set(abs, null);
7944
+ return null;
7905
7945
  }
7906
- const alreadyKnown = (name) => result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name);
7907
7946
  const hasAnyPrimitiveText = [...REACTIVE_PRIMITIVES].some((p) => content.includes(p));
7908
- if (!hasAnyPrimitiveText) {
7909
- for (const spec of specs) {
7910
- if (!alreadyKnown(spec.local))
7911
- result.cleanFactoryImports.add(spec.local);
7912
- }
7913
- continue;
7947
+ const hasReexportText = content.includes("export") && content.includes("from");
7948
+ if (!hasAnyPrimitiveText && !hasReexportText) {
7949
+ helperCache.set(abs, "clean");
7950
+ return "clean";
7914
7951
  }
7915
- const helperSf = ts8.createSourceFile(resolved + ".prescan", content, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7952
+ const sf = ts8.createSourceFile(abs + ".prescan", content, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7916
7953
  const localFns = new Map;
7917
7954
  const exportedFns = new Map;
7918
- for (const stmt of helperSf.statements) {
7955
+ for (const stmt of sf.statements) {
7919
7956
  if (ts8.isFunctionDeclaration(stmt) && stmt.name && stmt.body) {
7920
7957
  localFns.set(stmt.name.text, stmt);
7921
7958
  const hasExportModifier = stmt.modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword) ?? false;
@@ -7925,27 +7962,89 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7925
7962
  }
7926
7963
  }
7927
7964
  }
7928
- for (const stmt of helperSf.statements) {
7929
- if (ts8.isExportDeclaration(stmt) && stmt.exportClause && ts8.isNamedExports(stmt.exportClause) && !stmt.moduleSpecifier && !stmt.isTypeOnly) {
7965
+ const reexports = new Map;
7966
+ let hasStarReexport = false;
7967
+ for (const stmt of sf.statements) {
7968
+ if (!ts8.isExportDeclaration(stmt) || stmt.isTypeOnly)
7969
+ continue;
7970
+ if (!stmt.moduleSpecifier) {
7971
+ if (stmt.exportClause && ts8.isNamedExports(stmt.exportClause)) {
7972
+ for (const el of stmt.exportClause.elements) {
7973
+ if (el.isTypeOnly)
7974
+ continue;
7975
+ const fn = localFns.get((el.propertyName ?? el.name).text);
7976
+ if (fn)
7977
+ exportedFns.set(el.name.text, fn);
7978
+ }
7979
+ }
7980
+ continue;
7981
+ }
7982
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7983
+ continue;
7984
+ if (stmt.exportClause && ts8.isNamedExports(stmt.exportClause)) {
7930
7985
  for (const el of stmt.exportClause.elements) {
7931
7986
  if (el.isTypeOnly)
7932
7987
  continue;
7933
- const fn = localFns.get((el.propertyName ?? el.name).text);
7934
- if (fn)
7935
- exportedFns.set(el.name.text, fn);
7988
+ reexports.set(el.name.text, {
7989
+ source: stmt.moduleSpecifier.text,
7990
+ innerName: (el.propertyName ?? el.name).text
7991
+ });
7936
7992
  }
7937
- }
7938
- }
7939
- const moduleBindings = collectHelperModuleValueBindings(helperSf);
7993
+ } else if (!stmt.exportClause) {
7994
+ hasStarReexport = true;
7995
+ }
7996
+ }
7997
+ const moduleBindings = collectHelperModuleValueBindings(sf);
7998
+ const info = { sf, exportedFns, reexports, hasStarReexport, moduleBindings };
7999
+ helperCache.set(abs, info);
8000
+ return info;
8001
+ }
8002
+ function lookupExportedFactory(abs, exportedName, visited, hopsLeft) {
8003
+ if (visited.has(abs))
8004
+ return { kind: "unknown" };
8005
+ visited.add(abs);
8006
+ const file = loadHelperFile(abs);
8007
+ if (file === null)
8008
+ return { kind: "unknown" };
8009
+ if (file === "clean")
8010
+ return { kind: "clean" };
8011
+ const fn = file.exportedFns.get(exportedName);
8012
+ if (fn)
8013
+ return { kind: "fn", fn, file, definingPath: abs };
8014
+ const re = file.reexports.get(exportedName);
8015
+ if (re) {
8016
+ if (hopsLeft <= 0)
8017
+ return { kind: "unknown" };
8018
+ if (!re.source.startsWith("./") && !re.source.startsWith("../"))
8019
+ return { kind: "unknown" };
8020
+ const target = resolveRelativeImportToFile(re.source, abs);
8021
+ if (!target)
8022
+ return { kind: "unknown" };
8023
+ return lookupExportedFactory(target, re.innerName, visited, hopsLeft - 1);
8024
+ }
8025
+ if (file.hasStarReexport)
8026
+ return { kind: "unknown" };
8027
+ return { kind: "clean" };
8028
+ }
8029
+ for (const { src, specs } of importsToCheck) {
8030
+ const resolved = resolveRelativeImportToFile(src, filePath);
8031
+ if (!resolved)
8032
+ continue;
8033
+ const alreadyKnown = (name) => result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name);
7940
8034
  for (const spec of specs) {
7941
8035
  if (alreadyKnown(spec.local))
7942
8036
  continue;
7943
- const fn = exportedFns.get(spec.exported);
7944
- if (!fn) {
8037
+ const found = lookupExportedFactory(resolved, spec.exported, new Set, MAX_REEXPORT_HOPS);
8038
+ if (found.kind === "clean") {
7945
8039
  result.cleanFactoryImports.add(spec.local);
7946
8040
  continue;
7947
8041
  }
7948
- const det = detectReactiveFactory(fn, helperSf, resolved);
8042
+ if (found.kind === "unknown")
8043
+ continue;
8044
+ const { fn, file, definingPath } = found;
8045
+ const helperSf = file.sf;
8046
+ const moduleBindings = file.moduleBindings;
8047
+ const det = detectReactiveFactory(fn, helperSf, definingPath);
7949
8048
  if (!det) {
7950
8049
  result.cleanFactoryImports.add(spec.local);
7951
8050
  continue;
@@ -7970,11 +8069,15 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7970
8069
  const required = [];
7971
8070
  const pending = [];
7972
8071
  let declinedEntry = null;
7973
- for (const ref of capture.importedRefs) {
8072
+ const allRefs = [
8073
+ ...capture.importedRefs.map((r) => ({ ...r, isTypeOnly: false })),
8074
+ ...capture.importedTypeRefs.map((r) => ({ ...r, isTypeOnly: true }))
8075
+ ];
8076
+ for (const ref of allRefs) {
7974
8077
  let specifier;
7975
8078
  let targetKey;
7976
8079
  if (ref.source.startsWith("./") || ref.source.startsWith("../")) {
7977
- const abs = resolveRelativeImportToFile(ref.source, resolved);
8080
+ const abs = resolveRelativeImportToFile(ref.source, definingPath);
7978
8081
  if (!abs) {
7979
8082
  declinedEntry = {
7980
8083
  code: "BF112",
@@ -7990,7 +8093,7 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7990
8093
  targetKey = ref.source;
7991
8094
  }
7992
8095
  const existing = entryImportIndex.get(ref.localName);
7993
- if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName) {
8096
+ if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName && (ref.isTypeOnly || !existing.isTypeOnly)) {
7994
8097
  continue;
7995
8098
  }
7996
8099
  const planned = plannedInjections.get(ref.localName);
@@ -8004,7 +8107,7 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
8004
8107
  break;
8005
8108
  }
8006
8109
  pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }]);
8007
- required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier });
8110
+ required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier, isTypeOnly: ref.isTypeOnly || undefined });
8008
8111
  }
8009
8112
  if (declinedEntry) {
8010
8113
  result.declined.set(spec.local, declinedEntry);
@@ -8012,7 +8115,7 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
8012
8115
  }
8013
8116
  for (const [name, id] of pending)
8014
8117
  plannedInjections.set(name, id);
8015
- det.info.sourceFilePath = resolved;
8118
+ det.info.sourceFilePath = definingPath;
8016
8119
  if (required.length > 0)
8017
8120
  det.info.requiredImports = required;
8018
8121
  result.factories.set(spec.local, det.info);
@@ -8024,6 +8127,8 @@ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
8024
8127
  }
8025
8128
  function collectHelperModuleValueBindings(sf) {
8026
8129
  const local = new Set;
8130
+ const localTypes = new Set;
8131
+ const importedTypes = new Map;
8027
8132
  const imported = new Map;
8028
8133
  for (const stmt of sf.statements) {
8029
8134
  if (ts8.isVariableStatement(stmt)) {
@@ -8039,35 +8144,41 @@ function collectHelperModuleValueBindings(sf) {
8039
8144
  local.add(stmt.name.text);
8040
8145
  continue;
8041
8146
  }
8147
+ if ((ts8.isTypeAliasDeclaration(stmt) || ts8.isInterfaceDeclaration(stmt)) && stmt.name) {
8148
+ localTypes.add(stmt.name.text);
8149
+ continue;
8150
+ }
8042
8151
  if (ts8.isImportDeclaration(stmt)) {
8043
- if (stmt.importClause?.isTypeOnly)
8044
- continue;
8045
8152
  if (!ts8.isStringLiteral(stmt.moduleSpecifier))
8046
8153
  continue;
8047
8154
  const src = stmt.moduleSpecifier.text;
8048
8155
  if (src === "@barefootjs/client" || src === "@barefootjs/client/runtime")
8049
8156
  continue;
8157
+ const wholeTypeOnly = stmt.importClause?.isTypeOnly === true;
8050
8158
  if (stmt.importClause?.name)
8051
- local.add(stmt.importClause.name.text);
8159
+ (wholeTypeOnly ? localTypes : local).add(stmt.importClause.name.text);
8052
8160
  const namedBindings = stmt.importClause?.namedBindings;
8053
8161
  if (namedBindings && ts8.isNamedImports(namedBindings)) {
8054
8162
  for (const el of namedBindings.elements) {
8055
- if (el.isTypeOnly)
8056
- continue;
8057
- imported.set(el.name.text, { source: src, exportedName: (el.propertyName ?? el.name).text });
8163
+ const entry = { source: src, exportedName: (el.propertyName ?? el.name).text };
8164
+ if (wholeTypeOnly || el.isTypeOnly)
8165
+ importedTypes.set(el.name.text, entry);
8166
+ else
8167
+ imported.set(el.name.text, entry);
8058
8168
  }
8059
8169
  }
8060
8170
  if (namedBindings && ts8.isNamespaceImport(namedBindings)) {
8061
- local.add(namedBindings.name.text);
8171
+ (wholeTypeOnly ? localTypes : local).add(namedBindings.name.text);
8062
8172
  }
8063
8173
  }
8064
8174
  }
8065
- return { local, imported };
8175
+ return { local, imported, localTypes, importedTypes };
8066
8176
  }
8067
8177
  function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
8068
8178
  if (!fn.body)
8069
- return { captured: [], importedRefs: [] };
8179
+ return { captured: [], importedRefs: [], importedTypeRefs: [] };
8070
8180
  const free = extractFreeIdentifiersFromNode(fn.body);
8181
+ const freeTypes = extractFreeTypeIdentifiersFromNode(fn.body);
8071
8182
  const exclude = new Set(info.params);
8072
8183
  for (const b of info.localBindings)
8073
8184
  exclude.add(b);
@@ -8076,8 +8187,12 @@ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
8076
8187
  for (const p of REACTIVE_PRIMITIVES)
8077
8188
  exclude.add(p);
8078
8189
  exclude.add(selfName);
8190
+ if (fn.typeParameters)
8191
+ for (const p of fn.typeParameters)
8192
+ exclude.add(p.name.text);
8079
8193
  const captured = [];
8080
8194
  const importedRefs = [];
8195
+ const importedTypeRefs = [];
8081
8196
  for (const id of free) {
8082
8197
  if (exclude.has(id))
8083
8198
  continue;
@@ -8089,9 +8204,28 @@ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
8089
8204
  if (imp)
8090
8205
  importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName });
8091
8206
  }
8207
+ for (const id of freeTypes) {
8208
+ if (exclude.has(id))
8209
+ continue;
8210
+ if (free.has(id))
8211
+ continue;
8212
+ if (moduleBindings.localTypes.has(id) || moduleBindings.local.has(id)) {
8213
+ captured.push(id);
8214
+ continue;
8215
+ }
8216
+ const typeImp = moduleBindings.importedTypes.get(id);
8217
+ if (typeImp) {
8218
+ importedTypeRefs.push({ localName: id, source: typeImp.source, exportedName: typeImp.exportedName });
8219
+ continue;
8220
+ }
8221
+ const valueImp = moduleBindings.imported.get(id);
8222
+ if (valueImp)
8223
+ importedRefs.push({ localName: id, source: valueImp.source, exportedName: valueImp.exportedName });
8224
+ }
8092
8225
  captured.sort();
8093
8226
  importedRefs.sort((a, b) => a.localName < b.localName ? -1 : 1);
8094
- return { captured, importedRefs };
8227
+ importedTypeRefs.sort((a, b) => a.localName < b.localName ? -1 : 1);
8228
+ return { captured, importedRefs, importedTypeRefs };
8095
8229
  }
8096
8230
  function detectReactiveFactory(node, sourceFile, filePath) {
8097
8231
  if (!node.body || !node.name)
@@ -8110,6 +8244,17 @@ function detectReactiveFactory(node, sourceFile, filePath) {
8110
8244
  if (!hasReactiveCall)
8111
8245
  return null;
8112
8246
  const loc = getSourceLocation(node, sourceFile, filePath);
8247
+ let totalReturnCount = 0;
8248
+ function countReturns(n) {
8249
+ if (ts8.isFunctionLike(n))
8250
+ return;
8251
+ if (ts8.isReturnStatement(n)) {
8252
+ totalReturnCount++;
8253
+ return;
8254
+ }
8255
+ ts8.forEachChild(n, countReturns);
8256
+ }
8257
+ ts8.forEachChild(node.body, countReturns);
8113
8258
  let returnExpr = null;
8114
8259
  let returnCount = 0;
8115
8260
  for (const stmt of node.body.statements) {
@@ -8127,7 +8272,7 @@ function detectReactiveFactory(node, sourceFile, filePath) {
8127
8272
  expr = expr.expression;
8128
8273
  returnExpr = expr;
8129
8274
  }
8130
- if (returnCount !== 1 || !returnExpr)
8275
+ if (totalReturnCount !== 1 || returnCount !== 1 || !returnExpr)
8131
8276
  return { kind: "reactive-shaped" };
8132
8277
  const returnTupleIdentifiers = [];
8133
8278
  let returnKind;
@@ -8161,6 +8306,14 @@ function detectReactiveFactory(node, sourceFile, filePath) {
8161
8306
  } else {
8162
8307
  return { kind: "reactive-shaped" };
8163
8308
  }
8309
+ const params = [];
8310
+ for (const p of node.parameters) {
8311
+ if (ts8.isIdentifier(p.name)) {
8312
+ params.push(p.name.text);
8313
+ continue;
8314
+ }
8315
+ return { kind: "reactive-shaped" };
8316
+ }
8164
8317
  const localBindings = [];
8165
8318
  for (const stmt of node.body.statements) {
8166
8319
  if (ts8.isVariableStatement(stmt)) {
@@ -8171,15 +8324,95 @@ function detectReactiveFactory(node, sourceFile, filePath) {
8171
8324
  localBindings.push(stmt.name.text);
8172
8325
  }
8173
8326
  }
8174
- const bodyStatements = node.body.statements.filter((s) => !ts8.isReturnStatement(s)).map((s) => s.getText(sourceFile)).join(`
8327
+ const relevantNames = new Set([...params, ...localBindings, ...returnTupleIdentifiers]);
8328
+ const renameSites = [];
8329
+ let shadowedParam = null;
8330
+ function collectRenameSites(root, toBodyOffset) {
8331
+ function push(id, form) {
8332
+ renameSites.push({
8333
+ name: id.text,
8334
+ start: toBodyOffset(id.getStart(sourceFile)),
8335
+ end: toBodyOffset(id.getEnd()),
8336
+ form
8337
+ });
8338
+ }
8339
+ function classify(id) {
8340
+ if (!relevantNames.has(id.text))
8341
+ return;
8342
+ const p = id.parent;
8343
+ if (ts8.isPropertyAccessExpression(p) && p.name === id)
8344
+ return;
8345
+ if (ts8.isPropertyAssignment(p) && p.name === id)
8346
+ return;
8347
+ if (ts8.isBindingElement(p) && p.propertyName === id)
8348
+ return;
8349
+ if ((ts8.isMethodDeclaration(p) || ts8.isGetAccessorDeclaration(p) || ts8.isSetAccessorDeclaration(p) || ts8.isPropertyDeclaration(p) || ts8.isEnumMember(p)) && p.name === id)
8350
+ return;
8351
+ if (ts8.isJsxAttribute(p) && p.name === id)
8352
+ return;
8353
+ if (ts8.isLabeledStatement(p) && p.label === id || (ts8.isBreakStatement(p) || ts8.isContinueStatement(p)) && p.label === id)
8354
+ return;
8355
+ if ((ts8.isJsxOpeningElement(p) || ts8.isJsxSelfClosingElement(p) || ts8.isJsxClosingElement(p)) && p.tagName === id && /^[a-z]/.test(id.text))
8356
+ return;
8357
+ if (ts8.isShorthandPropertyAssignment(p) && p.name === id) {
8358
+ push(id, "shorthand");
8359
+ return;
8360
+ }
8361
+ if (ts8.isBindingElement(p) && p.name === id && !p.propertyName && ts8.isObjectBindingPattern(p.parent)) {
8362
+ if (params.includes(id.text))
8363
+ shadowedParam = id.text;
8364
+ push(id, "shorthand");
8365
+ return;
8366
+ }
8367
+ const isDecl = (ts8.isVariableDeclaration(p) || ts8.isParameter(p) || ts8.isBindingElement(p) || ts8.isFunctionDeclaration(p) || ts8.isFunctionExpression(p) || ts8.isClassDeclaration(p) || ts8.isClassExpression(p)) && p.name === id;
8368
+ if (isDecl && params.includes(id.text))
8369
+ shadowedParam = id.text;
8370
+ push(id, "plain");
8371
+ }
8372
+ function visit2(n) {
8373
+ if (ts8.isTypeNode(n) || ts8.isTypeParameterDeclaration(n) || ts8.isTypeAliasDeclaration(n) || ts8.isInterfaceDeclaration(n))
8374
+ return;
8375
+ if (ts8.isIdentifier(n)) {
8376
+ classify(n);
8377
+ return;
8378
+ }
8379
+ ts8.forEachChild(n, visit2);
8380
+ }
8381
+ visit2(root);
8382
+ }
8383
+ const keptStatements = node.body.statements.filter((s) => !ts8.isReturnStatement(s));
8384
+ const pieces = [];
8385
+ let base = 0;
8386
+ for (const stmt of keptStatements) {
8387
+ const text = stmt.getText(sourceFile);
8388
+ const stmtStart = stmt.getStart(sourceFile);
8389
+ collectRenameSites(stmt, (pos) => pos - stmtStart + base);
8390
+ pieces.push(text);
8391
+ base += text.length + 1;
8392
+ }
8393
+ const bodyStatements = pieces.join(`
8175
8394
  `);
8176
- const params = [];
8177
- for (const p of node.parameters) {
8178
- if (ts8.isIdentifier(p.name)) {
8179
- params.push(p.name.text);
8180
- continue;
8395
+ if (shadowedParam !== null) {
8396
+ return {
8397
+ kind: "declined",
8398
+ declined: {
8399
+ code: "BF114",
8400
+ detail: `parameter '${shadowedParam}' of '${node.name.text}' is shadowed by a nested declaration inside the factory body`,
8401
+ loc
8402
+ }
8403
+ };
8404
+ }
8405
+ for (const site of renameSites) {
8406
+ if (bodyStatements.slice(site.start, site.end) !== site.name) {
8407
+ return {
8408
+ kind: "declined",
8409
+ declined: {
8410
+ code: "BF111",
8411
+ detail: `internal rename-site offset mismatch for '${site.name}' — this is a compiler bug, ` + `please report it`,
8412
+ loc
8413
+ }
8414
+ };
8181
8415
  }
8182
- return { kind: "reactive-shaped" };
8183
8416
  }
8184
8417
  return {
8185
8418
  kind: "factory",
@@ -8189,7 +8422,8 @@ function detectReactiveFactory(node, sourceFile, filePath) {
8189
8422
  returnTupleIdentifiers,
8190
8423
  returnKind,
8191
8424
  localBindings,
8192
- loc
8425
+ loc,
8426
+ renameSites
8193
8427
  }
8194
8428
  };
8195
8429
  }
@@ -8293,24 +8527,31 @@ function rewriteFactoryCallsInSource(source, prescan) {
8293
8527
  const argTexts = args.map((a) => a.getText(sourceFile));
8294
8528
  const thisCallIndex = callSiteIndex++;
8295
8529
  const suffix = `_bf${thisCallIndex}`;
8296
- let body = factory.bodySource;
8297
- const internalRenames = new Set(factory.localBindings);
8298
- for (const ex of excludeFromSuffixRename)
8299
- internalRenames.delete(ex);
8300
- for (const name of internalRenames) {
8301
- body = body.replace(new RegExp(`\\b${escapeRegex(name)}\\b`, "g"), name + suffix);
8530
+ const renames = new Map;
8531
+ for (const name of factory.localBindings) {
8532
+ if (!excludeFromSuffixRename.has(name))
8533
+ renames.set(name, name + suffix);
8534
+ }
8535
+ if (renameReturnToCallerNames) {
8536
+ for (const [n, caller] of renameReturnToCallerNames) {
8537
+ if (caller !== n)
8538
+ renames.set(n, caller);
8539
+ }
8302
8540
  }
8303
8541
  const atomicArg = /^(?:[\w$.]+|'[^'\\]*'|"[^"\\]*"|-?\d+(?:\.\d+)?)$/;
8304
8542
  for (let i = 0;i < factory.params.length; i++) {
8305
8543
  const p = factory.params[i];
8306
- const a = argTexts[i] ?? "undefined";
8307
- const wrapped = atomicArg.test(a.trim()) ? a.trim() : `(${a})`;
8308
- body = body.replace(new RegExp(`\\b${escapeRegex(p)}\\b`, "g"), wrapped);
8544
+ const a = (argTexts[i] ?? "undefined").trim();
8545
+ renames.set(p, atomicArg.test(a) ? a : `(${a})`);
8309
8546
  }
8310
- if (renameReturnToCallerNames) {
8311
- for (const [n, caller] of renameReturnToCallerNames) {
8312
- body = body.replace(new RegExp(`\\b${escapeRegex(n)}\\b`, "g"), caller);
8313
- }
8547
+ let body = factory.bodySource;
8548
+ for (let i = factory.renameSites.length - 1;i >= 0; i--) {
8549
+ const site = factory.renameSites[i];
8550
+ const repl = renames.get(site.name);
8551
+ if (repl === undefined)
8552
+ continue;
8553
+ const text = site.form === "shorthand" ? `${site.name}: ${repl}` : repl;
8554
+ body = body.slice(0, site.start) + text + body.slice(site.end);
8314
8555
  }
8315
8556
  edits.push({
8316
8557
  start: stmt.getStart(sourceFile),
@@ -8323,20 +8564,44 @@ function rewriteFactoryCallsInSource(source, prescan) {
8323
8564
  if (edits.length === 0)
8324
8565
  return source;
8325
8566
  const importsBySpecifier = new Map;
8567
+ const typeImportsBySpecifier = new Map;
8326
8568
  for (const f of inlinedFactories) {
8327
8569
  for (const r of f.requiredImports ?? []) {
8328
- let names = importsBySpecifier.get(r.specifier);
8570
+ const bySpecifier = r.isTypeOnly ? typeImportsBySpecifier : importsBySpecifier;
8571
+ let names = bySpecifier.get(r.specifier);
8329
8572
  if (!names) {
8330
8573
  names = new Map;
8331
- importsBySpecifier.set(r.specifier, names);
8574
+ bySpecifier.set(r.specifier, names);
8332
8575
  }
8333
8576
  names.set(r.localName, r.exportedName);
8334
8577
  }
8335
8578
  }
8336
- if (importsBySpecifier.size > 0) {
8337
- const lines = [...importsBySpecifier].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([spec, names]) => {
8579
+ for (const [spec, typeNames] of typeImportsBySpecifier) {
8580
+ const valueNames = importsBySpecifier.get(spec);
8581
+ if (!valueNames)
8582
+ continue;
8583
+ for (const local of [...typeNames.keys()]) {
8584
+ if (valueNames.has(local))
8585
+ typeNames.delete(local);
8586
+ }
8587
+ if (typeNames.size === 0)
8588
+ typeImportsBySpecifier.delete(spec);
8589
+ }
8590
+ if (importsBySpecifier.size > 0 || typeImportsBySpecifier.size > 0) {
8591
+ const buildLine = (names, keyword, spec) => {
8338
8592
  const specifiers = [...names].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([local, exported]) => exported === local ? local : `${exported} as ${local}`);
8339
- return `import { ${specifiers.join(", ")} } from '${spec}'`;
8593
+ return `import ${keyword}{ ${specifiers.join(", ")} } from '${spec}'`;
8594
+ };
8595
+ const allSpecifiers = new Set([...importsBySpecifier.keys(), ...typeImportsBySpecifier.keys()]);
8596
+ const lines = [...allSpecifiers].sort().flatMap((spec) => {
8597
+ const out2 = [];
8598
+ const valueNames = importsBySpecifier.get(spec);
8599
+ if (valueNames)
8600
+ out2.push(buildLine(valueNames, "", spec));
8601
+ const typeNames = typeImportsBySpecifier.get(spec);
8602
+ if (typeNames)
8603
+ out2.push(buildLine(typeNames, "type ", spec));
8604
+ return out2;
8340
8605
  });
8341
8606
  const at = factoryImportInsertionOffset(sourceFile);
8342
8607
  edits.push({ start: at, end: at, replacement: at === 0 ? lines.join(`
@@ -8375,9 +8640,6 @@ function isPascalCaseComponentFn(node) {
8375
8640
  }
8376
8641
  return false;
8377
8642
  }
8378
- function escapeRegex(s) {
8379
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8380
- }
8381
8643
  function declinedFactoryMessage(callee, d) {
8382
8644
  if (d.code === "BF112") {
8383
8645
  return `Reactive factory '${callee}' references ${d.detail} from its own module ` + `scope and cannot be inlined. Move the referenced helper(s) into this file, ` + `pass them as factory arguments, or inline the factory here.`;
@@ -8393,6 +8655,8 @@ function declinedFactoryErrorCode(code) {
8393
8655
  return ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE;
8394
8656
  case "BF113":
8395
8657
  return ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION;
8658
+ case "BF114":
8659
+ return ErrorCodes.REACTIVE_FACTORY_PARAM_SHADOWED;
8396
8660
  default:
8397
8661
  return ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED;
8398
8662
  }
@@ -9028,6 +9292,20 @@ function resolveFreeRefs(node, env) {
9028
9292
 
9029
9293
  // src/to-locale-date-lowering.ts
9030
9294
  var TO_LOCALE_TZ_RE = /^(?:UTC|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
9295
+ var tzProbeCache = new Map;
9296
+ function isBuildResolvableTimeZone(value) {
9297
+ const cached = tzProbeCache.get(value);
9298
+ if (cached !== undefined)
9299
+ return cached;
9300
+ let verified;
9301
+ try {
9302
+ verified = new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone === value;
9303
+ } catch {
9304
+ verified = false;
9305
+ }
9306
+ tzProbeCache.set(value, verified);
9307
+ return verified;
9308
+ }
9031
9309
  var PROBE_UTC = new Date(Date.UTC(2001, 1, 3));
9032
9310
  var formatCache = new Map;
9033
9311
  var namesCache = new Map;
@@ -9266,7 +9544,7 @@ function matchToLocaleDateStringCall(callee, args, metadata) {
9266
9544
  return null;
9267
9545
  const value = String(prop.value.value);
9268
9546
  if (prop.key === "timeZone") {
9269
- if (!TO_LOCALE_TZ_RE.test(value))
9547
+ if (!TO_LOCALE_TZ_RE.test(value) && !isBuildResolvableTimeZone(value))
9270
9548
  return null;
9271
9549
  tz = value;
9272
9550
  } else {
@@ -12838,13 +13116,19 @@ function collectEventHandlersFromIR(node) {
12838
13116
  });
12839
13117
  return handlers;
12840
13118
  }
12841
- function traverseElements(node, visitor) {
13119
+ function traverseElements(node, visitor, stopAtReactiveConditionals = false) {
12842
13120
  walkIR(node, 0, {
12843
13121
  element: ({ node: el, scope: domDepth, descend }) => {
12844
13122
  visitor(el, domDepth);
12845
13123
  descend(domDepth + 1);
12846
13124
  },
12847
- loop: () => {}
13125
+ loop: () => {},
13126
+ ...stopAtReactiveConditionals ? {
13127
+ conditional: ({ node: c, scope: domDepth, descend }) => {
13128
+ if (!c.slotId)
13129
+ descend(domDepth);
13130
+ }
13131
+ } : {}
12848
13132
  });
12849
13133
  }
12850
13134
  function collectConditionalBranchEvents(node) {
@@ -12859,7 +13143,7 @@ function collectConditionalBranchEvents(node) {
12859
13143
  });
12860
13144
  }
12861
13145
  }
12862
- });
13146
+ }, true);
12863
13147
  return events;
12864
13148
  }
12865
13149
  function collectConditionalBranchRefs(node) {
@@ -12871,7 +13155,7 @@ function collectConditionalBranchRefs(node) {
12871
13155
  callback: el.ref
12872
13156
  });
12873
13157
  }
12874
- });
13158
+ }, true);
12875
13159
  return refs;
12876
13160
  }
12877
13161
  function collectLoopChildRefs(node) {
@@ -12967,7 +13251,7 @@ function traverseForComponents(node, components, skipConditionals = false) {
12967
13251
  }
12968
13252
  });
12969
13253
  }
12970
- function collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings) {
13254
+ function collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, stopAtReactiveConditionals = false) {
12971
13255
  const texts = [];
12972
13256
  walkIR(node, false, {
12973
13257
  ...stopAt("loop", "async", "ifStatement"),
@@ -12986,13 +13270,14 @@ function collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings)
12986
13270
  ...expanded.freeIds !== undefined && { freeIdentifiers: expanded.freeIds }
12987
13271
  });
12988
13272
  },
12989
- conditional: ({ descend }) => {
12990
- descend(true);
13273
+ conditional: ({ node: c, descend }) => {
13274
+ if (!stopAtReactiveConditionals || !c.slotId)
13275
+ descend(true);
12991
13276
  }
12992
13277
  });
12993
13278
  return texts;
12994
13279
  }
12995
- function collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings) {
13280
+ function collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, stopAtReactiveConditionals = false) {
12996
13281
  const attrs = [];
12997
13282
  traverseElements(node, (el) => {
12998
13283
  if (el.slotId) {
@@ -13019,7 +13304,7 @@ function collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings)
13019
13304
  });
13020
13305
  }
13021
13306
  }
13022
- });
13307
+ }, stopAtReactiveConditionals);
13023
13308
  return attrs;
13024
13309
  }
13025
13310
 
@@ -13843,8 +14128,8 @@ function collectLoopChildBindings(children, ctx, siblingOffsets, loopParam, loop
13843
14128
  const bindings = emptyLoopChildBindings();
13844
14129
  for (const child of children) {
13845
14130
  bindings.events.push(...collectLoopChildEventsWithNesting(child));
13846
- bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings));
13847
- bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings));
14131
+ bindings.reactiveAttrs.push(...collectLoopChildReactiveAttrs(child, ctx, loopParam, loopParamBindings, true));
14132
+ bindings.reactiveTexts.push(...collectLoopChildReactiveTexts(child, ctx, loopParam, loopParamBindings, true));
13848
14133
  bindings.refs.push(...collectLoopChildRefs(child));
13849
14134
  bindings.conditionals.push(...collectLoopChildConditionals(child, ctx, siblingOffsets, loopParam, loopParamBindings));
13850
14135
  }
@@ -13896,7 +14181,9 @@ function summarizeLoopChildBranch(node, ctx, siblingOffsets, loopParam, loopPara
13896
14181
  childComponents: collectConditionalBranchChildComponents(node),
13897
14182
  innerLoops: inner.length > 0 ? inner : undefined,
13898
14183
  conditionals: collectLoopChildConditionals(node, ctx, siblingOffsets, loopParam, loopParamBindings),
13899
- events: collectConditionalBranchEvents(node)
14184
+ events: collectConditionalBranchEvents(node),
14185
+ reactiveAttrs: collectLoopChildReactiveAttrs(node, ctx, loopParam, loopParamBindings, true),
14186
+ reactiveTexts: node.type === "expression" ? [] : collectLoopChildReactiveTexts(node, ctx, loopParam, loopParamBindings, true)
13900
14187
  };
13901
14188
  }
13902
14189
 
@@ -16904,6 +17191,39 @@ function buildLoopChildConditionalsPlan(args) {
16904
17191
  }
16905
17192
  return plans;
16906
17193
  }
17194
+ function buildArmAttrsPlan(attrs, wrap) {
17195
+ if (!attrs || attrs.length === 0)
17196
+ return [];
17197
+ const bySlot = new Map;
17198
+ for (const attr of attrs) {
17199
+ let bucket = bySlot.get(attr.childSlotId);
17200
+ if (!bucket) {
17201
+ bucket = [];
17202
+ bySlot.set(attr.childSlotId, bucket);
17203
+ }
17204
+ bucket.push(attr);
17205
+ }
17206
+ const slots = [];
17207
+ for (const [slotId, slotAttrs] of bySlot) {
17208
+ slots.push({
17209
+ slotId,
17210
+ attrs: slotAttrs.map((attr) => ({
17211
+ attrName: attr.attrName,
17212
+ wrappedExpression: wrap(attr.expression),
17213
+ meta: pickAttrMeta(attr)
17214
+ }))
17215
+ });
17216
+ }
17217
+ return slots;
17218
+ }
17219
+ function buildArmTextsPlan(texts, wrap) {
17220
+ if (!texts || texts.length === 0)
17221
+ return [];
17222
+ return texts.map((text) => ({
17223
+ slotId: text.slotId,
17224
+ wrappedExpression: wrap(text.expression)
17225
+ }));
17226
+ }
16907
17227
  function buildLoopChildArmPlan(args) {
16908
17228
  const { branch, wrap, loopParam, loopParamBindings } = args;
16909
17229
  return {
@@ -16929,7 +17249,8 @@ function buildLoopChildArmPlan(args) {
16929
17249
  loopParam,
16930
17250
  loopParamBindings
16931
17251
  }),
16932
- texts: []
17252
+ attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
17253
+ texts: buildArmTextsPlan(branch.reactiveTexts, wrap)
16933
17254
  };
16934
17255
  }
16935
17256
 
@@ -16957,49 +17278,20 @@ function buildReactiveEffectsPlan(args) {
16957
17278
  }))
16958
17279
  });
16959
17280
  }
16960
- const textSlotsInConditionals = new Set;
16961
- if (conditionals) {
16962
- for (const cond of conditionals) {
16963
- for (const text of texts) {
16964
- if (cond.whenTrueHtml.includes(`bf:${text.slotId}`) || cond.whenFalseHtml.includes(`bf:${text.slotId}`)) {
16965
- textSlotsInConditionals.add(text.slotId);
16966
- }
16967
- }
16968
- }
16969
- }
16970
- const outerTexts = [];
16971
- for (const text of texts) {
16972
- if (textSlotsInConditionals.has(text.slotId))
16973
- continue;
16974
- outerTexts.push({
16975
- slotId: text.slotId,
16976
- wrappedExpression: wrap(text.expression)
16977
- });
16978
- }
17281
+ const outerTexts = texts.map((text) => ({
17282
+ slotId: text.slotId,
17283
+ wrappedExpression: wrap(text.expression)
17284
+ }));
16979
17285
  const conditionalPlans = [];
16980
17286
  if (conditionals) {
16981
17287
  for (const cond of conditionals) {
16982
- const trueTexts = [];
16983
- const falseTexts = [];
16984
- for (const text of texts) {
16985
- if (!textSlotsInConditionals.has(text.slotId))
16986
- continue;
16987
- const wrapped = {
16988
- slotId: text.slotId,
16989
- wrappedExpression: wrap(text.expression)
16990
- };
16991
- if (cond.whenTrueHtml.includes(`bf:${text.slotId}`))
16992
- trueTexts.push(wrapped);
16993
- if (cond.whenFalseHtml.includes(`bf:${text.slotId}`))
16994
- falseTexts.push(wrapped);
16995
- }
16996
17288
  conditionalPlans.push({
16997
17289
  slotId: cond.slotId,
16998
17290
  wrappedCondition: wrap(cond.condition),
16999
17291
  whenTrueTemplateHtml: addCondAttrToTemplate(wrap(cond.whenTrueHtml), cond.slotId),
17000
17292
  whenFalseTemplateHtml: addCondAttrToTemplate(wrap(cond.whenFalseHtml), cond.slotId),
17001
- whenTrueArm: buildOuterArm(cond.whenTrue, trueTexts, wrap, loopParam, loopParamBindings, profileComponentName),
17002
- whenFalseArm: buildOuterArm(cond.whenFalse, falseTexts, wrap, loopParam, loopParamBindings, profileComponentName)
17293
+ whenTrueArm: buildOuterArm(cond.whenTrue, wrap, loopParam, loopParamBindings, profileComponentName),
17294
+ whenFalseArm: buildOuterArm(cond.whenFalse, wrap, loopParam, loopParamBindings, profileComponentName)
17003
17295
  });
17004
17296
  }
17005
17297
  }
@@ -17010,7 +17302,7 @@ function buildReactiveEffectsPlan(args) {
17010
17302
  profileComponentName
17011
17303
  };
17012
17304
  }
17013
- function buildOuterArm(branch, texts, wrap, loopParam, loopParamBindings, profileComponentName) {
17305
+ function buildOuterArm(branch, wrap, loopParam, loopParamBindings, profileComponentName) {
17014
17306
  return {
17015
17307
  events: buildBranchEventBindingsPlan({
17016
17308
  events: branch.events,
@@ -17035,7 +17327,8 @@ function buildOuterArm(branch, texts, wrap, loopParam, loopParamBindings, profil
17035
17327
  loopParam,
17036
17328
  loopParamBindings
17037
17329
  }),
17038
- texts
17330
+ attrs: buildArmAttrsPlan(branch.reactiveAttrs, wrap),
17331
+ texts: buildArmTextsPlan(branch.reactiveTexts, wrap)
17039
17332
  };
17040
17333
  }
17041
17334
  function buildLoopReactiveEffectsPlan(elem, profileComponentName) {
@@ -17814,6 +18107,21 @@ function emitReactiveChildProps(lines, ctx) {
17814
18107
  }
17815
18108
 
17816
18109
  // src/ir-to-client-js/control-flow/stringify/loop-child-arm.ts
18110
+ function stringifyBranchReactiveAttrs(lines, plan, indent, pc) {
18111
+ for (const slot of plan) {
18112
+ const varName = `__ra_${varSlotId(slot.slotId)}`;
18113
+ lines.push(`${indent}{ const ${varName} = qsa(__branchScope, '[bf="${slot.slotId}"]')`);
18114
+ lines.push(`${indent}if (${varName}) {`);
18115
+ for (const attr of slot.attrs) {
18116
+ lines.push(`${indent} __disposers.push(createDisposableEffect(() => {`);
18117
+ for (const stmt of emitAttrUpdate(varName, attr.attrName, attr.wrappedExpression, attr.meta)) {
18118
+ lines.push(`${indent} ${stmt}`);
18119
+ }
18120
+ lines.push(`${indent} }${profileBindingId(pc, slot.slotId)}))`);
18121
+ }
18122
+ lines.push(`${indent}} }`);
18123
+ }
18124
+ }
17817
18125
  function stringifyBranchEventBindings(lines, plan, indent) {
17818
18126
  for (const slot of plan) {
17819
18127
  const v = varSlotId(slot.slotId);
@@ -17891,12 +18199,22 @@ function stringifyLoopChildArm(lines, arm, armIndent, pc) {
17891
18199
  stringifyBranchEventBindings(lines, arm.events, armIndent);
17892
18200
  stringifyBranchChildComponentInits(lines, arm.childComponents, armIndent);
17893
18201
  stringifyBranchInnerLoops(lines, arm.innerLoops, armIndent, pc);
17894
- stringifyLoopChildConditionals(lines, arm.nestedConditionals, armIndent, pc);
18202
+ const hasDisposables = arm.attrs.length > 0 || arm.texts.length > 0 || arm.nestedConditionals.length > 0;
18203
+ if (!hasDisposables)
18204
+ return;
18205
+ lines.push(`${armIndent}const __disposers = []`);
18206
+ stringifyBranchReactiveAttrs(lines, arm.attrs, armIndent, pc);
18207
+ for (const cond of arm.nestedConditionals) {
18208
+ lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => {`);
18209
+ stringifyLoopChildConditional(lines, cond, `${armIndent} `, pc);
18210
+ lines.push(`${armIndent}}))`);
18211
+ }
17895
18212
  for (const text of arm.texts) {
17896
18213
  const varName = `__rt_${varSlotId(text.slotId)}`;
17897
- lines.push(`${armIndent}{ const [${varName}] = $t(__branchScope, '${text.slotId}')`);
17898
- lines.push(`${armIndent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}) }`);
18214
+ lines.push(`${armIndent}let ${varName} = $t(__branchScope, '${text.slotId}')[0]`);
18215
+ lines.push(`${armIndent}__disposers.push(createDisposableEffect(() => { ${varName} = __bfText(${varName}, ${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}))`);
17899
18216
  }
18217
+ lines.push(`${armIndent}return () => __disposers.forEach(d => d())`);
17900
18218
  }
17901
18219
 
17902
18220
  // src/ir-to-client-js/control-flow/stringify/reactive-effects.ts
@@ -17941,29 +18259,15 @@ function emitOuterConditional(lines, indent, elVar, cond, pc) {
17941
18259
  lines.push(`${indent}insert(${elVar}, '${cond.slotId}', () => ${cond.wrappedCondition}, {`);
17942
18260
  lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenTrueTemplateHtml}\`, slots: __slots } },`);
17943
18261
  lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`);
17944
- emitArmBody(lines, cond.whenTrueArm, armIndent, pc);
18262
+ stringifyLoopChildArm(lines, cond.whenTrueArm, armIndent, pc);
17945
18263
  lines.push(`${indent} }`);
17946
18264
  lines.push(`${indent}}, {`);
17947
18265
  lines.push(`${indent} template: () => { const __slots = []; return { html: \`${cond.whenFalseTemplateHtml}\`, slots: __slots } },`);
17948
18266
  lines.push(`${indent} bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`);
17949
- emitArmBody(lines, cond.whenFalseArm, armIndent, pc);
18267
+ stringifyLoopChildArm(lines, cond.whenFalseArm, armIndent, pc);
17950
18268
  lines.push(`${indent} }`);
17951
18269
  lines.push(`${indent}}${profileBindingId(pc, cond.slotId)})`);
17952
18270
  }
17953
- function emitArmBody(lines, arm, armIndent, pc) {
17954
- stringifyBranchEventBindings(lines, arm.events, armIndent);
17955
- stringifyBranchChildComponentInits(lines, arm.childComponents, armIndent);
17956
- stringifyBranchInnerLoops(lines, arm.innerLoops, armIndent, pc);
17957
- stringifyLoopChildConditionals(lines, arm.nestedConditionals, armIndent, pc);
17958
- for (const text of arm.texts) {
17959
- emitArmText(lines, armIndent, text, pc);
17960
- }
17961
- }
17962
- function emitArmText(lines, indent, text, pc) {
17963
- const varName = `__rt_${varSlotId(text.slotId)}`;
17964
- lines.push(`${indent}{ const [${varName}] = $t(__branchScope, '${text.slotId}')`);
17965
- lines.push(`${indent}if (${varName}) createEffect(() => { ${varName}.textContent = String(${text.wrappedExpression}) }${profileBindingId(pc, text.slotId)}) }`);
17966
- }
17967
18271
 
17968
18272
  // src/ir-to-client-js/control-flow/stringify/skeleton-paths.ts
17969
18273
  function pathExpr(base, path) {
@@ -18709,10 +19013,10 @@ function stringifyInsert(lines, plan, opts) {
18709
19013
  function emitArm(lines, arm, mode, armIndent, bodyIndent, profileComponentName) {
18710
19014
  lines.push(`${armIndent}template: () => { const __slots = []; return { html: \`${arm.templateHtml}\`, slots: __slots } },`);
18711
19015
  lines.push(`${armIndent}bindEvents: (__branchScope, { isFirstRun: __bfFirstRun = false } = {}) => {`);
18712
- emitArmBody2(lines, arm.body, mode, bodyIndent, profileComponentName);
19016
+ emitArmBody(lines, arm.body, mode, bodyIndent, profileComponentName);
18713
19017
  lines.push(`${armIndent}}`);
18714
19018
  }
18715
- function emitArmBody2(lines, body, mode, indent, profileComponentName) {
19019
+ function emitArmBody(lines, body, mode, indent, profileComponentName) {
18716
19020
  const bindingBfId = (slotId) => profileComponentName ? `, ${JSON.stringify(`${profileComponentName}#binding:${slotId}`)}` : "";
18717
19021
  const allSlotIds = new Set;
18718
19022
  for (const ev of body.events)
@@ -20894,7 +21198,7 @@ function pushDiagnostic(errors, seen, loc, method, receiverPath, isProp, typeNam
20894
21198
  return;
20895
21199
  seen.add(key);
20896
21200
  const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`;
20897
- const suggestion = method === "toLocaleDateString" && typeName === "Date" ? "Pass a literal locale and an explicit literal timeZone — .toLocaleDateString('ja-JP', { timeZone: 'UTC' }) (or a fixed '±HH:MM' offset) — to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. Alternatively add /* @client */ or pre-compute server-side." : "Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side.";
21201
+ const suggestion = method === "toLocaleDateString" && typeName === "Date" ? "Pass a literal locale and an explicit literal timeZone — .toLocaleDateString('ja-JP', { timeZone: 'UTC' }), a fixed '±HH:MM' offset, or a canonical IANA zone ID like 'Asia/Tokyo' (exact case; #2344) — to compile it to the format_date helper; for a runtime locale, resolve the pattern in your i18n layer and use formatDate(date, pattern, tz) from @barefootjs/client. Alternatively add /* @client */ or pre-compute server-side." : "Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side.";
20898
21202
  errors.push({
20899
21203
  code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
20900
21204
  severity: "error",
@@ -22531,27 +22835,31 @@ function resolveDangerousInnerHtml(element) {
22531
22835
  if (attr.clientOnly)
22532
22836
  return null;
22533
22837
  if (attr.value.kind !== "expression") {
22534
- return { kind: "dynamic", expr: "", loc: attr.loc };
22838
+ return { kind: "unlowerable", expr: "", loc: attr.loc };
22535
22839
  }
22536
22840
  const parsed = attr.value.parsed ?? parseExpression(attr.value.expr.trim());
22537
- const html = staticHtmlLiteral(parsed);
22538
- if (html !== null)
22539
- return { kind: "static", html };
22540
- return { kind: "dynamic", expr: attr.value.expr, loc: attr.loc };
22841
+ const value = htmlPropValue(parsed);
22842
+ if (value === null)
22843
+ return { kind: "unlowerable", expr: attr.value.expr, loc: attr.loc };
22844
+ if (value.kind === "literal" && value.literalType === "string") {
22845
+ return { kind: "static", html: value.value };
22846
+ }
22847
+ return {
22848
+ kind: "dynamic",
22849
+ valueExpr: stringifyParsedExpr(value),
22850
+ valueParsed: value,
22851
+ loc: attr.loc
22852
+ };
22541
22853
  }
22542
- function staticHtmlLiteral(parsed) {
22854
+ function htmlPropValue(parsed) {
22543
22855
  if (parsed.kind !== "object-literal")
22544
22856
  return null;
22545
22857
  if (parsed.properties.length !== 1)
22546
22858
  return null;
22547
22859
  const [prop] = parsed.properties;
22548
- if (prop.shorthand)
22549
- return null;
22550
22860
  if (prop.key !== "__html")
22551
22861
  return null;
22552
- if (prop.value.kind !== "literal" || prop.value.literalType !== "string")
22553
- return null;
22554
- return prop.value.value;
22862
+ return prop.value;
22555
22863
  }
22556
22864
  var TEMPLATE_METACHAR_PATTERNS = {
22557
22865
  blade: /\{\{|\{!!|<\?|@\w|<\/?\s*x[-:]/,
@@ -22573,14 +22881,14 @@ function dangerousInnerHtmlMetacharViolation(html, adapterId) {
22573
22881
  return `the literal HTML contains a sequence ${adapterId}'s own template compiler would interpret (not inert text once spliced into the template)`;
22574
22882
  }
22575
22883
  function dangerousInnerHtmlDiagnostic(expr, loc, reason) {
22576
- const detail = reason ? ` — ${reason}.` : "";
22884
+ const base = reason ? `dangerouslySetInnerHTML value cannot be lowered on this adapter — ${reason}` : "dangerouslySetInnerHTML requires an object literal with a single `__html` property (e.g. { __html: value }) — spreads, extra keys, and computed keys are not supported";
22577
22885
  return {
22578
22886
  code: "BF101",
22579
22887
  severity: "error",
22580
- message: `dangerouslySetInnerHTML requires a compile-time string literal __html value on template adapters (e.g. { __html: '...' })${expr ? `: ${expr.trim()}` : ""}${detail}`,
22888
+ message: `${base}${expr ? `: ${expr.trim()}` : ""}`,
22581
22889
  loc,
22582
22890
  suggestion: {
22583
- message: "Dynamic or signal-derived HTML for dangerouslySetInnerHTML is only supported on Hono/CSR today (tracked separately: https://github.com/piconic-ai/barefootjs/issues/2215). Use an inline string literal, or defer it to the client with /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it instead of SSR."
22891
+ message: "Pass an object literal { __html: value } with exactly one `__html` property (a string literal is spliced as trusted template text; a prop/signal value is lowered through the adapter's raw-output sink). To force it onto the client instead of SSR, use /* @client */ (e.g. dangerouslySetInnerHTML={/* @client */ { __html: expr }}) so hydration sets it."
22584
22892
  }
22585
22893
  };
22586
22894
  }