@pylonsync/sdk 0.3.316 → 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 +2 -2
- package/package.json +1 -1
- package/src/index.ts +47 -3
- package/src/route-discovery.test.ts +85 -0
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
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?:
|
|
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?:
|
|
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
|
-
|
|
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
|
+
});
|