@godxjp/ui 32.1.1 → 32.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "32.1.1",
4
- "godxUiMcp": "32.1.1",
3
+ "version": "32.1.3",
4
+ "godxUiMcp": "32.1.3",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1829,6 +1829,8 @@ function staleOwnedRules() {
1829
1829
  const FORM_TAGS = new Set(["Form", "FormRoot"]);
1830
1830
  const formOpenTag = new RegExp(`<(${[...FORM_TAGS].join("|")})(?=[\\s>])([^>]*)>`, "g");
1831
1831
  function formTagWalk(scanContent) {
1832
+ const formSpans = [];
1833
+ const formOpens = [];
1832
1834
  const stack = [];
1833
1835
  const bareByParent = new Map();
1834
1836
  const underForm = new Set();
@@ -1846,11 +1848,18 @@ function formTagWalk(scanContent) {
1846
1848
  break;
1847
1849
  }
1848
1850
  }
1849
- if (FORM_TAGS.has(tag)) formDepth = Math.max(0, formDepth - 1);
1851
+ if (FORM_TAGS.has(tag)) {
1852
+ formDepth = Math.max(0, formDepth - 1);
1853
+ const open = formOpens.pop();
1854
+ if (open !== undefined) formSpans.push([open, m.index]);
1855
+ }
1850
1856
  continue;
1851
1857
  }
1852
1858
  if (tag === "FormField" && formDepth === 0) {
1853
- const parent = stack.at(-1)?.index ?? -1;
1859
+ // A field at the TOP of a return has no parent tag; key it by its component, or two
1860
+ // one-field components in one file (`Field`, `Choice`) read as "Field renders 2 FormFields"
1861
+ // (gh#1260). A lone field is not flagged, so each is now judged on its own.
1862
+ const parent = stack.at(-1)?.index ?? `top:${enclosingComponent(scanContent, m.index)}`;
1854
1863
  (bareByParent.get(parent) ?? bareByParent.set(parent, []).get(parent)).push(m.index);
1855
1864
  }
1856
1865
  if (/^[A-Z]/.test(tag)) {
@@ -1859,11 +1868,78 @@ function formTagWalk(scanContent) {
1859
1868
  }
1860
1869
  if (selfClose) continue;
1861
1870
  stack.push({ tag, index: m.index });
1862
- if (FORM_TAGS.has(tag)) formDepth += 1;
1871
+ if (FORM_TAGS.has(tag)) {
1872
+ formDepth += 1;
1873
+ formOpens.push(m.index);
1874
+ }
1875
+ }
1876
+ // gh#1260: JSX inside a RENDER HELPER (`const legFields = (leg) => (…)`) has no Form above it in
1877
+ // the source, but it renders wherever the helper is CALLED. When every call of that helper is
1878
+ // under a Form — directly, or inside another such helper — the JSX is under that Form too.
1879
+ const covered = renderHelperCoverage(scanContent, formSpans);
1880
+ if (covered) {
1881
+ for (const [parent, indexes] of bareByParent) {
1882
+ const kept = indexes.filter((i) => !covered(i));
1883
+ if (kept.length) bareByParent.set(parent, kept);
1884
+ else bareByParent.delete(parent);
1885
+ }
1886
+ for (const [tag, indexes] of outsideForm) {
1887
+ const kept = indexes.filter((i) => !covered(i));
1888
+ if (kept.length < indexes.length) underForm.add(tag);
1889
+ if (kept.length) outsideForm.set(tag, kept);
1890
+ else outsideForm.delete(tag);
1891
+ }
1863
1892
  }
1864
1893
  return { bareByParent, underForm, outsideForm };
1865
1894
  }
1866
1895
 
1896
+ /**
1897
+ * `index → true` when `index` sits in a same-file render helper whose every call is under a Form
1898
+ * (gh#1260). Helpers are lowercase `function name(` / `const name = (…) =>` declarations; their
1899
+ * body is found by bracket matching, and a helper never called is NOT covered (it might be
1900
+ * exported or passed as a render prop). Returns null when the file declares no helper.
1901
+ */
1902
+ function renderHelperCoverage(scanContent, formSpans) {
1903
+ const helpers = [];
1904
+ for (const m of scanContent.matchAll(
1905
+ /(?<![\w$.])(?:function\s+([a-z]\w*)\s*(?:<[^>]*>)?\s*\(|(?:const|let)\s+([a-z]\w*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:\([^()]*(?:\([^()]*\)[^()]*)*\)|[a-z]\w*)\s*(?::[^=]+?)?=>\s*)/g,
1906
+ )) {
1907
+ const name = m[1] ?? m[2];
1908
+ let open;
1909
+ if (m[1]) {
1910
+ const params = matchBracket(scanContent, m.index + m[0].length - 1);
1911
+ if (params < 0) continue;
1912
+ open = scanContent.indexOf("{", params);
1913
+ } else {
1914
+ open = m.index + m[0].length;
1915
+ if (scanContent[open] !== "{" && scanContent[open] !== "(") continue;
1916
+ }
1917
+ const close = open < 0 ? -1 : matchBracket(scanContent, open);
1918
+ if (close < 0) continue;
1919
+ helpers.push({ name, from: open, to: close, decl: m.index });
1920
+ }
1921
+ if (!helpers.length) return null;
1922
+ const inForm = (i) => formSpans.some(([a, b]) => i > a && i < b);
1923
+ /** The innermost helper whose body holds `i`. */
1924
+ const helperAt = (i) =>
1925
+ helpers
1926
+ .filter((h) => i > h.from && i < h.to)
1927
+ .reduce((inner, h) => (!inner || h.from > inner.from ? h : inner), null);
1928
+ const callsOf = (name) =>
1929
+ [...scanContent.matchAll(new RegExp(`(?<![\\w$.])${name}\\s*\\(`, "g"))]
1930
+ .map((c) => c.index)
1931
+ .filter((i) => !helpers.some((h) => h.name === name && i >= h.decl && i < h.from));
1932
+ const covered = (i, depth = 0) => {
1933
+ if (inForm(i)) return true;
1934
+ if (depth > 4) return false;
1935
+ const helper = helperAt(i);
1936
+ if (!helper) return false;
1937
+ const calls = callsOf(helper.name);
1938
+ return calls.length > 0 && calls.every((c) => covered(c, depth + 1));
1939
+ };
1940
+ return (i) => covered(i);
1941
+ }
1942
+
1867
1943
  /** The PascalCase component declared last before `index` — the one a FormField group sits in. */
1868
1944
  function enclosingComponent(scanContent, index) {
1869
1945
  let name = null;