@pylonsync/sdk 0.3.246 → 0.3.248

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +54 -12
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.246",
6
+ "version": "0.3.248",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -331,8 +331,11 @@ export interface RouteDefinition {
331
331
  * NOT matched as navigable URLs — the host renders `not-found` for
332
332
  * unmatched URLs (HTTP 404) and `error` on render failure (HTTP 500).
333
333
  * `path` records the segment prefix the boundary covers (`/` for root).
334
+ * `"route"` is a form/method handler (`app/.../route.ts` exporting
335
+ * POST/PUT/PATCH/DELETE) — matched on its `path` for non-GET requests only,
336
+ * never rendered as a page.
334
337
  */
335
- kind?: "page" | "not-found" | "error";
338
+ kind?: "page" | "not-found" | "error" | "route";
336
339
  }
337
340
 
338
341
  export function defineRoute(route: RouteDefinition): RouteDefinition {
@@ -506,8 +509,8 @@ export interface ManifestRoute {
506
509
  auth?: string;
507
510
  component?: string;
508
511
  layouts?: string[];
509
- /** "not-found" / "error" boundary modules; omitted for normal pages. */
510
- kind?: "page" | "not-found" | "error";
512
+ /** "not-found" / "error" boundaries, or "route" form handlers; omitted for normal pages. */
513
+ kind?: "page" | "not-found" | "error" | "route";
511
514
  }
512
515
 
513
516
  export interface ManifestInputField {
@@ -669,8 +672,9 @@ export function routesToManifest(routes: RouteDefinition[]): ManifestRoute[] {
669
672
  * ones at each depth — so the Rust matcher's first-match-wins
670
673
  * lookup picks the right route.
671
674
  *
672
- * Phase 1 only: no `loading.tsx` / `error.tsx` / `not-found.tsx`
673
- * support yet.
675
+ * `not-found.tsx` / `error.tsx` boundaries are emitted as `kind`-tagged
676
+ * routes here; `loading.tsx` is resolved at render time by the SSR runtime
677
+ * (filesystem walk), so it needs no discovery entry.
674
678
  */
675
679
  export async function discoverAppRoutes(opts?: {
676
680
  appDir?: string;
@@ -719,8 +723,10 @@ export async function discoverAppRoutes(opts?: {
719
723
  layouts: string[];
720
724
  kind: "not-found" | "error";
721
725
  };
726
+ type RouteHandlerHit = { segments: string[]; component: string };
722
727
  const pages: PageHit[] = [];
723
728
  const boundaries: BoundaryHit[] = [];
729
+ const routeHandlers: RouteHandlerHit[] = [];
724
730
 
725
731
  // Resolve the first existing `<base>.{tsx,ts,jsx,js}` in `dir` and
726
732
  // return it as a cwd-relative, extension-less module path (or null).
@@ -770,6 +776,12 @@ export async function discoverAppRoutes(opts?: {
770
776
  kind: "error",
771
777
  });
772
778
  }
779
+ // Form/method handler (`route.ts` exporting POST/PUT/PATCH/DELETE). Matched
780
+ // on its path for non-GET requests only — never rendered, no layouts.
781
+ const routeHere = findModule(dir, "route");
782
+ if (routeHere) {
783
+ routeHandlers.push({ segments: [...segments], component: routeHere });
784
+ }
773
785
  for (const e of entries) {
774
786
  if (!e.isDirectory()) continue;
775
787
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
@@ -781,14 +793,27 @@ export async function discoverAppRoutes(opts?: {
781
793
  }
782
794
  walk(appDir, [], []);
783
795
 
796
+ // Segment kinds, least-to-most greedy:
797
+ // static "blog" rank 0 (most specific)
798
+ // dynamic param "[slug]" rank 1 → :slug
799
+ // catch-all "[...slug]" rank 2 → *slug (matches ≥1 segment)
800
+ // optional catch-all"[[...slug]]" rank 3 → *?slug (matches ≥0 segments)
801
+ // A catch-all is only valid as the LAST segment of a route (it consumes
802
+ // the rest of the path); Next.js enforces the same.
803
+ const isOptionalCatchAll = (s: string): boolean =>
804
+ s.startsWith("[[...") && s.endsWith("]]");
805
+ const isCatchAll = (s: string): boolean =>
806
+ s.startsWith("[...") && s.endsWith("]") && !isOptionalCatchAll(s);
784
807
  const isParam = (s: string): boolean =>
785
- s.startsWith("[") && s.endsWith("]");
808
+ s.startsWith("[") && s.endsWith("]") && !isCatchAll(s) && !isOptionalCatchAll(s);
809
+ const segRank = (s: string): number =>
810
+ isOptionalCatchAll(s) ? 3 : isCatchAll(s) ? 2 : isParam(s) ? 1 : 0;
786
811
  pages.sort((a, b) => {
787
812
  const minLen = Math.min(a.segments.length, b.segments.length);
788
813
  for (let i = 0; i < minLen; i++) {
789
- const ap = isParam(a.segments[i]);
790
- const bp = isParam(b.segments[i]);
791
- if (ap !== bp) return ap ? 1 : -1;
814
+ const ar = segRank(a.segments[i]);
815
+ const br = segRank(b.segments[i]);
816
+ if (ar !== br) return ar - br; // more specific (lower rank) first
792
817
  if (a.segments[i] !== b.segments[i]) {
793
818
  return a.segments[i] < b.segments[i] ? -1 : 1;
794
819
  }
@@ -796,9 +821,17 @@ export async function discoverAppRoutes(opts?: {
796
821
  return a.segments.length - b.segments.length;
797
822
  });
798
823
 
824
+ // Convert a discovered file segment to its route-pattern token. The Rust
825
+ // matcher (frontend.rs `match_ssr_route`) reads these markers back:
826
+ // :name dynamic param *name catch-all *?name optional catch-all
827
+ const segmentToToken = (s: string): string => {
828
+ if (isOptionalCatchAll(s)) return `*?${s.slice(5, -2)}`; // [[...x]] → *?x
829
+ if (isCatchAll(s)) return `*${s.slice(4, -1)}`; // [...x] → *x
830
+ if (isParam(s)) return `:${s.slice(1, -1)}`; // [x] → :x
831
+ return s;
832
+ };
799
833
  const segmentsToPath = (segments: string[]): string =>
800
- "/" +
801
- segments.map((s) => (isParam(s) ? `:${s.slice(1, -1)}` : s)).join("/");
834
+ "/" + segments.map(segmentToToken).join("/");
802
835
 
803
836
  const pageRoutes: RouteDefinition[] = pages.map((p) => ({
804
837
  path: segmentsToPath(p.segments),
@@ -821,7 +854,16 @@ export async function discoverAppRoutes(opts?: {
821
854
  kind: b.kind,
822
855
  }));
823
856
 
824
- return [...pageRoutes, ...boundaryRoutes];
857
+ // Form/method handlers. Keyed by the SAME path token form as pages
858
+ // (`:param` / `*catch-all`), matched by the host for non-GET methods only.
859
+ const routeRoutes: RouteDefinition[] = routeHandlers.map((r) => ({
860
+ path: segmentsToPath(r.segments),
861
+ mode: "ssr" as const,
862
+ component: r.component,
863
+ kind: "route" as const,
864
+ }));
865
+
866
+ return [...pageRoutes, ...boundaryRoutes, ...routeRoutes];
825
867
  }
826
868
 
827
869
  export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {