uidex 0.8.0 → 0.9.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.
@@ -153,7 +153,18 @@ interface SourceConfig {
153
153
  interface ConventionsConfig {
154
154
  primitives?: string[] | false;
155
155
  features?: string | false;
156
- pages?: "auto" | false;
156
+ /**
157
+ * How Page entities are derived from route files.
158
+ * - `"auto"` (default): infer the route root from the `app`/`pages`/
159
+ * `routes` directory-name heuristic.
160
+ * - `{ routesDir }`: the route root is exactly this dir (relative to the
161
+ * config dir), detected with the TanStack / React Router `routes/`-style
162
+ * convention. Use for a custom TanStack `routesDirectory`.
163
+ * - `false`: do not derive pages from routes.
164
+ */
165
+ pages?: "auto" | false | {
166
+ routesDir: string;
167
+ };
157
168
  flows?: string[] | false;
158
169
  regions?: "landmarks" | false;
159
170
  }
@@ -611,10 +622,26 @@ declare function emit(opts: EmitOptions): EmitResult;
611
622
  * Detect framework-aware routes:
612
623
  * - Next.js App Router: `**\/app/**\/page.tsx` -> `/segments`
613
624
  * - Next.js Pages Router: `**\/pages/**\/*.tsx` -> `/segments` (excludes _app, _document, api/)
614
- * - Vite + React Router / TanStack Router:
615
- * `**\/routes/**\/*.tsx` -> `/segments`
625
+ * - Vite + TanStack Router / React Router: `**\/routes/**\/*.tsx` -> `/segments`.
626
+ * Honors TanStack conventions: pathless layouts (`_layout`) and route
627
+ * groups (`(group)`) drop from the path, the folder route file
628
+ * (`route.tsx`) collapses like `index`, `-`-prefixed and `api/` files are
629
+ * excluded, and flat dot-notation filenames (`a.b.$c.tsx`) split into
630
+ * nested segments. Dynamic segments are normalized to the canonical
631
+ * `[param]` form used everywhere else in the pipeline: `$id` -> `[id]`,
632
+ * the splat `$` -> `[...splat]`, and `[.]`-escaped literals are unwrapped.
616
633
  */
617
- declare function detectRoutes(files: ScannedFile[]): DetectedRoute[];
634
+ interface DetectRoutesOptions {
635
+ /**
636
+ * Explicit routes root, as it appears in `displayPath` (relative to the
637
+ * config dir). When set, only files under this dir are routes and the
638
+ * TanStack / React Router `routes/`-style convention is applied — overriding
639
+ * the `app`/`pages`/`routes` directory-name heuristic. Use for a custom
640
+ * TanStack `routesDirectory`.
641
+ */
642
+ routesDir?: string;
643
+ }
644
+ declare function detectRoutes(files: ScannedFile[], options?: DetectRoutesOptions): DetectedRoute[];
618
645
  declare function pathToId(routePath: string): string;
619
646
 
620
647
  interface GitResolveOptions {
@@ -153,7 +153,18 @@ interface SourceConfig {
153
153
  interface ConventionsConfig {
154
154
  primitives?: string[] | false;
155
155
  features?: string | false;
156
- pages?: "auto" | false;
156
+ /**
157
+ * How Page entities are derived from route files.
158
+ * - `"auto"` (default): infer the route root from the `app`/`pages`/
159
+ * `routes` directory-name heuristic.
160
+ * - `{ routesDir }`: the route root is exactly this dir (relative to the
161
+ * config dir), detected with the TanStack / React Router `routes/`-style
162
+ * convention. Use for a custom TanStack `routesDirectory`.
163
+ * - `false`: do not derive pages from routes.
164
+ */
165
+ pages?: "auto" | false | {
166
+ routesDir: string;
167
+ };
157
168
  flows?: string[] | false;
158
169
  regions?: "landmarks" | false;
159
170
  }
@@ -611,10 +622,26 @@ declare function emit(opts: EmitOptions): EmitResult;
611
622
  * Detect framework-aware routes:
612
623
  * - Next.js App Router: `**\/app/**\/page.tsx` -> `/segments`
613
624
  * - Next.js Pages Router: `**\/pages/**\/*.tsx` -> `/segments` (excludes _app, _document, api/)
614
- * - Vite + React Router / TanStack Router:
615
- * `**\/routes/**\/*.tsx` -> `/segments`
625
+ * - Vite + TanStack Router / React Router: `**\/routes/**\/*.tsx` -> `/segments`.
626
+ * Honors TanStack conventions: pathless layouts (`_layout`) and route
627
+ * groups (`(group)`) drop from the path, the folder route file
628
+ * (`route.tsx`) collapses like `index`, `-`-prefixed and `api/` files are
629
+ * excluded, and flat dot-notation filenames (`a.b.$c.tsx`) split into
630
+ * nested segments. Dynamic segments are normalized to the canonical
631
+ * `[param]` form used everywhere else in the pipeline: `$id` -> `[id]`,
632
+ * the splat `$` -> `[...splat]`, and `[.]`-escaped literals are unwrapped.
616
633
  */
617
- declare function detectRoutes(files: ScannedFile[]): DetectedRoute[];
634
+ interface DetectRoutesOptions {
635
+ /**
636
+ * Explicit routes root, as it appears in `displayPath` (relative to the
637
+ * config dir). When set, only files under this dir are routes and the
638
+ * TanStack / React Router `routes/`-style convention is applied — overriding
639
+ * the `app`/`pages`/`routes` directory-name heuristic. Use for a custom
640
+ * TanStack `routesDirectory`.
641
+ */
642
+ routesDir?: string;
643
+ }
644
+ declare function detectRoutes(files: ScannedFile[], options?: DetectRoutesOptions): DetectedRoute[];
618
645
  declare function pathToId(routePath: string): string;
619
646
 
620
647
  interface GitResolveOptions {
@@ -135,7 +135,13 @@ function validateConfig(raw) {
135
135
  fail(`conventions.features must be a string or false`);
136
136
  }
137
137
  if (c.pages !== void 0 && c.pages !== false && c.pages !== "auto") {
138
- fail(`conventions.pages must be "auto" or false`);
138
+ const p2 = c.pages;
139
+ const routesDir = typeof p2 === "object" && p2 !== null && !Array.isArray(p2) ? p2.routesDir : void 0;
140
+ if (typeof p2 !== "object" || p2 === null || Array.isArray(p2) || Object.keys(p2).some((k) => k !== "routesDir") || typeof routesDir !== "string" || routesDir.length === 0) {
141
+ fail(
142
+ `conventions.pages must be "auto", false, or { routesDir: string }`
143
+ );
144
+ }
139
145
  }
140
146
  if (c.flows !== void 0 && c.flows !== false) {
141
147
  assertStringArray(c.flows, "conventions.flows");
@@ -1820,13 +1826,26 @@ import * as path4 from "path";
1820
1826
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
1821
1827
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
1822
1828
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
1823
- function detectRoutes(files) {
1829
+ function detectRoutes(files, options = {}) {
1824
1830
  const out2 = [];
1825
1831
  const seen = /* @__PURE__ */ new Set();
1832
+ const routesDir = options.routesDir ? options.routesDir.replace(/^\.?\/+/, "").replace(/\/+$/, "") : null;
1833
+ const routesDirParts = routesDir ? routesDir.split("/") : null;
1826
1834
  for (const f of files) {
1827
1835
  const rel = f.displayPath;
1828
1836
  const parts = rel.split("/");
1829
1837
  const base = parts[parts.length - 1];
1838
+ if (routesDirParts) {
1839
+ const end = dirEndIndex(parts, routesDirParts);
1840
+ if (end === -1 || end > parts.length - 1) continue;
1841
+ if (!PAGES_ROUTER_BASENAME.test(base)) continue;
1842
+ const routePath = routesStylePath(
1843
+ parts.slice(end, parts.length - 1),
1844
+ base
1845
+ );
1846
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
1847
+ continue;
1848
+ }
1830
1849
  const appIdx = parts.indexOf("app");
1831
1850
  if (appIdx !== -1 && PAGE_BASENAME.test(base)) {
1832
1851
  const routeSegments = parts.slice(appIdx + 1, parts.length - 1);
@@ -1853,15 +1872,11 @@ function detectRoutes(files) {
1853
1872
  }
1854
1873
  const routesIdx = parts.indexOf("routes");
1855
1874
  if (routesIdx !== -1 && PAGES_ROUTER_BASENAME.test(base)) {
1856
- const segs = parts.slice(routesIdx + 1);
1857
- const last = segs[segs.length - 1];
1858
- if (last.startsWith("_")) continue;
1859
- const normalized = [
1860
- ...segs.slice(0, -1),
1861
- base.replace(/\.[^.]+$/, "")
1862
- ].filter((s) => s !== "index" && s !== "__root");
1863
- const routePath = formatNextAppPath(normalized);
1864
- push(out2, seen, routePath, f.displayPath);
1875
+ const routePath = routesStylePath(
1876
+ parts.slice(routesIdx + 1, parts.length - 1),
1877
+ base
1878
+ );
1879
+ if (routePath !== null) push(out2, seen, routePath, f.displayPath);
1865
1880
  continue;
1866
1881
  }
1867
1882
  }
@@ -1877,6 +1892,63 @@ function formatNextAppPath(segments) {
1877
1892
  if (kept.length === 0) return "/";
1878
1893
  return "/" + kept.join("/");
1879
1894
  }
1895
+ function dirEndIndex(parts, dirParts) {
1896
+ if (dirParts.length === 0) return 0;
1897
+ for (let i = 0; i + dirParts.length <= parts.length; i++) {
1898
+ let match = true;
1899
+ for (let j = 0; j < dirParts.length; j++) {
1900
+ if (parts[i + j] !== dirParts[j]) {
1901
+ match = false;
1902
+ break;
1903
+ }
1904
+ }
1905
+ if (match) return i + dirParts.length;
1906
+ }
1907
+ return -1;
1908
+ }
1909
+ function routesStylePath(dirSegs, base) {
1910
+ const fileSegs = splitFlatSegments(base.replace(/\.[^.]+$/, ""));
1911
+ const leaf = fileSegs[fileSegs.length - 1];
1912
+ if (leaf.startsWith("_")) return null;
1913
+ const rawSegs = [...dirSegs, ...fileSegs];
1914
+ if (rawSegs.some((s) => s.startsWith("-"))) return null;
1915
+ if (rawSegs[0] === "api") return null;
1916
+ return formatTanStackPath(rawSegs);
1917
+ }
1918
+ function splitFlatSegments(name) {
1919
+ const segs = [];
1920
+ let cur = "";
1921
+ let depth = 0;
1922
+ for (const ch of name) {
1923
+ if (ch === "[") depth++;
1924
+ else if (ch === "]") depth = Math.max(0, depth - 1);
1925
+ if (ch === "." && depth === 0) {
1926
+ segs.push(cur);
1927
+ cur = "";
1928
+ continue;
1929
+ }
1930
+ cur += ch;
1931
+ }
1932
+ segs.push(cur);
1933
+ return segs;
1934
+ }
1935
+ function formatTanStackPath(segments) {
1936
+ const kept = [];
1937
+ for (const seg of segments) {
1938
+ if (!seg) continue;
1939
+ if (seg === "index" || seg === "route" || seg === "__root") continue;
1940
+ if (seg.startsWith("_")) continue;
1941
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
1942
+ kept.push(normalizeTanStackSegment(seg.replace(/_$/, "")));
1943
+ }
1944
+ if (kept.length === 0) return "/";
1945
+ return "/" + kept.join("/");
1946
+ }
1947
+ function normalizeTanStackSegment(seg) {
1948
+ if (seg === "$") return "[...splat]";
1949
+ if (seg.startsWith("$")) return `[${seg.slice(1)}]`;
1950
+ return seg.replace(/\[([^\]]*)\]/g, "$1");
1951
+ }
1880
1952
  function pathToId(routePath) {
1881
1953
  if (routePath === "/") return "root";
1882
1954
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -1972,9 +2044,12 @@ function resolve2(ctx) {
1972
2044
  function metaWithComposes(kind, id, base) {
1973
2045
  const composes = directChildren.get(`${kind}:${id}`);
1974
2046
  if (!composes || composes.length === 0) return base;
1975
- return { ...base ?? {}, composes };
2047
+ return { ...base, composes };
1976
2048
  }
1977
- const routes = conventions.pages === "auto" ? detectRoutes(ctx.extracted.map((e) => e.file)) : [];
2049
+ const routes = conventions.pages === false ? [] : detectRoutes(
2050
+ ctx.extracted.map((e) => e.file),
2051
+ conventions.pages === "auto" ? {} : { routesDir: conventions.pages.routesDir }
2052
+ );
1978
2053
  const handledPageFiles = /* @__PURE__ */ new Set();
1979
2054
  for (const route of routes) {
1980
2055
  const routeDir = path4.posix.dirname(route.file);