@pylonsync/sdk 0.3.317 → 0.3.318

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/dist/index.d.ts CHANGED
@@ -321,7 +321,7 @@ export interface RouteDefinition {
321
321
  * POST/PUT/PATCH/DELETE) — matched on its `path` for non-GET requests only,
322
322
  * never rendered as a page.
323
323
  */
324
- kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
324
+ kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots" | "og-image";
325
325
  }
326
326
  export declare function defineRoute(route: RouteDefinition): RouteDefinition;
327
327
  export interface InputFieldDefinition {
@@ -474,7 +474,7 @@ export interface ManifestRoute {
474
474
  component?: string;
475
475
  layouts?: string[];
476
476
  /** "not-found" / "error" boundaries, or "route" form handlers; omitted for normal pages. */
477
- kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
477
+ kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots" | "og-image";
478
478
  }
479
479
  export interface ManifestInputField {
480
480
  name: string;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.317",
6
+ "version": "0.3.318",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
package/src/index.ts CHANGED
@@ -429,7 +429,14 @@ export interface RouteDefinition {
429
429
  * POST/PUT/PATCH/DELETE) — matched on its `path` for non-GET requests only,
430
430
  * never rendered as a page.
431
431
  */
432
- kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
432
+ kind?:
433
+ | "page"
434
+ | "not-found"
435
+ | "error"
436
+ | "route"
437
+ | "sitemap"
438
+ | "robots"
439
+ | "og-image";
433
440
  }
434
441
 
435
442
  export function defineRoute(route: RouteDefinition): RouteDefinition {
@@ -630,7 +637,14 @@ export interface ManifestRoute {
630
637
  component?: string;
631
638
  layouts?: string[];
632
639
  /** "not-found" / "error" boundaries, or "route" form handlers; omitted for normal pages. */
633
- kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
640
+ kind?:
641
+ | "page"
642
+ | "not-found"
643
+ | "error"
644
+ | "route"
645
+ | "sitemap"
646
+ | "robots"
647
+ | "og-image";
634
648
  }
635
649
 
636
650
  export interface ManifestInputField {
@@ -990,6 +1004,11 @@ export async function discoverAppRoutes(opts?: {
990
1004
  const pages: PageHit[] = [];
991
1005
  const boundaries: BoundaryHit[] = [];
992
1006
  const routeHandlers: RouteHandlerHit[] = [];
1007
+ // `opengraph-image.{tsx,ts,jsx,js}` colocated with any segment. Each
1008
+ // default-exports a function returning an `ImageResponse` (or React
1009
+ // element); the SSR runtime renders it to a PNG on demand and pages under
1010
+ // the segment get an auto `og:image` meta tag pointing at it.
1011
+ const ogImages: RouteHandlerHit[] = [];
993
1012
 
994
1013
  // Resolve the first existing `<base>.{tsx,ts,jsx,js}` in `dir` and
995
1014
  // return it as a cwd-relative, extension-less module path (or null).
@@ -1045,6 +1064,12 @@ export async function discoverAppRoutes(opts?: {
1045
1064
  if (routeHere) {
1046
1065
  routeHandlers.push({ segments: [...segments], component: routeHere });
1047
1066
  }
1067
+ // Dynamic OG image colocated with this segment. Rendered to PNG on
1068
+ // demand; never navigable/hydrated.
1069
+ const ogImageHere = findModule(dir, "opengraph-image");
1070
+ if (ogImageHere) {
1071
+ ogImages.push({ segments: [...segments], component: ogImageHere });
1072
+ }
1048
1073
  for (const e of entries) {
1049
1074
  if (!e.isDirectory()) continue;
1050
1075
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
@@ -1152,7 +1177,26 @@ export async function discoverAppRoutes(opts?: {
1152
1177
  });
1153
1178
  }
1154
1179
 
1155
- return [...pageRoutes, ...boundaryRoutes, ...routeRoutes, ...dataRoutes];
1180
+ // Dynamic OG image routes: `<segment>/opengraph-image` (root → just
1181
+ // `/opengraph-image`). Matched like a page (params flow through) but
1182
+ // served as a PNG by the SSR runtime — never rendered as HTML or hydrated.
1183
+ const ogImageRoutes: RouteDefinition[] = ogImages.map((o) => ({
1184
+ path:
1185
+ o.segments.length === 0
1186
+ ? "/opengraph-image"
1187
+ : `${segmentsToPath(o.segments)}/opengraph-image`,
1188
+ mode: "ssr" as const,
1189
+ component: o.component,
1190
+ kind: "og-image" as const,
1191
+ }));
1192
+
1193
+ return [
1194
+ ...pageRoutes,
1195
+ ...boundaryRoutes,
1196
+ ...routeRoutes,
1197
+ ...dataRoutes,
1198
+ ...ogImageRoutes,
1199
+ ];
1156
1200
  }
1157
1201
 
1158
1202
  export function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[] {
@@ -0,0 +1,85 @@
1
+ // Route discovery (discoverAppRoutes) — the app/ file-convention walk. Focus
2
+ // here is the dynamic OG image convention (app/**/opengraph-image.tsx →
3
+ // kind:"og-image"), which must produce a matchable route at
4
+ // `<segment>/opengraph-image` (root → `/opengraph-image`) alongside the page.
5
+
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ import { discoverAppRoutes } from "./index";
11
+
12
+ describe("discoverAppRoutes — opengraph-image convention", () => {
13
+ const tmpdirs: string[] = [];
14
+ const prevCwd = process.cwd();
15
+ afterEach(() => {
16
+ process.chdir(prevCwd);
17
+ for (const d of tmpdirs.splice(0)) {
18
+ try {
19
+ fs.rmSync(d, { recursive: true, force: true });
20
+ } catch {
21
+ /* best effort */
22
+ }
23
+ }
24
+ });
25
+
26
+ // Materialize an app/ tree (relative file paths → contents) and chdir in.
27
+ function app(files: Record<string, string>): void {
28
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pylon-routes-"));
29
+ tmpdirs.push(root);
30
+ for (const [rel, src] of Object.entries(files)) {
31
+ const abs = path.join(root, "app", rel);
32
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
33
+ fs.writeFileSync(abs, src);
34
+ }
35
+ process.chdir(root);
36
+ }
37
+
38
+ const PAGE = "export default function P(){ return null; }";
39
+ const OG = "export default function OG(){ return null; }";
40
+
41
+ test("root opengraph-image → /opengraph-image kind:og-image", async () => {
42
+ app({ "page.tsx": PAGE, "opengraph-image.tsx": OG });
43
+ const routes = await discoverAppRoutes();
44
+ const og = routes.find((r) => r.path === "/opengraph-image");
45
+ expect(og).toBeDefined();
46
+ expect(og!.kind).toBe("og-image");
47
+ expect(og!.mode).toBe("ssr");
48
+ // The component points at the module (not "/page").
49
+ expect(og!.component).toMatch(/opengraph-image$/);
50
+ });
51
+
52
+ test("nested + dynamic-param segment → concrete pattern path", async () => {
53
+ app({
54
+ "page.tsx": PAGE,
55
+ "blog/[slug]/page.tsx": PAGE,
56
+ "blog/[slug]/opengraph-image.tsx": OG,
57
+ });
58
+ const routes = await discoverAppRoutes();
59
+ const og = routes.find((r) => r.kind === "og-image");
60
+ expect(og).toBeDefined();
61
+ // Dynamic segment tokenizes to `:slug`, matching the page route pattern.
62
+ expect(og!.path).toBe("/blog/:slug/opengraph-image");
63
+ });
64
+
65
+ test("multiple opengraph-images coexist (root default + per-section)", async () => {
66
+ app({
67
+ "page.tsx": PAGE,
68
+ "opengraph-image.tsx": OG,
69
+ "shop/page.tsx": PAGE,
70
+ "shop/opengraph-image.tsx": OG,
71
+ });
72
+ const routes = await discoverAppRoutes();
73
+ const ogPaths = routes
74
+ .filter((r) => r.kind === "og-image")
75
+ .map((r) => r.path)
76
+ .sort();
77
+ expect(ogPaths).toEqual(["/opengraph-image", "/shop/opengraph-image"]);
78
+ });
79
+
80
+ test("no opengraph-image → no og-image routes", async () => {
81
+ app({ "page.tsx": PAGE });
82
+ const routes = await discoverAppRoutes();
83
+ expect(routes.some((r) => r.kind === "og-image")).toBe(false);
84
+ });
85
+ });