@pylonsync/sdk 0.3.220 → 0.3.222

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 +150 -1
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.220",
6
+ "version": "0.3.222",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Route modes
3
3
  // ---------------------------------------------------------------------------
4
4
 
5
- export type RouteMode = "static" | "server" | "live";
5
+ export type RouteMode = "static" | "server" | "live" | "ssr";
6
6
 
7
7
  // ---------------------------------------------------------------------------
8
8
  // Field types
@@ -312,6 +312,18 @@ export interface RouteDefinition {
312
312
  mode: RouteMode;
313
313
  query?: string;
314
314
  auth?: AuthMode;
315
+ /**
316
+ * Project-relative module path (e.g. `app/hello/page`) for SSR
317
+ * routes. Required when `mode === "ssr"`. Discovered automatically
318
+ * by `discoverAppRoutes()`; only specify manually for one-off
319
+ * SSR routes outside the `app/` tree.
320
+ */
321
+ component?: string;
322
+ /**
323
+ * Layout module path chain (root→leaf). Each layout wraps the next
324
+ * as `children`. Only relevant for `mode === "ssr"`.
325
+ */
326
+ layouts?: string[];
315
327
  }
316
328
 
317
329
  export function defineRoute(route: RouteDefinition): RouteDefinition {
@@ -483,6 +495,8 @@ export interface ManifestRoute {
483
495
  mode: string;
484
496
  query?: string;
485
497
  auth?: string;
498
+ component?: string;
499
+ layouts?: string[];
486
500
  }
487
501
 
488
502
  export interface ManifestInputField {
@@ -621,10 +635,145 @@ export function routesToManifest(routes: RouteDefinition[]): ManifestRoute[] {
621
635
  const result: ManifestRoute = { path: r.path, mode: r.mode };
622
636
  if (r.query) result.query = r.query;
623
637
  if (r.auth) result.auth = r.auth;
638
+ if (r.component) result.component = r.component;
639
+ if (r.layouts && r.layouts.length > 0) result.layouts = r.layouts;
624
640
  return result;
625
641
  });
626
642
  }
627
643
 
644
+ /**
645
+ * Walk the project's `app/` directory and discover file-based SSR
646
+ * routes. Returns `RouteDefinition[]` ready to slot into
647
+ * `buildManifest({ routes })`.
648
+ *
649
+ * Mapping (Next App Router-shaped):
650
+ * - `app/page.tsx` → `/`
651
+ * - `app/about/page.tsx` → `/about`
652
+ * - `app/blog/[slug]/page.tsx` → `/blog/:slug`
653
+ * - `app/layout.tsx` wraps every page below
654
+ * - `app/(marketing)/about/page.tsx` → `/about` (group strip)
655
+ *
656
+ * Sorts deterministically — literal segments before parameterized
657
+ * ones at each depth — so the Rust matcher's first-match-wins
658
+ * lookup picks the right route.
659
+ *
660
+ * Phase 1 only: no `loading.tsx` / `error.tsx` / `not-found.tsx`
661
+ * support yet.
662
+ */
663
+ export async function discoverAppRoutes(opts?: {
664
+ appDir?: string;
665
+ }): Promise<RouteDefinition[]> {
666
+ // Pull node fs/path lazily. @pylonsync/sdk has no @types/node dep
667
+ // (kept light so client bundles stay tiny), so we type as `any`
668
+ // and validate at runtime — users call this from app.ts under
669
+ // Bun/Node, where the requires resolve.
670
+ //
671
+ // ESM Bun/Node doesn't expose `require` on globalThis. Build one
672
+ // via createRequire(import.meta.url) — the standard ESM →
673
+ // node-builtins escape hatch. The optional-chain on `require`
674
+ // covers the browser case (returns undefined, we fall through to
675
+ // []).
676
+ let fs: any;
677
+ let path: any;
678
+ try {
679
+ const nodeReq = (globalThis as any).require ??
680
+ (await import("node:module")).createRequire(import.meta.url);
681
+ fs = nodeReq("node:fs");
682
+ path = nodeReq("node:path");
683
+ } catch {
684
+ // Browser bundle hit — there's no `app/` to discover. Returning
685
+ // [] keeps the function safe to import from client code too.
686
+ return [];
687
+ }
688
+ if (!fs || !path) return [];
689
+
690
+ const cwd = (globalThis as any).process?.cwd?.() ?? ".";
691
+ const appDir =
692
+ opts?.appDir && path.isAbsolute(opts.appDir)
693
+ ? opts.appDir
694
+ : path.join(cwd, opts?.appDir ?? "app");
695
+ if (!fs.existsSync(appDir) || !fs.statSync(appDir).isDirectory()) {
696
+ return [];
697
+ }
698
+
699
+ type PageHit = {
700
+ segments: string[];
701
+ component: string;
702
+ layouts: string[];
703
+ };
704
+ const pages: PageHit[] = [];
705
+
706
+ function walk(dir: string, segments: string[], layouts: string[]): void {
707
+ let entries: Array<{ name: string; isDirectory(): boolean }>;
708
+ try {
709
+ entries = fs.readdirSync(dir, { withFileTypes: true });
710
+ } catch {
711
+ return;
712
+ }
713
+ const layoutHere = [
714
+ "layout.tsx",
715
+ "layout.ts",
716
+ "layout.jsx",
717
+ "layout.js",
718
+ ]
719
+ .map((n) => path.join(dir, n))
720
+ .find((p) => fs.existsSync(p));
721
+ const nextLayouts = layoutHere
722
+ ? [
723
+ ...layouts,
724
+ path.relative(cwd, layoutHere).replace(/\.(tsx?|jsx?)$/, ""),
725
+ ]
726
+ : layouts;
727
+ const pageHere = ["page.tsx", "page.ts", "page.jsx", "page.js"]
728
+ .map((n) => path.join(dir, n))
729
+ .find((p) => fs.existsSync(p));
730
+ if (pageHere) {
731
+ pages.push({
732
+ segments: [...segments],
733
+ component: path
734
+ .relative(cwd, pageHere)
735
+ .replace(/\.(tsx?|jsx?)$/, ""),
736
+ layouts: nextLayouts,
737
+ });
738
+ }
739
+ for (const e of entries) {
740
+ if (!e.isDirectory()) continue;
741
+ if (e.name.startsWith(".") || e.name === "node_modules") continue;
742
+ const sub = path.join(dir, e.name);
743
+ const isGroup = e.name.startsWith("(") && e.name.endsWith(")");
744
+ const newSegments = isGroup ? segments : [...segments, e.name];
745
+ walk(sub, newSegments, nextLayouts);
746
+ }
747
+ }
748
+ walk(appDir, [], []);
749
+
750
+ const isParam = (s: string): boolean =>
751
+ s.startsWith("[") && s.endsWith("]");
752
+ pages.sort((a, b) => {
753
+ const minLen = Math.min(a.segments.length, b.segments.length);
754
+ for (let i = 0; i < minLen; i++) {
755
+ const ap = isParam(a.segments[i]);
756
+ const bp = isParam(b.segments[i]);
757
+ if (ap !== bp) return ap ? 1 : -1;
758
+ if (a.segments[i] !== b.segments[i]) {
759
+ return a.segments[i] < b.segments[i] ? -1 : 1;
760
+ }
761
+ }
762
+ return a.segments.length - b.segments.length;
763
+ });
764
+
765
+ return pages.map((p) => ({
766
+ path:
767
+ "/" +
768
+ p.segments
769
+ .map((s) => (isParam(s) ? `:${s.slice(1, -1)}` : s))
770
+ .join("/"),
771
+ mode: "ssr" as const,
772
+ component: p.component,
773
+ layouts: p.layouts,
774
+ }));
775
+ }
776
+
628
777
  export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
629
778
  return queries.map((q) => {
630
779
  const result: ManifestQuery = { name: q.name };