@barefootjs/jsx 0.21.4 → 0.24.1

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/env-signal.d.ts +8 -0
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/analyzer-context.d.ts +16 -5
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts +10 -4
  6. package/dist/analyzer.d.ts.map +1 -1
  7. package/dist/builtin-lowering-plugins.d.ts.map +1 -1
  8. package/dist/date-lowering.d.ts +16 -0
  9. package/dist/date-lowering.d.ts.map +1 -1
  10. package/dist/errors.d.ts +3 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/format-date-lowering.d.ts +30 -0
  13. package/dist/format-date-lowering.d.ts.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1124 -74
  17. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/html-template.d.ts +1 -0
  19. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/imports.d.ts +2 -2
  21. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  22. package/dist/jsx-to-ir.d.ts.map +1 -1
  23. package/dist/to-locale-date-lowering.d.ts +111 -0
  24. package/dist/to-locale-date-lowering.d.ts.map +1 -0
  25. package/dist/types.d.ts +47 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +2 -2
  28. package/src/__tests__/format-date-lowering.test.ts +125 -0
  29. package/src/__tests__/reactive-factory-cross-file.test.ts +502 -0
  30. package/src/__tests__/reactive-factory-inlining.test.ts +293 -4
  31. package/src/__tests__/to-locale-date-lowering.test.ts +382 -0
  32. package/src/adapters/env-signal.ts +26 -3
  33. package/src/analyzer-context.ts +19 -4
  34. package/src/analyzer.ts +1012 -93
  35. package/src/builtin-lowering-plugins.ts +8 -1
  36. package/src/date-lowering.ts +1 -1
  37. package/src/errors.ts +19 -0
  38. package/src/format-date-lowering.ts +55 -0
  39. package/src/index.ts +1 -1
  40. package/src/ir-to-client-js/emit-reactive.ts +90 -1
  41. package/src/ir-to-client-js/html-template.ts +36 -2
  42. package/src/ir-to-client-js/imports.ts +4 -0
  43. package/src/jsx-to-ir.ts +90 -1
  44. package/src/rich-type-refusal.ts +9 -1
  45. package/src/to-locale-date-lowering.ts +563 -0
  46. package/src/types.ts +49 -1
package/dist/index.js CHANGED
@@ -3141,18 +3141,39 @@ function createTemplateAwareStringProtector() {
3141
3141
  stash.push(s);
3142
3142
  return `__STRLIT_${i}__`;
3143
3143
  };
3144
+ const STRING_LIT_RE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g;
3144
3145
  const protect = (s) => {
3145
3146
  s = s.replace(/`([^`]*)`/g, (_full, inner) => {
3146
3147
  const parts = splitTemplateInterpolations(inner);
3147
3148
  return "`" + parts.map((p) => p.startsWith("${") ? p : save(p)).join("") + "`";
3148
3149
  });
3149
- s = s.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (m) => save(m));
3150
+ s = s.replace(STRING_LIT_RE, (m) => save(m));
3150
3151
  return s;
3151
3152
  };
3152
3153
  const restore = (s) => {
3153
3154
  return s.replace(/__STRLIT_(\d+)__/g, (_, i) => stash[Number(i)]);
3154
3155
  };
3155
- return { protect, restore };
3156
+ const replaceProtectedCall = (haystack, needle, replacement) => {
3157
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3158
+ const litValues = [];
3159
+ let pattern = "";
3160
+ let last = 0;
3161
+ for (const m of needle.matchAll(STRING_LIT_RE)) {
3162
+ pattern += escape(needle.slice(last, m.index));
3163
+ pattern += "__STRLIT_(\\d+)__";
3164
+ litValues.push(m[0]);
3165
+ last = m.index + m[0].length;
3166
+ }
3167
+ pattern += escape(needle.slice(last));
3168
+ for (const m of haystack.matchAll(new RegExp(pattern, "g"))) {
3169
+ const verified = m.slice(1).every((idx, i) => stash[Number(idx)] === litValues[i]);
3170
+ if (!verified)
3171
+ continue;
3172
+ return haystack.slice(0, m.index) + replacement() + haystack.slice(m.index + m[0].length);
3173
+ }
3174
+ return haystack;
3175
+ };
3176
+ return { protect, restore, replaceProtectedCall };
3156
3177
  }
3157
3178
  var VOID_ELEMENTS = new Set([
3158
3179
  "area",
@@ -4709,6 +4730,9 @@ function createAnalyzerContext(sourceFile, filePath) {
4709
4730
  jsxFunctions: new Map,
4710
4731
  jsxMultiReturnFunctions: new Map,
4711
4732
  reactiveFactories: new Map,
4733
+ declinedReactiveFactories: new Map,
4734
+ reactiveShapedHelpers: new Set,
4735
+ cleanFactoryImports: new Set,
4712
4736
  signalTupleRefs: new Map,
4713
4737
  propsType: null,
4714
4738
  propsParams: [],
@@ -5217,7 +5241,10 @@ var ErrorCodes = {
5217
5241
  STAGE_INIT_LOCAL_IN_TEMPLATE: "BF061",
5218
5242
  STAGE_AWAIT_IN_TEMPLATE: "BF062",
5219
5243
  INLINE_JSX_CALLBACK_CAPTURE: "BF080",
5220
- UNRECOGNIZED_REACTIVE_FACTORY: "BF110"
5244
+ UNRECOGNIZED_REACTIVE_FACTORY: "BF110",
5245
+ REACTIVE_FACTORY_RENAME_UNSUPPORTED: "BF111",
5246
+ REACTIVE_FACTORY_MODULE_CAPTURE: "BF112",
5247
+ REACTIVE_FACTORY_IMPORT_COLLISION: "BF113"
5221
5248
  };
5222
5249
  var errorMessages = {
5223
5250
  [ErrorCodes.MISSING_USE_CLIENT]: "'use client' directive required for components with createSignal or event handlers",
@@ -5241,7 +5268,10 @@ var errorMessages = {
5241
5268
  [ErrorCodes.STAGE_INIT_LOCAL_IN_TEMPLATE]: "Init-scope local referenced from template scope. The template lambda runs at module scope (via render() / renderChild()) and cannot reach init-body locals. Wrap the JSX expression in /* @client */, or lift the value to a prop or module-scope const.",
5242
5269
  [ErrorCodes.STAGE_AWAIT_IN_TEMPLATE]: "AwaitExpression in template scope. The generated template and init functions are synchronous — a bare `await` produces a SyntaxError at parse time. Move the await into the component body (before the return) or into an onMount/effect callback, and pass the resolved value to JSX.",
5243
5270
  [ErrorCodes.INLINE_JSX_CALLBACK_CAPTURE]: "Inline JSX-returning arrow function captures a non-module identifier. Extract the callback into a top-level 'use client' component (e.g. `function MyNode(n) { return <div/> }` then `renderNode={MyNode}`) or pass captured values via component props.",
5244
- [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."
5271
+ [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
+ [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
+ [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."
5245
5275
  };
5246
5276
  function createError(code, loc, options) {
5247
5277
  if (code === undefined || !(code in errorMessages)) {
@@ -5567,6 +5597,10 @@ function analyzeComponent(source, filePath, targetComponentName, program) {
5567
5597
  }
5568
5598
  const ctx = createAnalyzerContext(sourceFile, filePath);
5569
5599
  ctx.checker = checker;
5600
+ ctx.reactiveFactories = prescan.factories;
5601
+ ctx.declinedReactiveFactories = prescan.declined;
5602
+ ctx.reactiveShapedHelpers = prescan.reactiveShaped;
5603
+ ctx.cleanFactoryImports = prescan.cleanFactoryImports;
5570
5604
  const brandImportLoc = findBrandPackageImportLoc(sourceFile, filePath);
5571
5605
  if (!hadSharedProgram && brandImportLoc !== null) {
5572
5606
  ctx.errors.push(createError(ErrorCodes.SHARED_PROGRAM_REQUIRED, brandImportLoc));
@@ -6472,6 +6506,7 @@ var CLIENT_EXPORTS = new Set([
6472
6506
  "cleanupPortalPlaceholder",
6473
6507
  "createSearchParams",
6474
6508
  "queryHref",
6509
+ "formatDate",
6475
6510
  "Async",
6476
6511
  "Region"
6477
6512
  ]);
@@ -7721,27 +7756,368 @@ var REACTIVE_PRIMITIVES = new Set([
7721
7756
  function prescanReactiveFactoriesInSource(source, filePath) {
7722
7757
  const sourceFile = ts8.createSourceFile(filePath + ".prescan", source, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7723
7758
  const factories = new Map;
7759
+ const declined = new Map;
7760
+ const reactiveShaped = new Set;
7761
+ const cleanFactoryImports = new Set;
7724
7762
  function visitTop(node) {
7725
7763
  if (ts8.isFunctionDeclaration(node) && node.name && node.body) {
7726
- const info = detectReactiveFactory(node, sourceFile, filePath);
7727
- if (info)
7728
- factories.set(node.name.text, info);
7764
+ const det = detectReactiveFactory(node, sourceFile, filePath);
7765
+ if (!det)
7766
+ return;
7767
+ switch (det.kind) {
7768
+ case "factory":
7769
+ factories.set(node.name.text, det.info);
7770
+ break;
7771
+ case "declined":
7772
+ declined.set(node.name.text, det.declined);
7773
+ break;
7774
+ case "reactive-shaped":
7775
+ reactiveShaped.add(node.name.text);
7776
+ break;
7777
+ }
7729
7778
  }
7730
7779
  }
7731
7780
  ts8.forEachChild(sourceFile, visitTop);
7732
- return { factories, sourceFile };
7781
+ const result = { factories, declined, reactiveShaped, cleanFactoryImports, sourceFile };
7782
+ prescanImportedReactiveFactories(sourceFile, filePath, result);
7783
+ return result;
7784
+ }
7785
+ function toComponentRelativeSpecifier(resolvedAbs, componentFilePath) {
7786
+ let rel = path_default.relative(path_default.dirname(componentFilePath), resolvedAbs).split(path_default.sep).join("/");
7787
+ rel = rel.replace(/\.(tsx|ts|jsx|js)$/, "");
7788
+ if (rel === "")
7789
+ rel = ".";
7790
+ if (!rel.startsWith("."))
7791
+ rel = "./" + rel;
7792
+ return rel;
7793
+ }
7794
+ function buildEntryImportIndex(sf, filePath) {
7795
+ const index = new Map;
7796
+ for (const stmt of sf.statements) {
7797
+ if (!ts8.isImportDeclaration(stmt))
7798
+ continue;
7799
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7800
+ continue;
7801
+ if (stmt.importClause?.isTypeOnly)
7802
+ continue;
7803
+ const src = stmt.moduleSpecifier.text;
7804
+ const targetKey = src.startsWith("./") || src.startsWith("../") ? resolveRelativeImportToFile(src, filePath) ?? "unresolved:" + src : src;
7805
+ const namedBindings = stmt.importClause?.namedBindings;
7806
+ if (namedBindings && ts8.isNamedImports(namedBindings)) {
7807
+ 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 });
7811
+ }
7812
+ }
7813
+ }
7814
+ return index;
7815
+ }
7816
+ function collectEntryBindingNames(sf) {
7817
+ const names = new Set;
7818
+ function visit2(node) {
7819
+ if (ts8.isImportDeclaration(node) && node.importClause) {
7820
+ if (node.importClause.name)
7821
+ names.add(node.importClause.name.text);
7822
+ const namedBindings = node.importClause.namedBindings;
7823
+ if (namedBindings && ts8.isNamedImports(namedBindings)) {
7824
+ for (const el of namedBindings.elements)
7825
+ names.add(el.name.text);
7826
+ }
7827
+ if (namedBindings && ts8.isNamespaceImport(namedBindings)) {
7828
+ names.add(namedBindings.name.text);
7829
+ }
7830
+ }
7831
+ if (ts8.isVariableDeclaration(node)) {
7832
+ const out = [];
7833
+ addBindingNames(node.name, out);
7834
+ for (const n of out)
7835
+ names.add(n);
7836
+ }
7837
+ if ((ts8.isFunctionDeclaration(node) || ts8.isClassDeclaration(node) || ts8.isEnumDeclaration(node)) && node.name) {
7838
+ names.add(node.name.text);
7839
+ }
7840
+ if (ts8.isFunctionLike(node)) {
7841
+ for (const p of node.parameters) {
7842
+ const out = [];
7843
+ addBindingNames(p.name, out);
7844
+ for (const n of out)
7845
+ names.add(n);
7846
+ }
7847
+ }
7848
+ ts8.forEachChild(node, visit2);
7849
+ }
7850
+ visit2(sf);
7851
+ return names;
7852
+ }
7853
+ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7854
+ const candidateCallees = new Set;
7855
+ function collectCandidates(node) {
7856
+ if (ts8.isVariableDeclaration(node) && (ts8.isArrayBindingPattern(node.name) || ts8.isObjectBindingPattern(node.name)) && node.initializer && ts8.isCallExpression(node.initializer) && ts8.isIdentifier(node.initializer.expression)) {
7857
+ candidateCallees.add(node.initializer.expression.text);
7858
+ }
7859
+ ts8.forEachChild(node, collectCandidates);
7860
+ }
7861
+ collectCandidates(entrySourceFile);
7862
+ if (candidateCallees.size === 0)
7863
+ return;
7864
+ const importsToCheck = [];
7865
+ for (const stmt of entrySourceFile.statements) {
7866
+ if (!ts8.isImportDeclaration(stmt))
7867
+ continue;
7868
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7869
+ continue;
7870
+ const src = stmt.moduleSpecifier.text;
7871
+ if (!src.startsWith("./") && !src.startsWith("../"))
7872
+ continue;
7873
+ if (stmt.importClause?.isTypeOnly)
7874
+ continue;
7875
+ const namedBindings = stmt.importClause?.namedBindings;
7876
+ if (!namedBindings || !ts8.isNamedImports(namedBindings))
7877
+ continue;
7878
+ const specs = [];
7879
+ for (const el of namedBindings.elements) {
7880
+ if (el.isTypeOnly)
7881
+ continue;
7882
+ const local = el.name.text;
7883
+ if (!candidateCallees.has(local))
7884
+ continue;
7885
+ specs.push({ exported: (el.propertyName ?? el.name).text, local });
7886
+ }
7887
+ if (specs.length === 0)
7888
+ continue;
7889
+ importsToCheck.push({ src, specs });
7890
+ }
7891
+ if (importsToCheck.length === 0)
7892
+ return;
7893
+ const entryBindingNames = collectEntryBindingNames(entrySourceFile);
7894
+ const entryImportIndex = buildEntryImportIndex(entrySourceFile, filePath);
7895
+ const plannedInjections = new Map;
7896
+ for (const { src, specs } of importsToCheck) {
7897
+ const resolved = resolveRelativeImportToFile(src, filePath);
7898
+ if (!resolved)
7899
+ continue;
7900
+ let content;
7901
+ try {
7902
+ content = fs.readFileSync(resolved, "utf8");
7903
+ } catch {
7904
+ continue;
7905
+ }
7906
+ const alreadyKnown = (name) => result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name);
7907
+ 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;
7914
+ }
7915
+ const helperSf = ts8.createSourceFile(resolved + ".prescan", content, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7916
+ const localFns = new Map;
7917
+ const exportedFns = new Map;
7918
+ for (const stmt of helperSf.statements) {
7919
+ if (ts8.isFunctionDeclaration(stmt) && stmt.name && stmt.body) {
7920
+ localFns.set(stmt.name.text, stmt);
7921
+ const hasExportModifier = stmt.modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword) ?? false;
7922
+ const hasDefaultModifier = stmt.modifiers?.some((m) => m.kind === ts8.SyntaxKind.DefaultKeyword) ?? false;
7923
+ if (hasExportModifier && !hasDefaultModifier) {
7924
+ exportedFns.set(stmt.name.text, stmt);
7925
+ }
7926
+ }
7927
+ }
7928
+ for (const stmt of helperSf.statements) {
7929
+ if (ts8.isExportDeclaration(stmt) && stmt.exportClause && ts8.isNamedExports(stmt.exportClause) && !stmt.moduleSpecifier && !stmt.isTypeOnly) {
7930
+ for (const el of stmt.exportClause.elements) {
7931
+ if (el.isTypeOnly)
7932
+ continue;
7933
+ const fn = localFns.get((el.propertyName ?? el.name).text);
7934
+ if (fn)
7935
+ exportedFns.set(el.name.text, fn);
7936
+ }
7937
+ }
7938
+ }
7939
+ const moduleBindings = collectHelperModuleValueBindings(helperSf);
7940
+ for (const spec of specs) {
7941
+ if (alreadyKnown(spec.local))
7942
+ continue;
7943
+ const fn = exportedFns.get(spec.exported);
7944
+ if (!fn) {
7945
+ result.cleanFactoryImports.add(spec.local);
7946
+ continue;
7947
+ }
7948
+ const det = detectReactiveFactory(fn, helperSf, resolved);
7949
+ if (!det) {
7950
+ result.cleanFactoryImports.add(spec.local);
7951
+ continue;
7952
+ }
7953
+ switch (det.kind) {
7954
+ case "reactive-shaped":
7955
+ result.reactiveShaped.add(spec.local);
7956
+ break;
7957
+ case "declined":
7958
+ result.declined.set(spec.local, det.declined);
7959
+ break;
7960
+ case "factory": {
7961
+ const capture = moduleCaptureCheck(fn, det.info, moduleBindings, fn.name.text);
7962
+ if (capture.captured.length > 0) {
7963
+ result.declined.set(spec.local, {
7964
+ code: "BF112",
7965
+ detail: `'${capture.captured.join("', '")}'`,
7966
+ loc: det.info.loc
7967
+ });
7968
+ break;
7969
+ }
7970
+ const required = [];
7971
+ const pending = [];
7972
+ let declinedEntry = null;
7973
+ for (const ref of capture.importedRefs) {
7974
+ let specifier;
7975
+ let targetKey;
7976
+ if (ref.source.startsWith("./") || ref.source.startsWith("../")) {
7977
+ const abs = resolveRelativeImportToFile(ref.source, resolved);
7978
+ if (!abs) {
7979
+ declinedEntry = {
7980
+ code: "BF112",
7981
+ detail: `'${ref.localName}' (import '${ref.source}' did not resolve from the helper file)`,
7982
+ loc: det.info.loc
7983
+ };
7984
+ break;
7985
+ }
7986
+ specifier = toComponentRelativeSpecifier(abs, filePath);
7987
+ targetKey = abs;
7988
+ } else {
7989
+ specifier = ref.source;
7990
+ targetKey = ref.source;
7991
+ }
7992
+ const existing = entryImportIndex.get(ref.localName);
7993
+ if (existing && existing.targetKey === targetKey && existing.exportedName === ref.exportedName) {
7994
+ continue;
7995
+ }
7996
+ const planned = plannedInjections.get(ref.localName);
7997
+ const collides = existing !== undefined || planned !== undefined && (planned.targetKey !== targetKey || planned.exportedName !== ref.exportedName) || planned === undefined && entryBindingNames.has(ref.localName);
7998
+ if (collides) {
7999
+ declinedEntry = {
8000
+ code: "BF113",
8001
+ detail: `'${ref.localName}' from '${specifier}'`,
8002
+ loc: det.info.loc
8003
+ };
8004
+ break;
8005
+ }
8006
+ pending.push([ref.localName, { targetKey, exportedName: ref.exportedName }]);
8007
+ required.push({ localName: ref.localName, exportedName: ref.exportedName, specifier });
8008
+ }
8009
+ if (declinedEntry) {
8010
+ result.declined.set(spec.local, declinedEntry);
8011
+ break;
8012
+ }
8013
+ for (const [name, id] of pending)
8014
+ plannedInjections.set(name, id);
8015
+ det.info.sourceFilePath = resolved;
8016
+ if (required.length > 0)
8017
+ det.info.requiredImports = required;
8018
+ result.factories.set(spec.local, det.info);
8019
+ break;
8020
+ }
8021
+ }
8022
+ }
8023
+ }
8024
+ }
8025
+ function collectHelperModuleValueBindings(sf) {
8026
+ const local = new Set;
8027
+ const imported = new Map;
8028
+ for (const stmt of sf.statements) {
8029
+ if (ts8.isVariableStatement(stmt)) {
8030
+ const out = [];
8031
+ for (const decl of stmt.declarationList.declarations) {
8032
+ addBindingNames(decl.name, out);
8033
+ }
8034
+ for (const n of out)
8035
+ local.add(n);
8036
+ continue;
8037
+ }
8038
+ if ((ts8.isFunctionDeclaration(stmt) || ts8.isClassDeclaration(stmt) || ts8.isEnumDeclaration(stmt)) && stmt.name) {
8039
+ local.add(stmt.name.text);
8040
+ continue;
8041
+ }
8042
+ if (ts8.isImportDeclaration(stmt)) {
8043
+ if (stmt.importClause?.isTypeOnly)
8044
+ continue;
8045
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
8046
+ continue;
8047
+ const src = stmt.moduleSpecifier.text;
8048
+ if (src === "@barefootjs/client" || src === "@barefootjs/client/runtime")
8049
+ continue;
8050
+ if (stmt.importClause?.name)
8051
+ local.add(stmt.importClause.name.text);
8052
+ const namedBindings = stmt.importClause?.namedBindings;
8053
+ if (namedBindings && ts8.isNamedImports(namedBindings)) {
8054
+ 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 });
8058
+ }
8059
+ }
8060
+ if (namedBindings && ts8.isNamespaceImport(namedBindings)) {
8061
+ local.add(namedBindings.name.text);
8062
+ }
8063
+ }
8064
+ }
8065
+ return { local, imported };
8066
+ }
8067
+ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
8068
+ if (!fn.body)
8069
+ return { captured: [], importedRefs: [] };
8070
+ const free = extractFreeIdentifiersFromNode(fn.body);
8071
+ const exclude = new Set(info.params);
8072
+ for (const b of info.localBindings)
8073
+ exclude.add(b);
8074
+ for (const r of info.returnTupleIdentifiers)
8075
+ exclude.add(r);
8076
+ for (const p of REACTIVE_PRIMITIVES)
8077
+ exclude.add(p);
8078
+ exclude.add(selfName);
8079
+ const captured = [];
8080
+ const importedRefs = [];
8081
+ for (const id of free) {
8082
+ if (exclude.has(id))
8083
+ continue;
8084
+ if (moduleBindings.local.has(id)) {
8085
+ captured.push(id);
8086
+ continue;
8087
+ }
8088
+ const imp = moduleBindings.imported.get(id);
8089
+ if (imp)
8090
+ importedRefs.push({ localName: id, source: imp.source, exportedName: imp.exportedName });
8091
+ }
8092
+ captured.sort();
8093
+ importedRefs.sort((a, b) => a.localName < b.localName ? -1 : 1);
8094
+ return { captured, importedRefs };
7733
8095
  }
7734
8096
  function detectReactiveFactory(node, sourceFile, filePath) {
7735
8097
  if (!node.body || !node.name)
7736
8098
  return null;
7737
- let tupleReturn = null;
8099
+ let hasReactiveCall = false;
8100
+ function checkForReactive(n) {
8101
+ if (hasReactiveCall)
8102
+ return;
8103
+ if (ts8.isCallExpression(n) && ts8.isIdentifier(n.expression) && REACTIVE_PRIMITIVES.has(n.expression.text)) {
8104
+ hasReactiveCall = true;
8105
+ return;
8106
+ }
8107
+ ts8.forEachChild(n, checkForReactive);
8108
+ }
8109
+ checkForReactive(node.body);
8110
+ if (!hasReactiveCall)
8111
+ return null;
8112
+ const loc = getSourceLocation(node, sourceFile, filePath);
8113
+ let returnExpr = null;
7738
8114
  let returnCount = 0;
7739
8115
  for (const stmt of node.body.statements) {
7740
8116
  if (!ts8.isReturnStatement(stmt))
7741
8117
  continue;
7742
8118
  returnCount++;
7743
8119
  if (!stmt.expression)
7744
- return null;
8120
+ return { kind: "reactive-shaped" };
7745
8121
  let expr = stmt.expression;
7746
8122
  while (ts8.isParenthesizedExpression(expr))
7747
8123
  expr = expr.expression;
@@ -7749,33 +8125,42 @@ function detectReactiveFactory(node, sourceFile, filePath) {
7749
8125
  expr = expr.expression;
7750
8126
  if (ts8.isTypeAssertionExpression(expr))
7751
8127
  expr = expr.expression;
7752
- if (!ts8.isArrayLiteralExpression(expr))
7753
- return null;
7754
- tupleReturn = expr;
8128
+ returnExpr = expr;
7755
8129
  }
7756
- if (returnCount !== 1 || !tupleReturn)
7757
- return null;
8130
+ if (returnCount !== 1 || !returnExpr)
8131
+ return { kind: "reactive-shaped" };
7758
8132
  const returnTupleIdentifiers = [];
7759
- for (const el of tupleReturn.elements) {
7760
- if (!ts8.isIdentifier(el))
7761
- return null;
7762
- returnTupleIdentifiers.push(el.text);
7763
- }
7764
- if (returnTupleIdentifiers.length === 0)
7765
- return null;
7766
- let hasReactiveCall = false;
7767
- function checkForReactive(n) {
7768
- if (hasReactiveCall)
7769
- return;
7770
- if (ts8.isCallExpression(n) && ts8.isIdentifier(n.expression) && REACTIVE_PRIMITIVES.has(n.expression.text)) {
7771
- hasReactiveCall = true;
7772
- return;
8133
+ let returnKind;
8134
+ if (ts8.isArrayLiteralExpression(returnExpr)) {
8135
+ returnKind = "tuple";
8136
+ for (const el of returnExpr.elements) {
8137
+ if (!ts8.isIdentifier(el))
8138
+ return { kind: "reactive-shaped" };
8139
+ returnTupleIdentifiers.push(el.text);
8140
+ }
8141
+ if (returnTupleIdentifiers.length === 0)
8142
+ return { kind: "reactive-shaped" };
8143
+ } else if (ts8.isObjectLiteralExpression(returnExpr)) {
8144
+ returnKind = "object";
8145
+ const hasNonShorthand = returnExpr.properties.some((p) => !ts8.isShorthandPropertyAssignment(p));
8146
+ if (hasNonShorthand) {
8147
+ return {
8148
+ kind: "declined",
8149
+ declined: {
8150
+ code: "BF111",
8151
+ detail: `return object of '${node.name.text}' uses non-shorthand properties`,
8152
+ loc
8153
+ }
8154
+ };
7773
8155
  }
7774
- ts8.forEachChild(n, checkForReactive);
8156
+ for (const p of returnExpr.properties) {
8157
+ returnTupleIdentifiers.push(p.name.text);
8158
+ }
8159
+ if (returnTupleIdentifiers.length === 0)
8160
+ return { kind: "reactive-shaped" };
8161
+ } else {
8162
+ return { kind: "reactive-shaped" };
7775
8163
  }
7776
- checkForReactive(node.body);
7777
- if (!hasReactiveCall)
7778
- return null;
7779
8164
  const localBindings = [];
7780
8165
  for (const stmt of node.body.statements) {
7781
8166
  if (ts8.isVariableStatement(stmt)) {
@@ -7788,19 +8173,24 @@ function detectReactiveFactory(node, sourceFile, filePath) {
7788
8173
  }
7789
8174
  const bodyStatements = node.body.statements.filter((s) => !ts8.isReturnStatement(s)).map((s) => s.getText(sourceFile)).join(`
7790
8175
  `);
7791
- const params = node.parameters.map((p) => {
7792
- if (ts8.isIdentifier(p.name))
7793
- return p.name.text;
7794
- return "";
7795
- });
7796
- if (params.some((p) => p === ""))
7797
- return null;
8176
+ const params = [];
8177
+ for (const p of node.parameters) {
8178
+ if (ts8.isIdentifier(p.name)) {
8179
+ params.push(p.name.text);
8180
+ continue;
8181
+ }
8182
+ return { kind: "reactive-shaped" };
8183
+ }
7798
8184
  return {
7799
- params,
7800
- bodySource: bodyStatements,
7801
- returnTupleIdentifiers,
7802
- localBindings,
7803
- loc: getSourceLocation(node, sourceFile, filePath)
8185
+ kind: "factory",
8186
+ info: {
8187
+ params,
8188
+ bodySource: bodyStatements,
8189
+ returnTupleIdentifiers,
8190
+ returnKind,
8191
+ localBindings,
8192
+ loc
8193
+ }
7804
8194
  };
7805
8195
  }
7806
8196
  function addBindingNames(name, out) {
@@ -7825,6 +8215,7 @@ function rewriteFactoryCallsInSource(source, prescan) {
7825
8215
  const { factories, sourceFile } = prescan;
7826
8216
  const edits = [];
7827
8217
  let callSiteIndex = 0;
8218
+ const inlinedFactories = new Set;
7828
8219
  function visitStmt(node, inComponent) {
7829
8220
  if (ts8.isVariableStatement(node) && inComponent) {
7830
8221
  for (const decl of node.declarationList.declarations) {
@@ -7838,8 +8229,6 @@ function rewriteFactoryCallsInSource(source, prescan) {
7838
8229
  });
7839
8230
  }
7840
8231
  function maybeRewriteDecl(stmt, decl) {
7841
- if (!ts8.isArrayBindingPattern(decl.name))
7842
- return;
7843
8232
  if (!decl.initializer || !ts8.isCallExpression(decl.initializer))
7844
8233
  return;
7845
8234
  if (!ts8.isIdentifier(decl.initializer.expression))
@@ -7848,7 +8237,21 @@ function rewriteFactoryCallsInSource(source, prescan) {
7848
8237
  const factory = factories.get(factoryName);
7849
8238
  if (!factory)
7850
8239
  return;
7851
- const elements = decl.name.elements;
8240
+ if (ts8.isArrayBindingPattern(decl.name)) {
8241
+ if (factory.returnKind !== "tuple")
8242
+ return;
8243
+ rewriteTupleDecl(stmt, decl.name, decl.initializer, factory);
8244
+ return;
8245
+ }
8246
+ if (ts8.isObjectBindingPattern(decl.name)) {
8247
+ if (factory.returnKind !== "object")
8248
+ return;
8249
+ rewriteObjectDecl(stmt, decl.name, decl.initializer, factory);
8250
+ return;
8251
+ }
8252
+ }
8253
+ function rewriteTupleDecl(stmt, pattern, call, factory) {
8254
+ const elements = pattern.elements;
7852
8255
  if (elements.length !== factory.returnTupleIdentifiers.length)
7853
8256
  return;
7854
8257
  const callerNames = [];
@@ -7857,15 +8260,43 @@ function rewriteFactoryCallsInSource(source, prescan) {
7857
8260
  return;
7858
8261
  callerNames.push(el.name.text);
7859
8262
  }
7860
- const argTexts = decl.initializer.arguments.map((a) => a.getText(sourceFile));
8263
+ const excludeFromSuffixRename = new Set(factory.params);
8264
+ for (const r of factory.returnTupleIdentifiers)
8265
+ excludeFromSuffixRename.add(r);
8266
+ const renameReturnToCallerNames = new Map;
8267
+ for (let i = 0;i < factory.returnTupleIdentifiers.length; i++) {
8268
+ renameReturnToCallerNames.set(factory.returnTupleIdentifiers[i], callerNames[i]);
8269
+ }
8270
+ inlineFactoryCallAtSite(stmt, factory, call.arguments, excludeFromSuffixRename, renameReturnToCallerNames);
8271
+ }
8272
+ function rewriteObjectDecl(stmt, pattern, call, factory) {
8273
+ const destructured = new Set;
8274
+ for (const el of pattern.elements) {
8275
+ if (el.dotDotDotToken)
8276
+ return;
8277
+ if (el.propertyName)
8278
+ return;
8279
+ if (el.initializer)
8280
+ return;
8281
+ if (!ts8.isIdentifier(el.name))
8282
+ return;
8283
+ if (!factory.returnTupleIdentifiers.includes(el.name.text))
8284
+ return;
8285
+ destructured.add(el.name.text);
8286
+ }
8287
+ const excludeFromSuffixRename = new Set(factory.params);
8288
+ for (const d of destructured)
8289
+ excludeFromSuffixRename.add(d);
8290
+ inlineFactoryCallAtSite(stmt, factory, call.arguments, excludeFromSuffixRename, null);
8291
+ }
8292
+ function inlineFactoryCallAtSite(stmt, factory, args, excludeFromSuffixRename, renameReturnToCallerNames) {
8293
+ const argTexts = args.map((a) => a.getText(sourceFile));
7861
8294
  const thisCallIndex = callSiteIndex++;
7862
8295
  const suffix = `_bf${thisCallIndex}`;
7863
8296
  let body = factory.bodySource;
7864
8297
  const internalRenames = new Set(factory.localBindings);
7865
- for (const p of factory.params)
7866
- internalRenames.delete(p);
7867
- for (const r of factory.returnTupleIdentifiers)
7868
- internalRenames.delete(r);
8298
+ for (const ex of excludeFromSuffixRename)
8299
+ internalRenames.delete(ex);
7869
8300
  for (const name of internalRenames) {
7870
8301
  body = body.replace(new RegExp(`\\b${escapeRegex(name)}\\b`, "g"), name + suffix);
7871
8302
  }
@@ -7876,20 +8307,44 @@ function rewriteFactoryCallsInSource(source, prescan) {
7876
8307
  const wrapped = atomicArg.test(a.trim()) ? a.trim() : `(${a})`;
7877
8308
  body = body.replace(new RegExp(`\\b${escapeRegex(p)}\\b`, "g"), wrapped);
7878
8309
  }
7879
- for (let i = 0;i < factory.returnTupleIdentifiers.length; i++) {
7880
- const n = factory.returnTupleIdentifiers[i];
7881
- const caller = callerNames[i];
7882
- body = body.replace(new RegExp(`\\b${escapeRegex(n)}\\b`, "g"), caller);
8310
+ if (renameReturnToCallerNames) {
8311
+ for (const [n, caller] of renameReturnToCallerNames) {
8312
+ body = body.replace(new RegExp(`\\b${escapeRegex(n)}\\b`, "g"), caller);
8313
+ }
7883
8314
  }
7884
8315
  edits.push({
7885
8316
  start: stmt.getStart(sourceFile),
7886
8317
  end: stmt.getEnd(),
7887
8318
  replacement: body
7888
8319
  });
8320
+ inlinedFactories.add(factory);
7889
8321
  }
7890
8322
  visitStmt(sourceFile, false);
7891
8323
  if (edits.length === 0)
7892
8324
  return source;
8325
+ const importsBySpecifier = new Map;
8326
+ for (const f of inlinedFactories) {
8327
+ for (const r of f.requiredImports ?? []) {
8328
+ let names = importsBySpecifier.get(r.specifier);
8329
+ if (!names) {
8330
+ names = new Map;
8331
+ importsBySpecifier.set(r.specifier, names);
8332
+ }
8333
+ names.set(r.localName, r.exportedName);
8334
+ }
8335
+ }
8336
+ if (importsBySpecifier.size > 0) {
8337
+ const lines = [...importsBySpecifier].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([spec, names]) => {
8338
+ 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}'`;
8340
+ });
8341
+ const at = factoryImportInsertionOffset(sourceFile);
8342
+ edits.push({ start: at, end: at, replacement: at === 0 ? lines.join(`
8343
+ `) + `
8344
+ ` : `
8345
+ ` + lines.join(`
8346
+ `) });
8347
+ }
7893
8348
  edits.sort((a, b) => b.start - a.start);
7894
8349
  let out = source;
7895
8350
  for (const e of edits) {
@@ -7897,6 +8352,20 @@ function rewriteFactoryCallsInSource(source, prescan) {
7897
8352
  }
7898
8353
  return out;
7899
8354
  }
8355
+ function factoryImportInsertionOffset(sf) {
8356
+ let lastImportEnd = -1;
8357
+ let directiveEnd = -1;
8358
+ for (const stmt of sf.statements) {
8359
+ if (ts8.isImportDeclaration(stmt)) {
8360
+ lastImportEnd = stmt.getEnd();
8361
+ continue;
8362
+ }
8363
+ if (directiveEnd === -1 && ts8.isExpressionStatement(stmt) && ts8.isStringLiteral(stmt.expression) && stmt.expression.text === "use client") {
8364
+ directiveEnd = stmt.getEnd();
8365
+ }
8366
+ }
8367
+ return lastImportEnd >= 0 ? lastImportEnd : directiveEnd >= 0 ? directiveEnd : 0;
8368
+ }
7900
8369
  function isPascalCaseComponentFn(node) {
7901
8370
  if (ts8.isFunctionDeclaration(node) && node.name) {
7902
8371
  return /^[A-Z]/.test(node.name.text);
@@ -7909,6 +8378,25 @@ function isPascalCaseComponentFn(node) {
7909
8378
  function escapeRegex(s) {
7910
8379
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7911
8380
  }
8381
+ function declinedFactoryMessage(callee, d) {
8382
+ if (d.code === "BF112") {
8383
+ 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.`;
8384
+ }
8385
+ if (d.code === "BF113") {
8386
+ return `Reactive factory '${callee}' cannot be inlined: it needs ${d.detail} ` + `imported 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 (import { x as y }).`;
8387
+ }
8388
+ return `Reactive factory '${callee}' cannot be inlined: ${d.detail}.`;
8389
+ }
8390
+ function declinedFactoryErrorCode(code) {
8391
+ switch (code) {
8392
+ case "BF112":
8393
+ return ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE;
8394
+ case "BF113":
8395
+ return ErrorCodes.REACTIVE_FACTORY_IMPORT_COLLISION;
8396
+ default:
8397
+ return ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED;
8398
+ }
8399
+ }
7912
8400
  function validateReactiveFactoryCalls(ctx) {
7913
8401
  if (!ctx.componentNode)
7914
8402
  return;
@@ -7919,26 +8407,104 @@ function validateReactiveFactoryCalls(ctx) {
7919
8407
  if (!ts8.isVariableStatement(stmt))
7920
8408
  continue;
7921
8409
  for (const decl of stmt.declarationList.declarations) {
7922
- if (!ts8.isArrayBindingPattern(decl.name))
7923
- continue;
7924
8410
  if (!decl.initializer || !ts8.isCallExpression(decl.initializer))
7925
8411
  continue;
7926
8412
  if (!ts8.isIdentifier(decl.initializer.expression))
7927
8413
  continue;
7928
8414
  const callee = decl.initializer.expression.text;
7929
- if (callee === "createSignal" || callee === "createMemo")
7930
- continue;
7931
- if (resolveEnvSignalKey(decl.initializer, ctx))
8415
+ const loc = getSourceLocation(stmt, ctx.sourceFile, ctx.filePath);
8416
+ if (ts8.isArrayBindingPattern(decl.name)) {
8417
+ if (callee === "createSignal" || callee === "createMemo")
8418
+ continue;
8419
+ if (resolveEnvSignalKey(decl.initializer, ctx))
8420
+ continue;
8421
+ const declinedEntry = ctx.declinedReactiveFactories.get(callee);
8422
+ if (declinedEntry) {
8423
+ ctx.errors.push(createError(declinedFactoryErrorCode(declinedEntry.code), loc, { severity: "error", message: declinedFactoryMessage(callee, declinedEntry) }));
8424
+ continue;
8425
+ }
8426
+ const objectFactory = ctx.reactiveFactories.get(callee);
8427
+ if (objectFactory && objectFactory.returnKind === "object") {
8428
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8429
+ severity: "error",
8430
+ message: `'${callee}' is a reactive factory that returns an object — destructure ` + `it with a matching object pattern: const { ${objectFactory.returnTupleIdentifiers.join(", ")} } = ${callee}(...)`
8431
+ }));
8432
+ continue;
8433
+ }
8434
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8435
+ severity: "error",
8436
+ message: `Tuple destructuring of '${callee}(...)': this helper is not a ` + `recognised reactive factory (createSignal / createMemo / a ` + `same-file helper that wraps them with a single \`return [a, b, ...]\`).`,
8437
+ suggestion: {
8438
+ message: `Inline the createSignal call at the call site, or move the ` + `helper into this file as a function that returns a tuple of ` + `identifiers at its single exit point.`
8439
+ }
8440
+ }));
7932
8441
  continue;
7933
- ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, getSourceLocation(stmt, ctx.sourceFile, ctx.filePath), {
8442
+ }
8443
+ if (ts8.isObjectBindingPattern(decl.name)) {
8444
+ validateObjectFactoryDestructure(ctx, decl.name, callee, loc);
8445
+ }
8446
+ }
8447
+ }
8448
+ }
8449
+ function validateObjectFactoryDestructure(ctx, pattern, callee, loc) {
8450
+ const factory = ctx.reactiveFactories.get(callee);
8451
+ if (factory) {
8452
+ if (factory.returnKind === "tuple") {
8453
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
7934
8454
  severity: "error",
7935
- message: `Tuple destructuring of '${callee}(...)': this helper is not a ` + `recognised reactive factory (createSignal / createMemo / a ` + `same-file helper that wraps them with a single \`return [a, b, ...]\`).`,
7936
- suggestion: {
7937
- message: `Inline the createSignal call at the call site, or move the ` + `helper into this file as a function that returns a tuple of ` + `identifiers at its single exit point.`
7938
- }
8455
+ message: `'${callee}' is a reactive factory that returns a tuple destructure ` + `it positionally: const [${factory.returnTupleIdentifiers.join(", ")}] = ${callee}(...)`
8456
+ }));
8457
+ return;
8458
+ }
8459
+ const hasUnsupportedElement = pattern.elements.some((el) => !!el.propertyName || !!el.initializer || !!el.dotDotDotToken || !ts8.isIdentifier(el.name));
8460
+ if (hasUnsupportedElement) {
8461
+ ctx.errors.push(createError(ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED, loc, {
8462
+ severity: "error",
8463
+ message: `Object destructure of reactive factory '${callee}' uses a property ` + `rename, default, or rest element; only shorthand destructuring of ` + `{ ${factory.returnTupleIdentifiers.join(", ")} } is supported.`
7939
8464
  }));
8465
+ return;
8466
+ }
8467
+ const unknown = pattern.elements.map((el) => ts8.isIdentifier(el.name) ? el.name.text : "").filter((name) => name && !factory.returnTupleIdentifiers.includes(name));
8468
+ if (unknown.length > 0) {
8469
+ const label = unknown.length === 1 ? "property" : "properties";
8470
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8471
+ severity: "error",
8472
+ message: `Object destructure of reactive factory '${callee}' references ${label} ` + `'${unknown.join("', '")}' not present in its return { ${factory.returnTupleIdentifiers.join(", ")} }.`
8473
+ }));
8474
+ return;
8475
+ }
8476
+ return;
8477
+ }
8478
+ const declinedEntry = ctx.declinedReactiveFactories.get(callee);
8479
+ if (declinedEntry) {
8480
+ ctx.errors.push(createError(declinedFactoryErrorCode(declinedEntry.code), loc, { severity: "error", message: declinedFactoryMessage(callee, declinedEntry) }));
8481
+ return;
8482
+ }
8483
+ if (ctx.reactiveShapedHelpers.has(callee)) {
8484
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8485
+ severity: "error",
8486
+ message: `Object destructure of '${callee}(...)': this helper wraps a reactive ` + `primitive but does not match the inlinable factory shape (single ` + "`return { a, b }` of shorthand identifiers at its one exit point)."
8487
+ }));
8488
+ return;
8489
+ }
8490
+ if (ctx.cleanFactoryImports.has(callee))
8491
+ return;
8492
+ let matchedImportSource = null;
8493
+ for (const imp of ctx.imports) {
8494
+ if (imp.isTypeOnly)
8495
+ continue;
8496
+ const spec = imp.specifiers.find((s) => !s.isTypeOnly && (s.alias ?? s.name) === callee);
8497
+ if (spec) {
8498
+ matchedImportSource = imp.source;
8499
+ break;
7940
8500
  }
7941
8501
  }
8502
+ if (matchedImportSource !== null && !matchedImportSource.startsWith("@barefootjs/") && /^(use|create)[A-Z]/.test(callee)) {
8503
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8504
+ severity: "error",
8505
+ message: `Object destructure of imported '${callee}(...)': the compiler cannot ` + `inspect this import (non-relative or unresolvable path), so if it wraps ` + `createSignal/createMemo the destructured bindings will not be reactive. Move ` + `the helper to a relative-imported file or inline its body.`
8506
+ }));
8507
+ }
7942
8508
  }
7943
8509
 
7944
8510
  // src/jsx-to-ir.ts
@@ -8460,6 +9026,330 @@ function resolveFreeRefs(node, env) {
8460
9026
  return resolveFreeRefsInternal(node, env, new Set);
8461
9027
  }
8462
9028
 
9029
+ // src/to-locale-date-lowering.ts
9030
+ var TO_LOCALE_TZ_RE = /^(?:UTC|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
9031
+ var PROBE_UTC = new Date(Date.UTC(2001, 1, 3));
9032
+ var formatCache = new Map;
9033
+ var namesCache = new Map;
9034
+ function deriveMonthNames(locale, ctx) {
9035
+ return deriveNamesCached(`${locale}|m|${ctx}`, () => {
9036
+ const months = (width) => Array.from({ length: 12 }, (_, m) => probePart(locale, ctx === "formatting" ? { month: width, day: "numeric" } : { month: width }, Date.UTC(2001, m, 15), "month"));
9037
+ return [...months("long"), ...months("short")];
9038
+ });
9039
+ }
9040
+ function deriveWeekdayNames(locale, ctx) {
9041
+ return deriveNamesCached(`${locale}|w|${ctx}`, () => {
9042
+ const weekdays = (width) => Array.from({ length: 7 }, (_, d) => probePart(locale, ctx === "formatting" ? { weekday: width, month: "numeric", day: "numeric" } : { weekday: width }, Date.UTC(2023, 0, 1 + d), "weekday"));
9043
+ return [...weekdays("long"), ...weekdays("short")];
9044
+ });
9045
+ }
9046
+ function probePart(locale, options, utc, type) {
9047
+ const parts = new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" }).formatToParts(new Date(utc));
9048
+ const found = parts.find((p) => p.type === type);
9049
+ if (!found || !found.value)
9050
+ throw new Error("missing part");
9051
+ return found.value;
9052
+ }
9053
+ function deriveNamesCached(key, derive) {
9054
+ const cached = namesCache.get(key);
9055
+ if (cached !== undefined)
9056
+ return cached;
9057
+ let derived;
9058
+ try {
9059
+ derived = derive();
9060
+ } catch {
9061
+ derived = null;
9062
+ }
9063
+ namesCache.set(key, derived);
9064
+ return derived;
9065
+ }
9066
+ function resolveLocaleDateFormat(locale, probeOptions) {
9067
+ const key = `${locale}|${JSON.stringify(probeOptions, Object.keys(probeOptions).sort())}`;
9068
+ const cached = formatCache.get(key);
9069
+ if (cached !== undefined)
9070
+ return cached;
9071
+ const derived = deriveFormat(locale, probeOptions);
9072
+ formatCache.set(key, derived);
9073
+ return derived;
9074
+ }
9075
+ var VERIFY_UTC = new Date(Date.UTC(2001, 4, 13));
9076
+ function renderPatternAt(pattern, names, y, m, d, wd) {
9077
+ const pad2 = (n) => String(n).padStart(2, "0");
9078
+ return pattern.replace(/YYYY|MMMM|MMM|MM|DD|dddd|ddd|M|D/g, (token) => {
9079
+ switch (token) {
9080
+ case "YYYY":
9081
+ return String(y).padStart(4, "0");
9082
+ case "MMMM":
9083
+ return names[m - 1] ?? "";
9084
+ case "MMM":
9085
+ return names[12 + m - 1] ?? "";
9086
+ case "MM":
9087
+ return pad2(m);
9088
+ case "M":
9089
+ return String(m);
9090
+ case "DD":
9091
+ return pad2(d);
9092
+ case "D":
9093
+ return String(d);
9094
+ case "dddd":
9095
+ return names[24 + wd] ?? "";
9096
+ default:
9097
+ return names[31 + wd] ?? "";
9098
+ }
9099
+ });
9100
+ }
9101
+ function deriveFormat(locale, probeOptions) {
9102
+ let dtf;
9103
+ let parts;
9104
+ try {
9105
+ dtf = new Intl.DateTimeFormat(locale, {
9106
+ ...probeOptions,
9107
+ timeZone: "UTC"
9108
+ });
9109
+ const resolved = dtf.resolvedOptions();
9110
+ if (resolved.calendar !== "gregory" || resolved.numberingSystem !== "latn")
9111
+ return null;
9112
+ parts = dtf.formatToParts(PROBE_UTC);
9113
+ } catch {
9114
+ return null;
9115
+ }
9116
+ const monthTables = [
9117
+ deriveMonthNames(locale, "formatting"),
9118
+ deriveMonthNames(locale, "standalone")
9119
+ ];
9120
+ const weekdayTables = [
9121
+ deriveWeekdayNames(locale, "formatting"),
9122
+ deriveWeekdayNames(locale, "standalone")
9123
+ ];
9124
+ let monthTable = null;
9125
+ let weekdayTable = null;
9126
+ let pattern = "";
9127
+ let usesNames = false;
9128
+ for (const part of parts) {
9129
+ switch (part.type) {
9130
+ case "year":
9131
+ if (part.value !== "2001")
9132
+ return null;
9133
+ pattern += "YYYY";
9134
+ break;
9135
+ case "month": {
9136
+ if (part.value === "2") {
9137
+ pattern += "M";
9138
+ break;
9139
+ }
9140
+ if (part.value === "02") {
9141
+ pattern += "MM";
9142
+ break;
9143
+ }
9144
+ const wide = monthTables.find((t) => t && part.value === t[1]) ?? null;
9145
+ const abbr = wide ? null : monthTables.find((t) => t && part.value === t[12 + 1]) ?? null;
9146
+ if (wide)
9147
+ pattern += "MMMM";
9148
+ else if (abbr)
9149
+ pattern += "MMM";
9150
+ else
9151
+ return null;
9152
+ monthTable = wide ?? abbr;
9153
+ usesNames = true;
9154
+ break;
9155
+ }
9156
+ case "day":
9157
+ if (part.value === "3")
9158
+ pattern += "D";
9159
+ else if (part.value === "03")
9160
+ pattern += "DD";
9161
+ else
9162
+ return null;
9163
+ break;
9164
+ case "weekday": {
9165
+ const wide = weekdayTables.find((t) => t && part.value === t[6]) ?? null;
9166
+ const abbr = wide ? null : weekdayTables.find((t) => t && part.value === t[7 + 6]) ?? null;
9167
+ if (wide)
9168
+ pattern += "dddd";
9169
+ else if (abbr)
9170
+ pattern += "ddd";
9171
+ else
9172
+ return null;
9173
+ weekdayTable = wide ?? abbr;
9174
+ usesNames = true;
9175
+ break;
9176
+ }
9177
+ case "literal":
9178
+ if (/[YMD]/.test(part.value) || /ddd/.test(part.value))
9179
+ return null;
9180
+ pattern += part.value;
9181
+ break;
9182
+ default:
9183
+ return null;
9184
+ }
9185
+ }
9186
+ if (!/YYYY|MMMM|MMM|MM|M/.test(pattern) && !/DD|D/.test(pattern))
9187
+ return null;
9188
+ if (!usesNames)
9189
+ return { pattern, names: null };
9190
+ const names = [
9191
+ ...monthTable ?? monthTables[0] ?? monthTables[1] ?? Array(24).fill(""),
9192
+ ...weekdayTable ?? weekdayTables[0] ?? weekdayTables[1] ?? Array(14).fill("")
9193
+ ];
9194
+ if (renderPatternAt(pattern, names, 2001, 5, 13, 0) !== dtf.format(VERIFY_UTC))
9195
+ return null;
9196
+ return { pattern, names };
9197
+ }
9198
+ function unionMemberLiteral(member) {
9199
+ const m = /^'([^'\\]*)'$|^"([^"\\]*)"$/.exec(member.raw.trim());
9200
+ return m ? m[1] ?? m[2] : null;
9201
+ }
9202
+ function resolveLocaleUnionMembers(locale, metadata) {
9203
+ let sourcePropName = null;
9204
+ if (metadata.propsObjectName) {
9205
+ if (locale.kind === "member" && !locale.computed && locale.object.kind === "identifier" && locale.object.name === metadata.propsObjectName) {
9206
+ sourcePropName = locale.property;
9207
+ }
9208
+ } else if (locale.kind === "identifier") {
9209
+ const name = locale.name;
9210
+ const param = metadata.propsParams?.find((pp) => pp.name === name);
9211
+ if (param)
9212
+ sourcePropName = param.sourceName ?? param.name;
9213
+ }
9214
+ if (!sourcePropName)
9215
+ return null;
9216
+ const target = sourcePropName;
9217
+ const prop = metadata.propsType?.properties?.find((p) => p.name === target);
9218
+ if (!prop || prop.optional)
9219
+ return null;
9220
+ const type = prop.type;
9221
+ if (type.kind !== "union" || !type.unionTypes || type.unionTypes.length === 0)
9222
+ return null;
9223
+ const members = [];
9224
+ for (const member of type.unionTypes) {
9225
+ const value = unionMemberLiteral(member);
9226
+ if (value === null)
9227
+ return null;
9228
+ members.push(value);
9229
+ }
9230
+ return members;
9231
+ }
9232
+ var strLit = (value) => ({ kind: "literal", value, literalType: "string" });
9233
+ function strArr(values) {
9234
+ return {
9235
+ kind: "array-literal",
9236
+ elements: values.map((v) => strLit(v)),
9237
+ raw: JSON.stringify(values)
9238
+ };
9239
+ }
9240
+ function foldMembers(locale, members, leaves, allEqual) {
9241
+ let expr = leaves[leaves.length - 1];
9242
+ if (allEqual)
9243
+ return expr;
9244
+ for (let i = leaves.length - 2;i >= 0; i--) {
9245
+ expr = {
9246
+ kind: "conditional",
9247
+ test: { kind: "binary", op: "===", left: locale, right: strLit(members[i]) },
9248
+ consequent: leaves[i],
9249
+ alternate: expr
9250
+ };
9251
+ }
9252
+ return expr;
9253
+ }
9254
+ function matchToLocaleDateStringCall(callee, args, metadata) {
9255
+ if (callee.kind !== "member" || callee.computed)
9256
+ return null;
9257
+ if (callee.property !== "toLocaleDateString" || args.length !== 2)
9258
+ return null;
9259
+ const [locale, options] = args;
9260
+ if (options.kind !== "object-literal")
9261
+ return null;
9262
+ let tz = null;
9263
+ const probeOptions = {};
9264
+ for (const prop of options.properties) {
9265
+ if (prop.value.kind !== "literal" || prop.value.literalType !== "string")
9266
+ return null;
9267
+ const value = String(prop.value.value);
9268
+ if (prop.key === "timeZone") {
9269
+ if (!TO_LOCALE_TZ_RE.test(value))
9270
+ return null;
9271
+ tz = value;
9272
+ } else {
9273
+ probeOptions[prop.key] = value;
9274
+ }
9275
+ }
9276
+ if (tz === null)
9277
+ return null;
9278
+ const receiverType = resolveReceiverType(callee.object, metadata, new Map);
9279
+ if (!receiverType || receiverType.kind !== "interface")
9280
+ return null;
9281
+ const typeName = baseTypeName(receiverType.raw);
9282
+ if (typeName !== "Date")
9283
+ return null;
9284
+ if (metadata.typeDefinitions.some((d) => d.name === typeName))
9285
+ return null;
9286
+ if (locale.kind === "literal" && locale.literalType === "string") {
9287
+ const format2 = resolveLocaleDateFormat(String(locale.value), probeOptions);
9288
+ if (format2 === null)
9289
+ return null;
9290
+ return {
9291
+ kind: "helper-call",
9292
+ helper: "format_date",
9293
+ args: [callee.object, strLit(format2.pattern), strLit(tz), strArr(format2.names ?? [])]
9294
+ };
9295
+ }
9296
+ const members = resolveLocaleUnionMembers(locale, metadata);
9297
+ if (!members)
9298
+ return null;
9299
+ const formats = [];
9300
+ for (const member of members) {
9301
+ const format2 = resolveLocaleDateFormat(member, probeOptions);
9302
+ if (format2 === null)
9303
+ return null;
9304
+ formats.push(format2);
9305
+ }
9306
+ const patterns = formats.map((f) => f.pattern);
9307
+ const nameTables = formats.map((f) => JSON.stringify(f.names ?? []));
9308
+ return {
9309
+ kind: "helper-call",
9310
+ helper: "format_date",
9311
+ args: [
9312
+ callee.object,
9313
+ foldMembers(locale, members, patterns.map(strLit), new Set(patterns).size === 1),
9314
+ strLit(tz),
9315
+ foldMembers(locale, members, formats.map((f) => strArr(f.names ?? [])), new Set(nameTables).size === 1)
9316
+ ]
9317
+ };
9318
+ }
9319
+ function foldedArgToClientJs(arg, localeText) {
9320
+ if (arg.kind === "literal")
9321
+ return JSON.stringify(arg.value);
9322
+ if (arg.kind === "array-literal") {
9323
+ const values = [];
9324
+ for (const el of arg.elements) {
9325
+ if (el.kind !== "literal")
9326
+ return null;
9327
+ values.push(String(el.value));
9328
+ }
9329
+ return JSON.stringify(values);
9330
+ }
9331
+ if (arg.kind !== "conditional")
9332
+ return null;
9333
+ const t = arg.test;
9334
+ if (t.kind !== "binary" || t.op !== "===" || t.right.kind !== "literal")
9335
+ return null;
9336
+ if (arg.consequent.kind !== "literal" && arg.consequent.kind !== "array-literal")
9337
+ return null;
9338
+ const cons = foldedArgToClientJs(arg.consequent, localeText);
9339
+ const rest = foldedArgToClientJs(arg.alternate, localeText);
9340
+ if (cons === null || rest === null)
9341
+ return null;
9342
+ return `${localeText} === ${JSON.stringify(t.right.value)} ? ${cons} : ${rest}`;
9343
+ }
9344
+ var toLocaleDatePlugin = {
9345
+ name: "toLocaleDateString",
9346
+ prepare(metadata) {
9347
+ if (!metadata.propsType || !typeReachesDate(metadata.propsType, metadata, new Set))
9348
+ return null;
9349
+ return (callee, args) => matchToLocaleDateStringCall(callee, args, metadata);
9350
+ }
9351
+ };
9352
+
8463
9353
  // src/jsx-to-ir.ts
8464
9354
  import { toHTMLAttrName, decodeEntities } from "@barefootjs/shared";
8465
9355
  var CLIENT_DIRECTIVE_INTERIOR_RE2 = /^\s*@client\s*$/;
@@ -8555,6 +9445,19 @@ function getDateLoweringMatcher(ctx) {
8555
9445
  }
8556
9446
  return ctx._dateLoweringMatcher;
8557
9447
  }
9448
+ function getToLocaleDateLoweringMatcher(ctx) {
9449
+ if (ctx._toLocaleDateLoweringMatcher === undefined) {
9450
+ const a = ctx.analyzer;
9451
+ const metadataSlice = {
9452
+ propsType: a.propsType,
9453
+ propsObjectName: a.propsObjectName,
9454
+ propsParams: a.propsParams,
9455
+ typeDefinitions: a.typeDefinitions
9456
+ };
9457
+ ctx._toLocaleDateLoweringMatcher = toLocaleDatePlugin.prepare(metadataSlice);
9458
+ }
9459
+ return ctx._toLocaleDateLoweringMatcher;
9460
+ }
8558
9461
  function lowerDateCalls(text, expr, ctx) {
8559
9462
  const matcher = getDateLoweringMatcher(ctx);
8560
9463
  if (!matcher)
@@ -8583,8 +9486,48 @@ function lowerDateCalls(text, expr, ctx) {
8583
9486
  }
8584
9487
  return restore(result);
8585
9488
  }
9489
+ function lowerToLocaleDateCalls(text, expr, ctx) {
9490
+ const matcher = getToLocaleDateLoweringMatcher(ctx);
9491
+ if (!matcher)
9492
+ return text;
9493
+ const candidates = [];
9494
+ function visit2(n) {
9495
+ if (ts11.isCallExpression(n) && n.arguments.length === 2 && ts11.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && n.expression.name.text === "toLocaleDateString") {
9496
+ candidates.push(n);
9497
+ }
9498
+ ts11.forEachChild(n, visit2);
9499
+ }
9500
+ visit2(expr);
9501
+ if (candidates.length === 0)
9502
+ return text;
9503
+ const { protect, restore, replaceProtectedCall } = createTemplateAwareStringProtector();
9504
+ let result = protect(text);
9505
+ for (const call of candidates) {
9506
+ const propAccess = call.expression;
9507
+ const node = matcher(tsNodeToParsedExpr(propAccess), call.arguments.map((a) => tsNodeToParsedExpr(a)));
9508
+ if (!node || node.kind !== "helper-call" || node.helper !== "format_date")
9509
+ continue;
9510
+ const [, patternArg, tzArg, namesArg] = node.args;
9511
+ if (!patternArg || tzArg?.kind !== "literal")
9512
+ continue;
9513
+ const localeText = ctx.getJS(call.arguments[0]);
9514
+ const patternJs = foldedArgToClientJs(patternArg, localeText);
9515
+ if (patternJs === null)
9516
+ continue;
9517
+ let namesJs = null;
9518
+ if (namesArg && !(namesArg.kind === "array-literal" && namesArg.elements.length === 0)) {
9519
+ namesJs = foldedArgToClientJs(namesArg, localeText);
9520
+ if (namesJs === null)
9521
+ continue;
9522
+ }
9523
+ const receiverText = ctx.getJS(propAccess.expression);
9524
+ const matchText = ctx.getJS(call);
9525
+ result = replaceProtectedCall(result, matchText, () => `formatDate(${receiverText}, ${patternJs}, ${JSON.stringify(tzArg.value)}${namesJs !== null ? `, ${namesJs}` : ""})`);
9526
+ }
9527
+ return restore(result);
9528
+ }
8586
9529
  function rewriteBarePropRefs2(text, expr, ctx) {
8587
- const dateLowered = lowerDateCalls(text, expr, ctx);
9530
+ const dateLowered = lowerToLocaleDateCalls(lowerDateCalls(text, expr, ctx), expr, ctx);
8588
9531
  let propNames = getDestructuredPropNames(ctx);
8589
9532
  if (!propNames)
8590
9533
  return dateLowered === text ? undefined : dateLowered;
@@ -13436,7 +14379,8 @@ var RUNTIME_IMPORT_CANDIDATES = [
13436
14379
  "tAfter",
13437
14380
  "beginTurn",
13438
14381
  "endTurn",
13439
- "date"
14382
+ "date",
14383
+ "formatDate"
13440
14384
  ];
13441
14385
  var RUNTIME_MODULE = "@barefootjs/client/runtime";
13442
14386
  var IMPORT_PLACEHOLDER = "/* __BAREFOOTJS_DOM_IMPORTS__ */";
@@ -14722,7 +15666,7 @@ function importsSearchParams(metadata) {
14722
15666
  function queryHrefLocalNames(metadata) {
14723
15667
  const names = new Set;
14724
15668
  for (const imp of metadata.imports) {
14725
- if (!QUERY_HREF_SOURCES.has(imp.source) || imp.isTypeOnly)
15669
+ if (!CLIENT_HELPER_SOURCES.has(imp.source) || imp.isTypeOnly)
14726
15670
  continue;
14727
15671
  for (const s of imp.specifiers) {
14728
15672
  if (s.isTypeOnly || s.isNamespace || s.isDefault)
@@ -14733,10 +15677,24 @@ function queryHrefLocalNames(metadata) {
14733
15677
  }
14734
15678
  return names;
14735
15679
  }
14736
- var QUERY_HREF_SOURCES = new Set([
15680
+ var CLIENT_HELPER_SOURCES = new Set([
14737
15681
  "@barefootjs/client",
14738
15682
  "@barefootjs/client/runtime"
14739
15683
  ]);
15684
+ function formatDateLocalNames(metadata) {
15685
+ const names = new Set;
15686
+ for (const imp of metadata.imports) {
15687
+ if (!CLIENT_HELPER_SOURCES.has(imp.source) || imp.isTypeOnly)
15688
+ continue;
15689
+ for (const s of imp.specifiers) {
15690
+ if (s.isTypeOnly || s.isNamespace || s.isDefault)
15691
+ continue;
15692
+ if (s.name === "formatDate")
15693
+ names.add(s.alias ?? s.name);
15694
+ }
15695
+ }
15696
+ return names;
15697
+ }
14740
15698
  function matchSearchParamsMethodCall(callee, args, localNames) {
14741
15699
  if (callee.kind !== "member" || callee.computed)
14742
15700
  return null;
@@ -16602,6 +17560,66 @@ function getReactiveDateLoweringMatcher(ctx) {
16602
17560
  };
16603
17561
  return datePlugin.prepare(metadataSlice);
16604
17562
  }
17563
+ function getReactiveToLocaleMatcher(ctx) {
17564
+ if (!ctx.propsType)
17565
+ return null;
17566
+ const metadataSlice = {
17567
+ propsType: ctx.propsType,
17568
+ propsObjectName: ctx.propsObjectName,
17569
+ propsParams: ctx.propsParams,
17570
+ typeDefinitions: ctx.typeDefinitions ?? []
17571
+ };
17572
+ return toLocaleDatePlugin.prepare(metadataSlice);
17573
+ }
17574
+ function lowerToLocaleCallsInReactiveExpr(expr, matcher) {
17575
+ if (!matcher)
17576
+ return expr;
17577
+ let sourceFile;
17578
+ try {
17579
+ sourceFile = ts14.createSourceFile("__reactive_expr__.ts", `(${expr});`, ts14.ScriptTarget.Latest, true, ts14.ScriptKind.TS);
17580
+ } catch {
17581
+ return expr;
17582
+ }
17583
+ const stmt = sourceFile.statements[0];
17584
+ if (!stmt || !ts14.isExpressionStatement(stmt))
17585
+ return expr;
17586
+ const root = ts14.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
17587
+ const candidates = [];
17588
+ const visit3 = (n) => {
17589
+ if (ts14.isCallExpression(n) && n.arguments.length === 2 && ts14.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && n.expression.name.text === "toLocaleDateString") {
17590
+ candidates.push(n);
17591
+ }
17592
+ ts14.forEachChild(n, visit3);
17593
+ };
17594
+ visit3(root);
17595
+ if (candidates.length === 0)
17596
+ return expr;
17597
+ const { protect, restore, replaceProtectedCall } = createTemplateAwareStringProtector();
17598
+ let result = protect(expr);
17599
+ for (const call of candidates) {
17600
+ const propAccess = call.expression;
17601
+ const node = matcher(tsNodeToParsedExpr(propAccess), call.arguments.map((a) => tsNodeToParsedExpr(a)));
17602
+ if (!node || node.kind !== "helper-call" || node.helper !== "format_date")
17603
+ continue;
17604
+ const [, patternArg, tzArg, namesArg] = node.args;
17605
+ if (!patternArg || tzArg?.kind !== "literal")
17606
+ continue;
17607
+ const localeText = call.arguments[0].getText(sourceFile);
17608
+ const patternJs = foldedArgToClientJs(patternArg, localeText);
17609
+ if (patternJs === null)
17610
+ continue;
17611
+ let namesJs = null;
17612
+ if (namesArg && !(namesArg.kind === "array-literal" && namesArg.elements.length === 0)) {
17613
+ namesJs = foldedArgToClientJs(namesArg, localeText);
17614
+ if (namesJs === null)
17615
+ continue;
17616
+ }
17617
+ const receiverText = propAccess.expression.getText(sourceFile);
17618
+ const matchText = call.getText(sourceFile);
17619
+ result = replaceProtectedCall(result, matchText, () => `formatDate(${receiverText}, ${patternJs}, ${JSON.stringify(tzArg.value)}${namesJs !== null ? `, ${namesJs}` : ""})`);
17620
+ }
17621
+ return restore(result);
17622
+ }
16605
17623
  function lowerDateCallsInReactiveExpr(expr, matcher) {
16606
17624
  if (!matcher)
16607
17625
  return expr;
@@ -16641,6 +17659,7 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
16641
17659
  }
16642
17660
  function emitDynamicTextUpdates(lines, ctx) {
16643
17661
  const dateLoweringMatcher = getReactiveDateLoweringMatcher(ctx);
17662
+ const toLocaleMatcher = getReactiveToLocaleMatcher(ctx);
16644
17663
  const byExpression = new Map;
16645
17664
  for (const elem of ctx.dynamicElements) {
16646
17665
  const key = elem.expression;
@@ -16650,7 +17669,7 @@ function emitDynamicTextUpdates(lines, ctx) {
16650
17669
  byExpression.get(key).push(elem);
16651
17670
  }
16652
17671
  for (const [rawExpr, elems] of byExpression) {
16653
- const expr = lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher);
17672
+ const expr = lowerToLocaleCallsInReactiveExpr(lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher), toLocaleMatcher);
16654
17673
  const conditionalElems = elems.filter((e) => e.insideConditional);
16655
17674
  const normalElems = elems.filter((e) => !e.insideConditional);
16656
17675
  if (normalElems.length > 0 || conditionalElems.length > 0) {
@@ -19875,13 +20894,14 @@ function pushDiagnostic(errors, seen, loc, method, receiverPath, isProp, typeNam
19875
20894
  return;
19876
20895
  seen.add(key);
19877
20896
  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.";
19878
20898
  errors.push({
19879
20899
  code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
19880
20900
  severity: "error",
19881
20901
  message: `Expression cannot be compiled to marked template: method '.${method}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
19882
20902
  loc,
19883
20903
  suggestion: {
19884
- message: "Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side."
20904
+ message: suggestion
19885
20905
  }
19886
20906
  });
19887
20907
  }
@@ -21397,6 +22417,30 @@ function isOmitBranch(node) {
21397
22417
  }
21398
22418
  return false;
21399
22419
  }
22420
+ // src/format-date-lowering.ts
22421
+ var UTC_LITERAL = { kind: "literal", value: "UTC", literalType: "string" };
22422
+ var EMPTY_NAMES = { kind: "array-literal", elements: [], raw: "[]" };
22423
+ function matchFormatDateCall(callee, args, locals) {
22424
+ if (callee.kind !== "identifier" || !locals.has(callee.name))
22425
+ return null;
22426
+ if (args.length < 2 || args.length > 4)
22427
+ return null;
22428
+ return {
22429
+ kind: "helper-call",
22430
+ helper: "format_date",
22431
+ args: [args[0], args[1], args[2] ?? UTC_LITERAL, args[3] ?? EMPTY_NAMES]
22432
+ };
22433
+ }
22434
+ var formatDatePlugin = {
22435
+ name: "formatDate",
22436
+ prepare(metadata) {
22437
+ const locals = formatDateLocalNames(metadata);
22438
+ if (locals.size === 0)
22439
+ return null;
22440
+ return (callee, args) => matchFormatDateCall(callee, args, locals);
22441
+ }
22442
+ };
22443
+
21400
22444
  // src/builtin-lowering-plugins.ts
21401
22445
  var queryHrefPlugin = {
21402
22446
  name: "queryHref",
@@ -21410,7 +22454,12 @@ var queryHrefPlugin = {
21410
22454
  };
21411
22455
  }
21412
22456
  };
21413
- var BUILTIN_LOWERING_PLUGINS = [queryHrefPlugin, datePlugin];
22457
+ var BUILTIN_LOWERING_PLUGINS = [
22458
+ queryHrefPlugin,
22459
+ datePlugin,
22460
+ formatDatePlugin,
22461
+ toLocaleDatePlugin
22462
+ ];
21414
22463
  function registerBuiltinLoweringPlugins() {
21415
22464
  for (const plugin of BUILTIN_LOWERING_PLUGINS)
21416
22465
  registerLoweringPlugin(plugin);
@@ -24453,6 +25502,7 @@ export {
24453
25502
  formatFallbackExplanations,
24454
25503
  formatEventSummary,
24455
25504
  formatError,
25505
+ formatDateLocalNames,
24456
25506
  formatComponentSummary,
24457
25507
  formatComponentGraph,
24458
25508
  formatBudgetDiff,