@barefootjs/jsx 0.21.4 → 0.23.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/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 +2 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/format-date-lowering.d.ts +27 -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 +706 -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 +72 -0
  24. package/dist/to-locale-date-lowering.d.ts.map +1 -0
  25. package/dist/types.d.ts +23 -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 +109 -0
  29. package/src/__tests__/reactive-factory-cross-file.test.ts +239 -0
  30. package/src/__tests__/reactive-factory-inlining.test.ts +293 -4
  31. package/src/__tests__/to-locale-date-lowering.test.ts +181 -0
  32. package/src/adapters/env-signal.ts +26 -3
  33. package/src/analyzer-context.ts +19 -4
  34. package/src/analyzer.ts +712 -91
  35. package/src/builtin-lowering-plugins.ts +8 -1
  36. package/src/date-lowering.ts +1 -1
  37. package/src/errors.ts +13 -0
  38. package/src/format-date-lowering.ts +51 -0
  39. package/src/index.ts +1 -1
  40. package/src/ir-to-client-js/emit-reactive.ts +80 -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 +79 -1
  44. package/src/rich-type-refusal.ts +9 -1
  45. package/src/to-locale-date-lowering.ts +175 -0
  46. package/src/types.ts +24 -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,9 @@ 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"
5221
5247
  };
5222
5248
  var errorMessages = {
5223
5249
  [ErrorCodes.MISSING_USE_CLIENT]: "'use client' directive required for components with createSignal or event handlers",
@@ -5241,7 +5267,9 @@ var errorMessages = {
5241
5267
  [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
5268
  [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
5269
  [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."
5270
+ [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.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.",
5272
+ [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."
5245
5273
  };
5246
5274
  function createError(code, loc, options) {
5247
5275
  if (code === undefined || !(code in errorMessages)) {
@@ -5567,6 +5595,10 @@ function analyzeComponent(source, filePath, targetComponentName, program) {
5567
5595
  }
5568
5596
  const ctx = createAnalyzerContext(sourceFile, filePath);
5569
5597
  ctx.checker = checker;
5598
+ ctx.reactiveFactories = prescan.factories;
5599
+ ctx.declinedReactiveFactories = prescan.declined;
5600
+ ctx.reactiveShapedHelpers = prescan.reactiveShaped;
5601
+ ctx.cleanFactoryImports = prescan.cleanFactoryImports;
5570
5602
  const brandImportLoc = findBrandPackageImportLoc(sourceFile, filePath);
5571
5603
  if (!hadSharedProgram && brandImportLoc !== null) {
5572
5604
  ctx.errors.push(createError(ErrorCodes.SHARED_PROGRAM_REQUIRED, brandImportLoc));
@@ -6472,6 +6504,7 @@ var CLIENT_EXPORTS = new Set([
6472
6504
  "cleanupPortalPlaceholder",
6473
6505
  "createSearchParams",
6474
6506
  "queryHref",
6507
+ "formatDate",
6475
6508
  "Async",
6476
6509
  "Region"
6477
6510
  ]);
@@ -7721,27 +7754,241 @@ var REACTIVE_PRIMITIVES = new Set([
7721
7754
  function prescanReactiveFactoriesInSource(source, filePath) {
7722
7755
  const sourceFile = ts8.createSourceFile(filePath + ".prescan", source, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7723
7756
  const factories = new Map;
7757
+ const declined = new Map;
7758
+ const reactiveShaped = new Set;
7759
+ const cleanFactoryImports = new Set;
7724
7760
  function visitTop(node) {
7725
7761
  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);
7762
+ const det = detectReactiveFactory(node, sourceFile, filePath);
7763
+ if (!det)
7764
+ return;
7765
+ switch (det.kind) {
7766
+ case "factory":
7767
+ factories.set(node.name.text, det.info);
7768
+ break;
7769
+ case "declined":
7770
+ declined.set(node.name.text, det.declined);
7771
+ break;
7772
+ case "reactive-shaped":
7773
+ reactiveShaped.add(node.name.text);
7774
+ break;
7775
+ }
7729
7776
  }
7730
7777
  }
7731
7778
  ts8.forEachChild(sourceFile, visitTop);
7732
- return { factories, sourceFile };
7779
+ const result = { factories, declined, reactiveShaped, cleanFactoryImports, sourceFile };
7780
+ prescanImportedReactiveFactories(sourceFile, filePath, result);
7781
+ return result;
7782
+ }
7783
+ function prescanImportedReactiveFactories(entrySourceFile, filePath, result) {
7784
+ const candidateCallees = new Set;
7785
+ function collectCandidates(node) {
7786
+ if (ts8.isVariableDeclaration(node) && (ts8.isArrayBindingPattern(node.name) || ts8.isObjectBindingPattern(node.name)) && node.initializer && ts8.isCallExpression(node.initializer) && ts8.isIdentifier(node.initializer.expression)) {
7787
+ candidateCallees.add(node.initializer.expression.text);
7788
+ }
7789
+ ts8.forEachChild(node, collectCandidates);
7790
+ }
7791
+ collectCandidates(entrySourceFile);
7792
+ if (candidateCallees.size === 0)
7793
+ return;
7794
+ const importsToCheck = [];
7795
+ for (const stmt of entrySourceFile.statements) {
7796
+ if (!ts8.isImportDeclaration(stmt))
7797
+ continue;
7798
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7799
+ continue;
7800
+ const src = stmt.moduleSpecifier.text;
7801
+ if (!src.startsWith("./") && !src.startsWith("../"))
7802
+ continue;
7803
+ if (stmt.importClause?.isTypeOnly)
7804
+ continue;
7805
+ const namedBindings = stmt.importClause?.namedBindings;
7806
+ if (!namedBindings || !ts8.isNamedImports(namedBindings))
7807
+ continue;
7808
+ const specs = [];
7809
+ for (const el of namedBindings.elements) {
7810
+ if (el.isTypeOnly)
7811
+ continue;
7812
+ const local = el.name.text;
7813
+ if (!candidateCallees.has(local))
7814
+ continue;
7815
+ specs.push({ exported: (el.propertyName ?? el.name).text, local });
7816
+ }
7817
+ if (specs.length === 0)
7818
+ continue;
7819
+ importsToCheck.push({ src, specs });
7820
+ }
7821
+ if (importsToCheck.length === 0)
7822
+ return;
7823
+ for (const { src, specs } of importsToCheck) {
7824
+ const resolved = resolveRelativeImportToFile(src, filePath);
7825
+ if (!resolved)
7826
+ continue;
7827
+ let content;
7828
+ try {
7829
+ content = fs.readFileSync(resolved, "utf8");
7830
+ } catch {
7831
+ continue;
7832
+ }
7833
+ const alreadyKnown = (name) => result.factories.has(name) || result.declined.has(name) || result.reactiveShaped.has(name);
7834
+ const hasAnyPrimitiveText = [...REACTIVE_PRIMITIVES].some((p) => content.includes(p));
7835
+ if (!hasAnyPrimitiveText) {
7836
+ for (const spec of specs) {
7837
+ if (!alreadyKnown(spec.local))
7838
+ result.cleanFactoryImports.add(spec.local);
7839
+ }
7840
+ continue;
7841
+ }
7842
+ const helperSf = ts8.createSourceFile(resolved + ".prescan", content, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TSX);
7843
+ const localFns = new Map;
7844
+ const exportedFns = new Map;
7845
+ for (const stmt of helperSf.statements) {
7846
+ if (ts8.isFunctionDeclaration(stmt) && stmt.name && stmt.body) {
7847
+ localFns.set(stmt.name.text, stmt);
7848
+ const hasExportModifier = stmt.modifiers?.some((m) => m.kind === ts8.SyntaxKind.ExportKeyword) ?? false;
7849
+ const hasDefaultModifier = stmt.modifiers?.some((m) => m.kind === ts8.SyntaxKind.DefaultKeyword) ?? false;
7850
+ if (hasExportModifier && !hasDefaultModifier) {
7851
+ exportedFns.set(stmt.name.text, stmt);
7852
+ }
7853
+ }
7854
+ }
7855
+ for (const stmt of helperSf.statements) {
7856
+ if (ts8.isExportDeclaration(stmt) && stmt.exportClause && ts8.isNamedExports(stmt.exportClause) && !stmt.moduleSpecifier && !stmt.isTypeOnly) {
7857
+ for (const el of stmt.exportClause.elements) {
7858
+ if (el.isTypeOnly)
7859
+ continue;
7860
+ const fn = localFns.get((el.propertyName ?? el.name).text);
7861
+ if (fn)
7862
+ exportedFns.set(el.name.text, fn);
7863
+ }
7864
+ }
7865
+ }
7866
+ const moduleBindings = collectHelperModuleValueBindings(helperSf);
7867
+ for (const spec of specs) {
7868
+ if (alreadyKnown(spec.local))
7869
+ continue;
7870
+ const fn = exportedFns.get(spec.exported);
7871
+ if (!fn) {
7872
+ result.cleanFactoryImports.add(spec.local);
7873
+ continue;
7874
+ }
7875
+ const det = detectReactiveFactory(fn, helperSf, resolved);
7876
+ if (!det) {
7877
+ result.cleanFactoryImports.add(spec.local);
7878
+ continue;
7879
+ }
7880
+ switch (det.kind) {
7881
+ case "reactive-shaped":
7882
+ result.reactiveShaped.add(spec.local);
7883
+ break;
7884
+ case "declined":
7885
+ result.declined.set(spec.local, det.declined);
7886
+ break;
7887
+ case "factory": {
7888
+ const offending = moduleCaptureCheck(fn, det.info, moduleBindings, fn.name.text);
7889
+ if (offending.length > 0) {
7890
+ result.declined.set(spec.local, {
7891
+ code: "BF112",
7892
+ detail: `'${offending.join("', '")}'`,
7893
+ loc: det.info.loc
7894
+ });
7895
+ } else {
7896
+ det.info.sourceFilePath = resolved;
7897
+ result.factories.set(spec.local, det.info);
7898
+ }
7899
+ break;
7900
+ }
7901
+ }
7902
+ }
7903
+ }
7904
+ }
7905
+ function collectHelperModuleValueBindings(sf) {
7906
+ const names = new Set;
7907
+ for (const stmt of sf.statements) {
7908
+ if (ts8.isVariableStatement(stmt)) {
7909
+ const out = [];
7910
+ for (const decl of stmt.declarationList.declarations) {
7911
+ addBindingNames(decl.name, out);
7912
+ }
7913
+ for (const n of out)
7914
+ names.add(n);
7915
+ continue;
7916
+ }
7917
+ if ((ts8.isFunctionDeclaration(stmt) || ts8.isClassDeclaration(stmt) || ts8.isEnumDeclaration(stmt)) && stmt.name) {
7918
+ names.add(stmt.name.text);
7919
+ continue;
7920
+ }
7921
+ if (ts8.isImportDeclaration(stmt)) {
7922
+ if (stmt.importClause?.isTypeOnly)
7923
+ continue;
7924
+ if (!ts8.isStringLiteral(stmt.moduleSpecifier))
7925
+ continue;
7926
+ const src = stmt.moduleSpecifier.text;
7927
+ if (src === "@barefootjs/client" || src === "@barefootjs/client/runtime")
7928
+ continue;
7929
+ if (stmt.importClause?.name)
7930
+ names.add(stmt.importClause.name.text);
7931
+ const namedBindings = stmt.importClause?.namedBindings;
7932
+ if (namedBindings && ts8.isNamedImports(namedBindings)) {
7933
+ for (const el of namedBindings.elements) {
7934
+ if (el.isTypeOnly)
7935
+ continue;
7936
+ names.add(el.name.text);
7937
+ }
7938
+ }
7939
+ if (namedBindings && ts8.isNamespaceImport(namedBindings)) {
7940
+ names.add(namedBindings.name.text);
7941
+ }
7942
+ }
7943
+ }
7944
+ return names;
7945
+ }
7946
+ function moduleCaptureCheck(fn, info, moduleBindings, selfName) {
7947
+ if (!fn.body)
7948
+ return [];
7949
+ const free = extractFreeIdentifiersFromNode(fn.body);
7950
+ const exclude = new Set(info.params);
7951
+ for (const b of info.localBindings)
7952
+ exclude.add(b);
7953
+ for (const r of info.returnTupleIdentifiers)
7954
+ exclude.add(r);
7955
+ for (const p of REACTIVE_PRIMITIVES)
7956
+ exclude.add(p);
7957
+ exclude.add(selfName);
7958
+ const offending = [];
7959
+ for (const id of free) {
7960
+ if (exclude.has(id))
7961
+ continue;
7962
+ if (moduleBindings.has(id))
7963
+ offending.push(id);
7964
+ }
7965
+ return offending.sort();
7733
7966
  }
7734
7967
  function detectReactiveFactory(node, sourceFile, filePath) {
7735
7968
  if (!node.body || !node.name)
7736
7969
  return null;
7737
- let tupleReturn = null;
7970
+ let hasReactiveCall = false;
7971
+ function checkForReactive(n) {
7972
+ if (hasReactiveCall)
7973
+ return;
7974
+ if (ts8.isCallExpression(n) && ts8.isIdentifier(n.expression) && REACTIVE_PRIMITIVES.has(n.expression.text)) {
7975
+ hasReactiveCall = true;
7976
+ return;
7977
+ }
7978
+ ts8.forEachChild(n, checkForReactive);
7979
+ }
7980
+ checkForReactive(node.body);
7981
+ if (!hasReactiveCall)
7982
+ return null;
7983
+ const loc = getSourceLocation(node, sourceFile, filePath);
7984
+ let returnExpr = null;
7738
7985
  let returnCount = 0;
7739
7986
  for (const stmt of node.body.statements) {
7740
7987
  if (!ts8.isReturnStatement(stmt))
7741
7988
  continue;
7742
7989
  returnCount++;
7743
7990
  if (!stmt.expression)
7744
- return null;
7991
+ return { kind: "reactive-shaped" };
7745
7992
  let expr = stmt.expression;
7746
7993
  while (ts8.isParenthesizedExpression(expr))
7747
7994
  expr = expr.expression;
@@ -7749,33 +7996,42 @@ function detectReactiveFactory(node, sourceFile, filePath) {
7749
7996
  expr = expr.expression;
7750
7997
  if (ts8.isTypeAssertionExpression(expr))
7751
7998
  expr = expr.expression;
7752
- if (!ts8.isArrayLiteralExpression(expr))
7753
- return null;
7754
- tupleReturn = expr;
7999
+ returnExpr = expr;
7755
8000
  }
7756
- if (returnCount !== 1 || !tupleReturn)
7757
- return null;
8001
+ if (returnCount !== 1 || !returnExpr)
8002
+ return { kind: "reactive-shaped" };
7758
8003
  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;
8004
+ let returnKind;
8005
+ if (ts8.isArrayLiteralExpression(returnExpr)) {
8006
+ returnKind = "tuple";
8007
+ for (const el of returnExpr.elements) {
8008
+ if (!ts8.isIdentifier(el))
8009
+ return { kind: "reactive-shaped" };
8010
+ returnTupleIdentifiers.push(el.text);
8011
+ }
8012
+ if (returnTupleIdentifiers.length === 0)
8013
+ return { kind: "reactive-shaped" };
8014
+ } else if (ts8.isObjectLiteralExpression(returnExpr)) {
8015
+ returnKind = "object";
8016
+ const hasNonShorthand = returnExpr.properties.some((p) => !ts8.isShorthandPropertyAssignment(p));
8017
+ if (hasNonShorthand) {
8018
+ return {
8019
+ kind: "declined",
8020
+ declined: {
8021
+ code: "BF111",
8022
+ detail: `return object of '${node.name.text}' uses non-shorthand properties`,
8023
+ loc
8024
+ }
8025
+ };
7773
8026
  }
7774
- ts8.forEachChild(n, checkForReactive);
8027
+ for (const p of returnExpr.properties) {
8028
+ returnTupleIdentifiers.push(p.name.text);
8029
+ }
8030
+ if (returnTupleIdentifiers.length === 0)
8031
+ return { kind: "reactive-shaped" };
8032
+ } else {
8033
+ return { kind: "reactive-shaped" };
7775
8034
  }
7776
- checkForReactive(node.body);
7777
- if (!hasReactiveCall)
7778
- return null;
7779
8035
  const localBindings = [];
7780
8036
  for (const stmt of node.body.statements) {
7781
8037
  if (ts8.isVariableStatement(stmt)) {
@@ -7788,19 +8044,24 @@ function detectReactiveFactory(node, sourceFile, filePath) {
7788
8044
  }
7789
8045
  const bodyStatements = node.body.statements.filter((s) => !ts8.isReturnStatement(s)).map((s) => s.getText(sourceFile)).join(`
7790
8046
  `);
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;
8047
+ const params = [];
8048
+ for (const p of node.parameters) {
8049
+ if (ts8.isIdentifier(p.name)) {
8050
+ params.push(p.name.text);
8051
+ continue;
8052
+ }
8053
+ return { kind: "reactive-shaped" };
8054
+ }
7798
8055
  return {
7799
- params,
7800
- bodySource: bodyStatements,
7801
- returnTupleIdentifiers,
7802
- localBindings,
7803
- loc: getSourceLocation(node, sourceFile, filePath)
8056
+ kind: "factory",
8057
+ info: {
8058
+ params,
8059
+ bodySource: bodyStatements,
8060
+ returnTupleIdentifiers,
8061
+ returnKind,
8062
+ localBindings,
8063
+ loc
8064
+ }
7804
8065
  };
7805
8066
  }
7806
8067
  function addBindingNames(name, out) {
@@ -7838,8 +8099,6 @@ function rewriteFactoryCallsInSource(source, prescan) {
7838
8099
  });
7839
8100
  }
7840
8101
  function maybeRewriteDecl(stmt, decl) {
7841
- if (!ts8.isArrayBindingPattern(decl.name))
7842
- return;
7843
8102
  if (!decl.initializer || !ts8.isCallExpression(decl.initializer))
7844
8103
  return;
7845
8104
  if (!ts8.isIdentifier(decl.initializer.expression))
@@ -7848,7 +8107,21 @@ function rewriteFactoryCallsInSource(source, prescan) {
7848
8107
  const factory = factories.get(factoryName);
7849
8108
  if (!factory)
7850
8109
  return;
7851
- const elements = decl.name.elements;
8110
+ if (ts8.isArrayBindingPattern(decl.name)) {
8111
+ if (factory.returnKind !== "tuple")
8112
+ return;
8113
+ rewriteTupleDecl(stmt, decl.name, decl.initializer, factory);
8114
+ return;
8115
+ }
8116
+ if (ts8.isObjectBindingPattern(decl.name)) {
8117
+ if (factory.returnKind !== "object")
8118
+ return;
8119
+ rewriteObjectDecl(stmt, decl.name, decl.initializer, factory);
8120
+ return;
8121
+ }
8122
+ }
8123
+ function rewriteTupleDecl(stmt, pattern, call, factory) {
8124
+ const elements = pattern.elements;
7852
8125
  if (elements.length !== factory.returnTupleIdentifiers.length)
7853
8126
  return;
7854
8127
  const callerNames = [];
@@ -7857,15 +8130,43 @@ function rewriteFactoryCallsInSource(source, prescan) {
7857
8130
  return;
7858
8131
  callerNames.push(el.name.text);
7859
8132
  }
7860
- const argTexts = decl.initializer.arguments.map((a) => a.getText(sourceFile));
8133
+ const excludeFromSuffixRename = new Set(factory.params);
8134
+ for (const r of factory.returnTupleIdentifiers)
8135
+ excludeFromSuffixRename.add(r);
8136
+ const renameReturnToCallerNames = new Map;
8137
+ for (let i = 0;i < factory.returnTupleIdentifiers.length; i++) {
8138
+ renameReturnToCallerNames.set(factory.returnTupleIdentifiers[i], callerNames[i]);
8139
+ }
8140
+ inlineFactoryCallAtSite(stmt, factory, call.arguments, excludeFromSuffixRename, renameReturnToCallerNames);
8141
+ }
8142
+ function rewriteObjectDecl(stmt, pattern, call, factory) {
8143
+ const destructured = new Set;
8144
+ for (const el of pattern.elements) {
8145
+ if (el.dotDotDotToken)
8146
+ return;
8147
+ if (el.propertyName)
8148
+ return;
8149
+ if (el.initializer)
8150
+ return;
8151
+ if (!ts8.isIdentifier(el.name))
8152
+ return;
8153
+ if (!factory.returnTupleIdentifiers.includes(el.name.text))
8154
+ return;
8155
+ destructured.add(el.name.text);
8156
+ }
8157
+ const excludeFromSuffixRename = new Set(factory.params);
8158
+ for (const d of destructured)
8159
+ excludeFromSuffixRename.add(d);
8160
+ inlineFactoryCallAtSite(stmt, factory, call.arguments, excludeFromSuffixRename, null);
8161
+ }
8162
+ function inlineFactoryCallAtSite(stmt, factory, args, excludeFromSuffixRename, renameReturnToCallerNames) {
8163
+ const argTexts = args.map((a) => a.getText(sourceFile));
7861
8164
  const thisCallIndex = callSiteIndex++;
7862
8165
  const suffix = `_bf${thisCallIndex}`;
7863
8166
  let body = factory.bodySource;
7864
8167
  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);
8168
+ for (const ex of excludeFromSuffixRename)
8169
+ internalRenames.delete(ex);
7869
8170
  for (const name of internalRenames) {
7870
8171
  body = body.replace(new RegExp(`\\b${escapeRegex(name)}\\b`, "g"), name + suffix);
7871
8172
  }
@@ -7876,10 +8177,10 @@ function rewriteFactoryCallsInSource(source, prescan) {
7876
8177
  const wrapped = atomicArg.test(a.trim()) ? a.trim() : `(${a})`;
7877
8178
  body = body.replace(new RegExp(`\\b${escapeRegex(p)}\\b`, "g"), wrapped);
7878
8179
  }
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);
8180
+ if (renameReturnToCallerNames) {
8181
+ for (const [n, caller] of renameReturnToCallerNames) {
8182
+ body = body.replace(new RegExp(`\\b${escapeRegex(n)}\\b`, "g"), caller);
8183
+ }
7883
8184
  }
7884
8185
  edits.push({
7885
8186
  start: stmt.getStart(sourceFile),
@@ -7909,6 +8210,12 @@ function isPascalCaseComponentFn(node) {
7909
8210
  function escapeRegex(s) {
7910
8211
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7911
8212
  }
8213
+ function declinedFactoryMessage(callee, d) {
8214
+ if (d.code === "BF112") {
8215
+ 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.`;
8216
+ }
8217
+ return `Reactive factory '${callee}' cannot be inlined: ${d.detail}.`;
8218
+ }
7912
8219
  function validateReactiveFactoryCalls(ctx) {
7913
8220
  if (!ctx.componentNode)
7914
8221
  return;
@@ -7919,26 +8226,104 @@ function validateReactiveFactoryCalls(ctx) {
7919
8226
  if (!ts8.isVariableStatement(stmt))
7920
8227
  continue;
7921
8228
  for (const decl of stmt.declarationList.declarations) {
7922
- if (!ts8.isArrayBindingPattern(decl.name))
7923
- continue;
7924
8229
  if (!decl.initializer || !ts8.isCallExpression(decl.initializer))
7925
8230
  continue;
7926
8231
  if (!ts8.isIdentifier(decl.initializer.expression))
7927
8232
  continue;
7928
8233
  const callee = decl.initializer.expression.text;
7929
- if (callee === "createSignal" || callee === "createMemo")
7930
- continue;
7931
- if (resolveEnvSignalKey(decl.initializer, ctx))
8234
+ const loc = getSourceLocation(stmt, ctx.sourceFile, ctx.filePath);
8235
+ if (ts8.isArrayBindingPattern(decl.name)) {
8236
+ if (callee === "createSignal" || callee === "createMemo")
8237
+ continue;
8238
+ if (resolveEnvSignalKey(decl.initializer, ctx))
8239
+ continue;
8240
+ const declinedEntry = ctx.declinedReactiveFactories.get(callee);
8241
+ if (declinedEntry) {
8242
+ ctx.errors.push(createError(declinedEntry.code === "BF112" ? ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE : ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED, loc, { severity: "error", message: declinedFactoryMessage(callee, declinedEntry) }));
8243
+ continue;
8244
+ }
8245
+ const objectFactory = ctx.reactiveFactories.get(callee);
8246
+ if (objectFactory && objectFactory.returnKind === "object") {
8247
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8248
+ severity: "error",
8249
+ message: `'${callee}' is a reactive factory that returns an object — destructure ` + `it with a matching object pattern: const { ${objectFactory.returnTupleIdentifiers.join(", ")} } = ${callee}(...)`
8250
+ }));
8251
+ continue;
8252
+ }
8253
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8254
+ severity: "error",
8255
+ 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, ...]\`).`,
8256
+ suggestion: {
8257
+ 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.`
8258
+ }
8259
+ }));
7932
8260
  continue;
7933
- ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, getSourceLocation(stmt, ctx.sourceFile, ctx.filePath), {
8261
+ }
8262
+ if (ts8.isObjectBindingPattern(decl.name)) {
8263
+ validateObjectFactoryDestructure(ctx, decl.name, callee, loc);
8264
+ }
8265
+ }
8266
+ }
8267
+ }
8268
+ function validateObjectFactoryDestructure(ctx, pattern, callee, loc) {
8269
+ const factory = ctx.reactiveFactories.get(callee);
8270
+ if (factory) {
8271
+ if (factory.returnKind === "tuple") {
8272
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
7934
8273
  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
- }
8274
+ message: `'${callee}' is a reactive factory that returns a tuple destructure ` + `it positionally: const [${factory.returnTupleIdentifiers.join(", ")}] = ${callee}(...)`
8275
+ }));
8276
+ return;
8277
+ }
8278
+ const hasUnsupportedElement = pattern.elements.some((el) => !!el.propertyName || !!el.initializer || !!el.dotDotDotToken || !ts8.isIdentifier(el.name));
8279
+ if (hasUnsupportedElement) {
8280
+ ctx.errors.push(createError(ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED, loc, {
8281
+ severity: "error",
8282
+ message: `Object destructure of reactive factory '${callee}' uses a property ` + `rename, default, or rest element; only shorthand destructuring of ` + `{ ${factory.returnTupleIdentifiers.join(", ")} } is supported.`
8283
+ }));
8284
+ return;
8285
+ }
8286
+ const unknown = pattern.elements.map((el) => ts8.isIdentifier(el.name) ? el.name.text : "").filter((name) => name && !factory.returnTupleIdentifiers.includes(name));
8287
+ if (unknown.length > 0) {
8288
+ const label = unknown.length === 1 ? "property" : "properties";
8289
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8290
+ severity: "error",
8291
+ message: `Object destructure of reactive factory '${callee}' references ${label} ` + `'${unknown.join("', '")}' not present in its return { ${factory.returnTupleIdentifiers.join(", ")} }.`
7939
8292
  }));
8293
+ return;
8294
+ }
8295
+ return;
8296
+ }
8297
+ const declinedEntry = ctx.declinedReactiveFactories.get(callee);
8298
+ if (declinedEntry) {
8299
+ ctx.errors.push(createError(declinedEntry.code === "BF112" ? ErrorCodes.REACTIVE_FACTORY_MODULE_CAPTURE : ErrorCodes.REACTIVE_FACTORY_RENAME_UNSUPPORTED, loc, { severity: "error", message: declinedFactoryMessage(callee, declinedEntry) }));
8300
+ return;
8301
+ }
8302
+ if (ctx.reactiveShapedHelpers.has(callee)) {
8303
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8304
+ severity: "error",
8305
+ 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)."
8306
+ }));
8307
+ return;
8308
+ }
8309
+ if (ctx.cleanFactoryImports.has(callee))
8310
+ return;
8311
+ let matchedImportSource = null;
8312
+ for (const imp of ctx.imports) {
8313
+ if (imp.isTypeOnly)
8314
+ continue;
8315
+ const spec = imp.specifiers.find((s) => !s.isTypeOnly && (s.alias ?? s.name) === callee);
8316
+ if (spec) {
8317
+ matchedImportSource = imp.source;
8318
+ break;
7940
8319
  }
7941
8320
  }
8321
+ if (matchedImportSource !== null && !matchedImportSource.startsWith("@barefootjs/") && /^(use|create)[A-Z]/.test(callee)) {
8322
+ ctx.errors.push(createError(ErrorCodes.UNRECOGNIZED_REACTIVE_FACTORY, loc, {
8323
+ severity: "error",
8324
+ 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.`
8325
+ }));
8326
+ }
7942
8327
  }
7943
8328
 
7944
8329
  // src/jsx-to-ir.ts
@@ -8460,6 +8845,114 @@ function resolveFreeRefs(node, env) {
8460
8845
  return resolveFreeRefsInternal(node, env, new Set);
8461
8846
  }
8462
8847
 
8848
+ // src/to-locale-date-lowering.ts
8849
+ var TO_LOCALE_TZ_RE = /^(?:UTC|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
8850
+ var PROBE_UTC = new Date(Date.UTC(2001, 1, 3));
8851
+ var patternCache = new Map;
8852
+ function resolveLocaleDatePattern(locale) {
8853
+ const cached = patternCache.get(locale);
8854
+ if (cached !== undefined)
8855
+ return cached;
8856
+ const derived = derivePattern(locale);
8857
+ patternCache.set(locale, derived);
8858
+ return derived;
8859
+ }
8860
+ function derivePattern(locale) {
8861
+ let parts;
8862
+ try {
8863
+ const dtf = new Intl.DateTimeFormat(locale, { timeZone: "UTC" });
8864
+ const resolved = dtf.resolvedOptions();
8865
+ if (resolved.calendar !== "gregory" || resolved.numberingSystem !== "latn")
8866
+ return null;
8867
+ parts = dtf.formatToParts(PROBE_UTC);
8868
+ } catch {
8869
+ return null;
8870
+ }
8871
+ let pattern = "";
8872
+ for (const part of parts) {
8873
+ switch (part.type) {
8874
+ case "year":
8875
+ if (part.value !== "2001")
8876
+ return null;
8877
+ pattern += "YYYY";
8878
+ break;
8879
+ case "month":
8880
+ if (part.value === "2")
8881
+ pattern += "M";
8882
+ else if (part.value === "02")
8883
+ pattern += "MM";
8884
+ else
8885
+ return null;
8886
+ break;
8887
+ case "day":
8888
+ if (part.value === "3")
8889
+ pattern += "D";
8890
+ else if (part.value === "03")
8891
+ pattern += "DD";
8892
+ else
8893
+ return null;
8894
+ break;
8895
+ case "literal":
8896
+ if (/[YMD]/.test(part.value))
8897
+ return null;
8898
+ pattern += part.value;
8899
+ break;
8900
+ default:
8901
+ return null;
8902
+ }
8903
+ }
8904
+ if (!pattern.includes("YYYY") || !/M/.test(pattern) || !/D/.test(pattern))
8905
+ return null;
8906
+ return pattern;
8907
+ }
8908
+ function matchToLocaleDateStringCall(callee, args, metadata) {
8909
+ if (callee.kind !== "member" || callee.computed)
8910
+ return null;
8911
+ if (callee.property !== "toLocaleDateString" || args.length !== 2)
8912
+ return null;
8913
+ const [locale, options] = args;
8914
+ if (locale.kind !== "literal" || locale.literalType !== "string")
8915
+ return null;
8916
+ if (options.kind !== "object-literal" || options.properties.length !== 1)
8917
+ return null;
8918
+ const prop = options.properties[0];
8919
+ if (prop.key !== "timeZone")
8920
+ return null;
8921
+ if (prop.value.kind !== "literal" || prop.value.literalType !== "string")
8922
+ return null;
8923
+ const tz = String(prop.value.value);
8924
+ if (!TO_LOCALE_TZ_RE.test(tz))
8925
+ return null;
8926
+ const receiverType = resolveReceiverType(callee.object, metadata, new Map);
8927
+ if (!receiverType || receiverType.kind !== "interface")
8928
+ return null;
8929
+ const typeName = baseTypeName(receiverType.raw);
8930
+ if (typeName !== "Date")
8931
+ return null;
8932
+ if (metadata.typeDefinitions.some((d) => d.name === typeName))
8933
+ return null;
8934
+ const pattern = resolveLocaleDatePattern(String(locale.value));
8935
+ if (pattern === null)
8936
+ return null;
8937
+ return {
8938
+ kind: "helper-call",
8939
+ helper: "format_date",
8940
+ args: [
8941
+ callee.object,
8942
+ { kind: "literal", value: pattern, literalType: "string" },
8943
+ { kind: "literal", value: tz, literalType: "string" }
8944
+ ]
8945
+ };
8946
+ }
8947
+ var toLocaleDatePlugin = {
8948
+ name: "toLocaleDateString",
8949
+ prepare(metadata) {
8950
+ if (!metadata.propsType || !typeReachesDate(metadata.propsType, metadata, new Set))
8951
+ return null;
8952
+ return (callee, args) => matchToLocaleDateStringCall(callee, args, metadata);
8953
+ }
8954
+ };
8955
+
8463
8956
  // src/jsx-to-ir.ts
8464
8957
  import { toHTMLAttrName, decodeEntities } from "@barefootjs/shared";
8465
8958
  var CLIENT_DIRECTIVE_INTERIOR_RE2 = /^\s*@client\s*$/;
@@ -8555,6 +9048,19 @@ function getDateLoweringMatcher(ctx) {
8555
9048
  }
8556
9049
  return ctx._dateLoweringMatcher;
8557
9050
  }
9051
+ function getToLocaleDateLoweringMatcher(ctx) {
9052
+ if (ctx._toLocaleDateLoweringMatcher === undefined) {
9053
+ const a = ctx.analyzer;
9054
+ const metadataSlice = {
9055
+ propsType: a.propsType,
9056
+ propsObjectName: a.propsObjectName,
9057
+ propsParams: a.propsParams,
9058
+ typeDefinitions: a.typeDefinitions
9059
+ };
9060
+ ctx._toLocaleDateLoweringMatcher = toLocaleDatePlugin.prepare(metadataSlice);
9061
+ }
9062
+ return ctx._toLocaleDateLoweringMatcher;
9063
+ }
8558
9064
  function lowerDateCalls(text, expr, ctx) {
8559
9065
  const matcher = getDateLoweringMatcher(ctx);
8560
9066
  if (!matcher)
@@ -8583,8 +9089,38 @@ function lowerDateCalls(text, expr, ctx) {
8583
9089
  }
8584
9090
  return restore(result);
8585
9091
  }
9092
+ function lowerToLocaleDateCalls(text, expr, ctx) {
9093
+ const matcher = getToLocaleDateLoweringMatcher(ctx);
9094
+ if (!matcher)
9095
+ return text;
9096
+ const candidates = [];
9097
+ function visit2(n) {
9098
+ if (ts11.isCallExpression(n) && n.arguments.length === 2 && ts11.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && n.expression.name.text === "toLocaleDateString") {
9099
+ candidates.push(n);
9100
+ }
9101
+ ts11.forEachChild(n, visit2);
9102
+ }
9103
+ visit2(expr);
9104
+ if (candidates.length === 0)
9105
+ return text;
9106
+ const { protect, restore, replaceProtectedCall } = createTemplateAwareStringProtector();
9107
+ let result = protect(text);
9108
+ for (const call of candidates) {
9109
+ const propAccess = call.expression;
9110
+ const node = matcher(tsNodeToParsedExpr(propAccess), call.arguments.map((a) => tsNodeToParsedExpr(a)));
9111
+ if (!node || node.kind !== "helper-call" || node.helper !== "format_date")
9112
+ continue;
9113
+ const [, patternArg, tzArg] = node.args;
9114
+ if (patternArg?.kind !== "literal" || tzArg?.kind !== "literal")
9115
+ continue;
9116
+ const receiverText = ctx.getJS(propAccess.expression);
9117
+ const matchText = ctx.getJS(call);
9118
+ result = replaceProtectedCall(result, matchText, () => `formatDate(${receiverText}, ${JSON.stringify(patternArg.value)}, ${JSON.stringify(tzArg.value)})`);
9119
+ }
9120
+ return restore(result);
9121
+ }
8586
9122
  function rewriteBarePropRefs2(text, expr, ctx) {
8587
- const dateLowered = lowerDateCalls(text, expr, ctx);
9123
+ const dateLowered = lowerToLocaleDateCalls(lowerDateCalls(text, expr, ctx), expr, ctx);
8588
9124
  let propNames = getDestructuredPropNames(ctx);
8589
9125
  if (!propNames)
8590
9126
  return dateLowered === text ? undefined : dateLowered;
@@ -13436,7 +13972,8 @@ var RUNTIME_IMPORT_CANDIDATES = [
13436
13972
  "tAfter",
13437
13973
  "beginTurn",
13438
13974
  "endTurn",
13439
- "date"
13975
+ "date",
13976
+ "formatDate"
13440
13977
  ];
13441
13978
  var RUNTIME_MODULE = "@barefootjs/client/runtime";
13442
13979
  var IMPORT_PLACEHOLDER = "/* __BAREFOOTJS_DOM_IMPORTS__ */";
@@ -14722,7 +15259,7 @@ function importsSearchParams(metadata) {
14722
15259
  function queryHrefLocalNames(metadata) {
14723
15260
  const names = new Set;
14724
15261
  for (const imp of metadata.imports) {
14725
- if (!QUERY_HREF_SOURCES.has(imp.source) || imp.isTypeOnly)
15262
+ if (!CLIENT_HELPER_SOURCES.has(imp.source) || imp.isTypeOnly)
14726
15263
  continue;
14727
15264
  for (const s of imp.specifiers) {
14728
15265
  if (s.isTypeOnly || s.isNamespace || s.isDefault)
@@ -14733,10 +15270,24 @@ function queryHrefLocalNames(metadata) {
14733
15270
  }
14734
15271
  return names;
14735
15272
  }
14736
- var QUERY_HREF_SOURCES = new Set([
15273
+ var CLIENT_HELPER_SOURCES = new Set([
14737
15274
  "@barefootjs/client",
14738
15275
  "@barefootjs/client/runtime"
14739
15276
  ]);
15277
+ function formatDateLocalNames(metadata) {
15278
+ const names = new Set;
15279
+ for (const imp of metadata.imports) {
15280
+ if (!CLIENT_HELPER_SOURCES.has(imp.source) || imp.isTypeOnly)
15281
+ continue;
15282
+ for (const s of imp.specifiers) {
15283
+ if (s.isTypeOnly || s.isNamespace || s.isDefault)
15284
+ continue;
15285
+ if (s.name === "formatDate")
15286
+ names.add(s.alias ?? s.name);
15287
+ }
15288
+ }
15289
+ return names;
15290
+ }
14740
15291
  function matchSearchParamsMethodCall(callee, args, localNames) {
14741
15292
  if (callee.kind !== "member" || callee.computed)
14742
15293
  return null;
@@ -16602,6 +17153,56 @@ function getReactiveDateLoweringMatcher(ctx) {
16602
17153
  };
16603
17154
  return datePlugin.prepare(metadataSlice);
16604
17155
  }
17156
+ function getReactiveToLocaleMatcher(ctx) {
17157
+ if (!ctx.propsType)
17158
+ return null;
17159
+ const metadataSlice = {
17160
+ propsType: ctx.propsType,
17161
+ propsObjectName: ctx.propsObjectName,
17162
+ propsParams: ctx.propsParams,
17163
+ typeDefinitions: ctx.typeDefinitions ?? []
17164
+ };
17165
+ return toLocaleDatePlugin.prepare(metadataSlice);
17166
+ }
17167
+ function lowerToLocaleCallsInReactiveExpr(expr, matcher) {
17168
+ if (!matcher)
17169
+ return expr;
17170
+ let sourceFile;
17171
+ try {
17172
+ sourceFile = ts14.createSourceFile("__reactive_expr__.ts", `(${expr});`, ts14.ScriptTarget.Latest, true, ts14.ScriptKind.TS);
17173
+ } catch {
17174
+ return expr;
17175
+ }
17176
+ const stmt = sourceFile.statements[0];
17177
+ if (!stmt || !ts14.isExpressionStatement(stmt))
17178
+ return expr;
17179
+ const root = ts14.isParenthesizedExpression(stmt.expression) ? stmt.expression.expression : stmt.expression;
17180
+ const candidates = [];
17181
+ const visit3 = (n) => {
17182
+ if (ts14.isCallExpression(n) && n.arguments.length === 2 && ts14.isPropertyAccessExpression(n.expression) && !n.expression.questionDotToken && n.expression.name.text === "toLocaleDateString") {
17183
+ candidates.push(n);
17184
+ }
17185
+ ts14.forEachChild(n, visit3);
17186
+ };
17187
+ visit3(root);
17188
+ if (candidates.length === 0)
17189
+ return expr;
17190
+ const { protect, restore, replaceProtectedCall } = createTemplateAwareStringProtector();
17191
+ let result = protect(expr);
17192
+ for (const call of candidates) {
17193
+ const propAccess = call.expression;
17194
+ const node = matcher(tsNodeToParsedExpr(propAccess), call.arguments.map((a) => tsNodeToParsedExpr(a)));
17195
+ if (!node || node.kind !== "helper-call" || node.helper !== "format_date")
17196
+ continue;
17197
+ const [, patternArg, tzArg] = node.args;
17198
+ if (patternArg?.kind !== "literal" || tzArg?.kind !== "literal")
17199
+ continue;
17200
+ const receiverText = propAccess.expression.getText(sourceFile);
17201
+ const matchText = call.getText(sourceFile);
17202
+ result = replaceProtectedCall(result, matchText, () => `formatDate(${receiverText}, ${JSON.stringify(patternArg.value)}, ${JSON.stringify(tzArg.value)})`);
17203
+ }
17204
+ return restore(result);
17205
+ }
16605
17206
  function lowerDateCallsInReactiveExpr(expr, matcher) {
16606
17207
  if (!matcher)
16607
17208
  return expr;
@@ -16641,6 +17242,7 @@ function lowerDateCallsInReactiveExpr(expr, matcher) {
16641
17242
  }
16642
17243
  function emitDynamicTextUpdates(lines, ctx) {
16643
17244
  const dateLoweringMatcher = getReactiveDateLoweringMatcher(ctx);
17245
+ const toLocaleMatcher = getReactiveToLocaleMatcher(ctx);
16644
17246
  const byExpression = new Map;
16645
17247
  for (const elem of ctx.dynamicElements) {
16646
17248
  const key = elem.expression;
@@ -16650,7 +17252,7 @@ function emitDynamicTextUpdates(lines, ctx) {
16650
17252
  byExpression.get(key).push(elem);
16651
17253
  }
16652
17254
  for (const [rawExpr, elems] of byExpression) {
16653
- const expr = lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher);
17255
+ const expr = lowerToLocaleCallsInReactiveExpr(lowerDateCallsInReactiveExpr(rawExpr, dateLoweringMatcher), toLocaleMatcher);
16654
17256
  const conditionalElems = elems.filter((e) => e.insideConditional);
16655
17257
  const normalElems = elems.filter((e) => !e.insideConditional);
16656
17258
  if (normalElems.length > 0 || conditionalElems.length > 0) {
@@ -19875,13 +20477,14 @@ function pushDiagnostic(errors, seen, loc, method, receiverPath, isProp, typeNam
19875
20477
  return;
19876
20478
  seen.add(key);
19877
20479
  const receiver = isProp ? `prop '${receiverPath}'` : `'${receiverPath}'`;
20480
+ 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
20481
  errors.push({
19879
20482
  code: ErrorCodes.UNSUPPORTED_JSX_PATTERN,
19880
20483
  severity: "error",
19881
20484
  message: `Expression cannot be compiled to marked template: method '.${method}()' on ${receiver} of host type '${typeName}' has no catalogued lowering.`,
19882
20485
  loc,
19883
20486
  suggestion: {
19884
- message: "Add /* @client */ to evaluate this expression on the client only, or pre-compute the value server-side."
20487
+ message: suggestion
19885
20488
  }
19886
20489
  });
19887
20490
  }
@@ -21397,6 +22000,29 @@ function isOmitBranch(node) {
21397
22000
  }
21398
22001
  return false;
21399
22002
  }
22003
+ // src/format-date-lowering.ts
22004
+ var UTC_LITERAL = { kind: "literal", value: "UTC", literalType: "string" };
22005
+ function matchFormatDateCall(callee, args, locals) {
22006
+ if (callee.kind !== "identifier" || !locals.has(callee.name))
22007
+ return null;
22008
+ if (args.length < 2 || args.length > 3)
22009
+ return null;
22010
+ return {
22011
+ kind: "helper-call",
22012
+ helper: "format_date",
22013
+ args: [args[0], args[1], args[2] ?? UTC_LITERAL]
22014
+ };
22015
+ }
22016
+ var formatDatePlugin = {
22017
+ name: "formatDate",
22018
+ prepare(metadata) {
22019
+ const locals = formatDateLocalNames(metadata);
22020
+ if (locals.size === 0)
22021
+ return null;
22022
+ return (callee, args) => matchFormatDateCall(callee, args, locals);
22023
+ }
22024
+ };
22025
+
21400
22026
  // src/builtin-lowering-plugins.ts
21401
22027
  var queryHrefPlugin = {
21402
22028
  name: "queryHref",
@@ -21410,7 +22036,12 @@ var queryHrefPlugin = {
21410
22036
  };
21411
22037
  }
21412
22038
  };
21413
- var BUILTIN_LOWERING_PLUGINS = [queryHrefPlugin, datePlugin];
22039
+ var BUILTIN_LOWERING_PLUGINS = [
22040
+ queryHrefPlugin,
22041
+ datePlugin,
22042
+ formatDatePlugin,
22043
+ toLocaleDatePlugin
22044
+ ];
21414
22045
  function registerBuiltinLoweringPlugins() {
21415
22046
  for (const plugin of BUILTIN_LOWERING_PLUGINS)
21416
22047
  registerLoweringPlugin(plugin);
@@ -24453,6 +25084,7 @@ export {
24453
25084
  formatFallbackExplanations,
24454
25085
  formatEventSummary,
24455
25086
  formatError,
25087
+ formatDateLocalNames,
24456
25088
  formatComponentSummary,
24457
25089
  formatComponentGraph,
24458
25090
  formatBudgetDiff,