@yak-io/nextjs 0.7.1 → 0.8.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.
@@ -1,2 +1,335 @@
1
- // Re-export everything from server/index.ts
2
- export * from "./server/index.js";
1
+ // src/server/createNextYakHandler.ts
2
+ import * as fs2 from "node:fs";
3
+ import * as path2 from "node:path";
4
+ import {
5
+ createYakConfigHandler,
6
+ createYakHandler,
7
+ createYakToolsHandler
8
+ } from "@yak-io/javascript/server";
9
+
10
+ // src/server/scan-routes.ts
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ function extractMetadata(filePath) {
14
+ try {
15
+ const content = fs.readFileSync(filePath, "utf-8");
16
+ const metadataMatch = content.match(
17
+ /export\s+const\s+metadata\s*(?::\s*Metadata\s*)?=\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/s
18
+ );
19
+ if (!metadataMatch) {
20
+ return {};
21
+ }
22
+ const metadataBlock = metadataMatch[1];
23
+ const result = {};
24
+ const titleMatch = metadataBlock?.match(/title\s*:\s*["'`]([^"'`]+)["'`]/);
25
+ if (titleMatch?.[1]) {
26
+ result.title = titleMatch[1];
27
+ }
28
+ const descMatch = metadataBlock?.match(/description\s*:\s*["'`]([^"'`]+)["'`]/);
29
+ if (descMatch?.[1]) {
30
+ result.description = descMatch[1];
31
+ }
32
+ return result;
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+ function isRouteGroup(segment) {
38
+ return segment.startsWith("(") && segment.endsWith(")");
39
+ }
40
+ function isOptionalCatchAll(segment) {
41
+ return segment.startsWith("[[...") && segment.endsWith("]]");
42
+ }
43
+ function isRequiredCatchAll(segment) {
44
+ return segment.startsWith("[...") && segment.endsWith("]") && !segment.startsWith("[[");
45
+ }
46
+ function isDynamicSegment(segment) {
47
+ return segment.startsWith("[") && segment.endsWith("]") && !isOptionalCatchAll(segment) && !isRequiredCatchAll(segment);
48
+ }
49
+ function normalizeDynamicSegment(segment) {
50
+ const paramName = segment.slice(1, -1);
51
+ return `:${paramName}`;
52
+ }
53
+ function normalizeRoutePath(segments) {
54
+ const urlSegments = segments.filter((seg) => !isRouteGroup(seg) && !isOptionalCatchAll(seg)).map((seg) => {
55
+ if (isDynamicSegment(seg)) {
56
+ return normalizeDynamicSegment(seg);
57
+ }
58
+ if (isRequiredCatchAll(seg)) {
59
+ const paramName = seg.slice(4, -1);
60
+ return `:${paramName}+`;
61
+ }
62
+ return seg;
63
+ });
64
+ if (urlSegments.length === 0) return "/";
65
+ return `/${urlSegments.join("/")}`;
66
+ }
67
+ function isPageFile(filename) {
68
+ return filename === "page.tsx" || filename === "page.js";
69
+ }
70
+ function scanAppDirectory(dirPath, segments = []) {
71
+ const routes = [];
72
+ try {
73
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
74
+ for (const entry of entries) {
75
+ const fullPath = path.join(dirPath, entry.name);
76
+ if (entry.isDirectory()) {
77
+ if (entry.name.startsWith("_") || entry.name === "api") {
78
+ continue;
79
+ }
80
+ routes.push(...scanAppDirectory(fullPath, [...segments, entry.name]));
81
+ } else if (entry.isFile() && isPageFile(entry.name)) {
82
+ const metadata = extractMetadata(fullPath);
83
+ routes.push({
84
+ path: normalizeRoutePath(segments),
85
+ title: metadata.title,
86
+ description: metadata.description
87
+ });
88
+ }
89
+ }
90
+ } catch (error) {
91
+ console.error(`Error scanning directory ${dirPath}:`, error);
92
+ }
93
+ return routes;
94
+ }
95
+ var VALID_PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]);
96
+ var SPECIAL_PAGE_FILENAMES = /* @__PURE__ */ new Set([
97
+ "_app",
98
+ "_document",
99
+ "_error",
100
+ "404",
101
+ "500",
102
+ "middleware",
103
+ "_middleware"
104
+ ]);
105
+ function scanPagesDirectory(dirPath, segments = []) {
106
+ const routes = [];
107
+ try {
108
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
109
+ for (const entry of entries) {
110
+ const fullPath = path.join(dirPath, entry.name);
111
+ if (entry.isDirectory()) {
112
+ if (entry.name.startsWith("_") || entry.name === "api") {
113
+ continue;
114
+ }
115
+ routes.push(...scanPagesDirectory(fullPath, [...segments, entry.name]));
116
+ } else if (entry.isFile()) {
117
+ const extension = path.extname(entry.name);
118
+ if (!VALID_PAGE_EXTENSIONS.has(extension)) {
119
+ continue;
120
+ }
121
+ const baseName = entry.name.slice(0, -extension.length);
122
+ if (!baseName || SPECIAL_PAGE_FILENAMES.has(baseName) || baseName.startsWith("_")) {
123
+ continue;
124
+ }
125
+ const routeSegments = buildPagesSegments(segments, baseName);
126
+ const metadata = extractMetadata(fullPath);
127
+ routes.push({
128
+ path: normalizeRoutePath(routeSegments),
129
+ title: metadata.title,
130
+ description: metadata.description
131
+ });
132
+ }
133
+ }
134
+ } catch (error) {
135
+ console.error(`Error scanning directory ${dirPath}:`, error);
136
+ }
137
+ return routes;
138
+ }
139
+ function buildPagesSegments(segments, baseName) {
140
+ const routeSegments = [...segments];
141
+ if (baseName !== "index") {
142
+ routeSegments.push(baseName);
143
+ }
144
+ return routeSegments;
145
+ }
146
+ function normalizeScanOptions(options) {
147
+ return {
148
+ directoryType: options?.directoryType ?? "app"
149
+ };
150
+ }
151
+ function scanRoutes(directory, options) {
152
+ const normalizedOptions = normalizeScanOptions(options);
153
+ const targetDir = path.resolve(process.cwd(), directory);
154
+ if (!fs.existsSync(targetDir)) {
155
+ throw new Error(`App directory not found: ${targetDir}`);
156
+ }
157
+ const routes = normalizedOptions.directoryType === "pages" ? scanPagesDirectory(targetDir, []) : scanAppDirectory(targetDir, []);
158
+ return routes.sort((a, b) => a.path.localeCompare(b.path));
159
+ }
160
+
161
+ // src/server/createNextYakHandler.ts
162
+ var DEFAULT_MANIFEST_PATHS = [
163
+ "./src/yak-routes-manifest.json",
164
+ "./yak-routes-manifest.json",
165
+ "./public/yak-routes-manifest.json"
166
+ ];
167
+ function loadRouteManifest(manifestPath) {
168
+ const pathsToTry = manifestPath ? [manifestPath] : [...DEFAULT_MANIFEST_PATHS];
169
+ for (const relativePath of pathsToTry) {
170
+ try {
171
+ const fullPath = path2.resolve(process.cwd(), relativePath);
172
+ if (fs2.existsSync(fullPath)) {
173
+ const content = fs2.readFileSync(fullPath, "utf-8");
174
+ return JSON.parse(content);
175
+ }
176
+ } catch {
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+ function loadRoutes(manifestPath) {
182
+ const manifest = loadRouteManifest(manifestPath);
183
+ if (!manifest) {
184
+ const pathsChecked = manifestPath ? manifestPath : [...DEFAULT_MANIFEST_PATHS].join(", ");
185
+ throw new Error(
186
+ `Route manifest not found. Checked: ${pathsChecked}
187
+
188
+ In production environments (like Vercel), route scanning requires a pre-built manifest.
189
+ Generate it at build time by adding to your build script:
190
+
191
+ yak-nextjs generate-manifest
192
+
193
+ Or provide routes explicitly in your handler configuration.`
194
+ );
195
+ }
196
+ return manifest.routes;
197
+ }
198
+ function createNextYakHandler(config) {
199
+ return createYakHandler({
200
+ routes: resolveRouteSources(config),
201
+ tools: resolveToolSources(config)
202
+ });
203
+ }
204
+ async function tryLoadRoutes(appDir) {
205
+ const targetDir = path2.resolve(process.cwd(), appDir);
206
+ if (fs2.existsSync(targetDir)) {
207
+ return scanRoutes(appDir);
208
+ }
209
+ const manifest = loadRouteManifest();
210
+ if (manifest) {
211
+ return manifest.routes;
212
+ }
213
+ throw new Error(
214
+ `Route manifest not found.
215
+
216
+ Generate the manifest at build time by adding to package.json:
217
+ "prebuild": "yak-nextjs generate-manifest"
218
+
219
+ The manifest will be created at ./src/yak-routes-manifest.json and automatically
220
+ bundled with your serverless function.
221
+
222
+ Alternatively, provide routes explicitly:
223
+ createNextYakHandler({ routes: [{ path: "/", title: "Home" }] })`
224
+ );
225
+ }
226
+ function resolveRouteSources(config) {
227
+ if (config.routes) {
228
+ return config.routes;
229
+ }
230
+ if (config.getRoutes) {
231
+ return config.getRoutes;
232
+ }
233
+ return async () => {
234
+ const routes = await tryLoadRoutes(config.appDir ?? "./src/app");
235
+ return applyRouteFilters(routes, config.routeFilter);
236
+ };
237
+ }
238
+ function applyRouteFilters(routes, filter) {
239
+ if (!filter) {
240
+ return routes;
241
+ }
242
+ const { include = [], exclude = [] } = filter;
243
+ return routes.filter((route) => {
244
+ const path3 = route.path;
245
+ if (include.length > 0 && !include.some((pattern) => matches(pattern, path3))) {
246
+ return false;
247
+ }
248
+ if (exclude.some((pattern) => matches(pattern, path3))) {
249
+ return false;
250
+ }
251
+ return true;
252
+ });
253
+ }
254
+ function matches(pattern, value) {
255
+ pattern.lastIndex = 0;
256
+ return pattern.test(value);
257
+ }
258
+ function resolveToolSources(config) {
259
+ if (config.tools) {
260
+ return config.tools;
261
+ }
262
+ if (config.getTools) {
263
+ return {
264
+ getTools: config.getTools,
265
+ executeTool: config.executeTool
266
+ };
267
+ }
268
+ if (config.executeTool) {
269
+ throw new Error("'executeTool' was provided without a matching tool manifest source");
270
+ }
271
+ return void 0;
272
+ }
273
+ function createNextYakConfigHandler(config) {
274
+ return createYakConfigHandler({
275
+ routes: resolveRouteSources(config),
276
+ tools: config.tools ?? (config.getTools ? { getTools: config.getTools } : void 0)
277
+ });
278
+ }
279
+ function createNextYakToolsHandler(config) {
280
+ if (!config.tools && !config.getTools) {
281
+ throw new Error("createNextYakToolsHandler requires either 'tools' or 'getTools'");
282
+ }
283
+ const toolSource = config.tools ?? {
284
+ // getTools is guaranteed to be defined here since we check above
285
+ getTools: config.getTools,
286
+ executeTool: config.executeTool
287
+ };
288
+ return createYakToolsHandler({ tools: toolSource });
289
+ }
290
+
291
+ // src/server/route-manifest-adapter.ts
292
+ function matchesPattern(pattern, routePath) {
293
+ if (pattern.endsWith("/*")) {
294
+ const prefix = pattern.slice(0, -1);
295
+ return routePath === prefix.slice(0, -1) || routePath.startsWith(prefix);
296
+ }
297
+ return pattern === routePath;
298
+ }
299
+ function filterRoutes(routes, allowedRoutes, disallowedRoutes) {
300
+ return routes.filter((route) => {
301
+ const path3 = route.path;
302
+ if (allowedRoutes && allowedRoutes.length > 0) {
303
+ const isAllowed = allowedRoutes.some((pattern) => matchesPattern(pattern, path3));
304
+ if (!isAllowed) {
305
+ return false;
306
+ }
307
+ }
308
+ if (disallowedRoutes && disallowedRoutes.length > 0) {
309
+ const isDisallowed = disallowedRoutes.some((pattern) => matchesPattern(pattern, path3));
310
+ if (isDisallowed) {
311
+ return false;
312
+ }
313
+ }
314
+ return true;
315
+ });
316
+ }
317
+ function createRouteManifestAdapter(config) {
318
+ const { routes, id = "manifest", allowedRoutes, disallowedRoutes } = config;
319
+ return {
320
+ id,
321
+ getRoutes: async () => {
322
+ return filterRoutes(routes, allowedRoutes, disallowedRoutes);
323
+ }
324
+ };
325
+ }
326
+ export {
327
+ createNextYakConfigHandler,
328
+ createNextYakHandler,
329
+ createNextYakToolsHandler,
330
+ createRouteManifestAdapter,
331
+ loadRouteManifest,
332
+ loadRoutes,
333
+ scanRoutes
334
+ };
335
+ //# sourceMappingURL=index.server.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server/createNextYakHandler.ts", "../src/server/scan-routes.ts", "../src/server/route-manifest-adapter.ts"],
4
+ "sourcesContent": ["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type {\n RouteInfo,\n RouteManifest,\n RouteSourceInput,\n ToolExecutor,\n ToolManifest,\n ToolSourceInput,\n} from \"@yak-io/javascript/server\";\nimport {\n createYakConfigHandler,\n createYakHandler,\n createYakToolsHandler,\n} from \"@yak-io/javascript/server\";\nimport { scanRoutes } from \"./scan-routes.js\";\n\n/**\n * Default paths to check for pre-built route manifests (JSON files).\n * These are read via fs.readFileSync at runtime.\n *\n * Priority:\n * 1. ./src/yak-routes-manifest.json - Default output, bundled by Next.js file tracing\n * 2. ./yak-routes-manifest.json - Root fallback\n * 3. ./public/yak-routes-manifest.json - Legacy location (doesn't work on Vercel serverless)\n */\nconst DEFAULT_MANIFEST_PATHS = [\n \"./src/yak-routes-manifest.json\",\n \"./yak-routes-manifest.json\",\n \"./public/yak-routes-manifest.json\",\n] as const;\n\n/**\n * Load a pre-built route manifest from disk (JSON file).\n *\n * This works in traditional Node.js deployments where the filesystem is available.\n * For Vercel serverless, the manifest must be imported directly in your route file.\n *\n * Generate the manifest at build time using:\n * yak-nextjs generate-manifest\n *\n * @param manifestPath - Path to the manifest JSON file (default: tries common locations)\n * @returns Route manifest or null if not found\n */\nexport function loadRouteManifest(manifestPath?: string): RouteManifest | null {\n const pathsToTry = manifestPath ? [manifestPath] : [...DEFAULT_MANIFEST_PATHS];\n\n for (const relativePath of pathsToTry) {\n try {\n const fullPath = path.resolve(process.cwd(), relativePath);\n if (fs.existsSync(fullPath)) {\n const content = fs.readFileSync(fullPath, \"utf-8\");\n return JSON.parse(content) as RouteManifest;\n }\n } catch {\n // Continue to next path\n }\n }\n\n return null;\n}\n\n/**\n * Load routes from a pre-built manifest.\n * Throws if manifest is not found.\n */\nexport function loadRoutes(manifestPath?: string): RouteInfo[] {\n const manifest = loadRouteManifest(manifestPath);\n if (!manifest) {\n const pathsChecked = manifestPath ? manifestPath : [...DEFAULT_MANIFEST_PATHS].join(\", \");\n throw new Error(\n `Route manifest not found. Checked: ${pathsChecked}\\n\\n` +\n \"In production environments (like Vercel), route scanning requires a pre-built manifest.\\n\" +\n \"Generate it at build time by adding to your build script:\\n\\n\" +\n \" yak-nextjs generate-manifest\\n\\n\" +\n \"Or provide routes explicitly in your handler configuration.\"\n );\n }\n return manifest.routes;\n}\n\nexport type NextYakRouteFilter = {\n include?: RegExp[];\n exclude?: RegExp[];\n};\n\nexport type NextYakHandlerConfig = {\n appDir?: string;\n routeFilter?: NextYakRouteFilter;\n routes?: RouteSourceInput;\n getRoutes?: () => Promise<RouteInfo[]>;\n tools?: ToolSourceInput;\n pagesDir?: string | false;\n getTools?: () => Promise<ToolManifest>;\n executeTool?: ToolExecutor;\n};\n\n/**\n * Create a unified Next.js App Router handler backed by the core primitives\n */\nexport function createNextYakHandler(config: NextYakHandlerConfig) {\n return createYakHandler({\n routes: resolveRouteSources(config),\n tools: resolveToolSources(config),\n });\n}\n\n/**\n * Attempt to load routes from filesystem or fetch from public URL.\n *\n * In development: scans the app directory directly\n * In production (Vercel, etc.): falls back to pre-built manifest via filesystem or HTTP fetch\n */\nasync function tryLoadRoutes(appDir: string): Promise<RouteInfo[]> {\n const targetDir = path.resolve(process.cwd(), appDir);\n\n // If the app directory exists, scan it directly (development mode)\n if (fs.existsSync(targetDir)) {\n return scanRoutes(appDir);\n }\n\n // In production, the source directory doesn't exist - try loading from manifest file\n const manifest = loadRouteManifest();\n if (manifest) {\n return manifest.routes;\n }\n\n // Neither source nor manifest available - provide helpful error\n throw new Error(\n \"Route manifest not found.\\n\\n\" +\n \"Generate the manifest at build time by adding to package.json:\\n\" +\n ` \"prebuild\": \"yak-nextjs generate-manifest\"\\n\\n` +\n \"The manifest will be created at ./src/yak-routes-manifest.json and automatically\\n\" +\n \"bundled with your serverless function.\\n\\n\" +\n \"Alternatively, provide routes explicitly:\\n\" +\n ` createNextYakHandler({ routes: [{ path: \"/\", title: \"Home\" }] })`\n );\n}\n\nfunction resolveRouteSources(config: NextYakHandlerConfig): RouteSourceInput {\n if (config.routes) {\n return config.routes;\n }\n if (config.getRoutes) {\n return config.getRoutes;\n }\n return async () => {\n const routes = await tryLoadRoutes(config.appDir ?? \"./src/app\");\n return applyRouteFilters(routes, config.routeFilter);\n };\n}\n\nfunction applyRouteFilters(routes: RouteInfo[], filter?: NextYakRouteFilter): RouteInfo[] {\n if (!filter) {\n return routes;\n }\n\n const { include = [], exclude = [] } = filter;\n\n return routes.filter((route) => {\n const path = route.path;\n\n if (include.length > 0 && !include.some((pattern) => matches(pattern, path))) {\n return false;\n }\n\n if (exclude.some((pattern) => matches(pattern, path))) {\n return false;\n }\n\n return true;\n });\n}\n\nfunction matches(pattern: RegExp, value: string): boolean {\n pattern.lastIndex = 0;\n return pattern.test(value);\n}\n\nfunction resolveToolSources(config: NextYakHandlerConfig): ToolSourceInput | undefined {\n if (config.tools) {\n return config.tools;\n }\n if (config.getTools) {\n return {\n getTools: config.getTools,\n executeTool: config.executeTool,\n } satisfies ToolSourceInput;\n }\n if (config.executeTool) {\n throw new Error(\"'executeTool' was provided without a matching tool manifest source\");\n }\n return undefined;\n}\n\nexport type NextYakConfigHandlerConfig = {\n appDir?: string;\n routeFilter?: NextYakRouteFilter;\n routes?: RouteSourceInput;\n getRoutes?: () => Promise<RouteInfo[]>;\n tools?: ToolSourceInput;\n getTools?: () => Promise<ToolManifest>;\n};\n\nexport function createNextYakConfigHandler(config: NextYakConfigHandlerConfig) {\n return createYakConfigHandler({\n routes: resolveRouteSources(config as NextYakHandlerConfig),\n tools: config.tools ?? (config.getTools ? { getTools: config.getTools } : undefined),\n });\n}\n\nexport type NextYakToolsHandlerConfig = {\n tools?: ToolSourceInput;\n getTools?: () => Promise<ToolManifest>;\n executeTool: ToolExecutor;\n};\n\nexport function createNextYakToolsHandler(config: NextYakToolsHandlerConfig) {\n if (!config.tools && !config.getTools) {\n throw new Error(\"createNextYakToolsHandler requires either 'tools' or 'getTools'\");\n }\n\n const toolSource: ToolSourceInput = config.tools ?? {\n // getTools is guaranteed to be defined here since we check above\n getTools: config.getTools as NonNullable<typeof config.getTools>,\n executeTool: config.executeTool,\n };\n\n return createYakToolsHandler({ tools: toolSource });\n}\n", "import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { RouteInfo } from \"@yak-io/javascript/server\";\n\nexport type ScanRoutesOptions = {\n directoryType?: \"app\" | \"pages\";\n};\n\n/**\n * Extract static metadata (title and description) from a Next.js page file.\n * Parses `export const metadata = { title: \"...\", description: \"...\" }` patterns.\n * Does not support dynamic generateMetadata functions.\n */\nfunction extractMetadata(filePath: string): { title?: string; description?: string } {\n try {\n const content = fs.readFileSync(filePath, \"utf-8\");\n\n // Match `export const metadata` object\n const metadataMatch = content.match(\n /export\\s+const\\s+metadata\\s*(?::\\s*Metadata\\s*)?=\\s*\\{([^}]*(?:\\{[^}]*\\}[^}]*)*)\\}/s\n );\n\n if (!metadataMatch) {\n return {};\n }\n\n const metadataBlock = metadataMatch[1];\n const result: { title?: string; description?: string } = {};\n\n // Extract title - handle both single and double quotes\n const titleMatch = metadataBlock?.match(/title\\s*:\\s*[\"'`]([^\"'`]+)[\"'`]/);\n if (titleMatch?.[1]) {\n result.title = titleMatch[1];\n }\n\n // Extract description - handle both single and double quotes\n const descMatch = metadataBlock?.match(/description\\s*:\\s*[\"'`]([^\"'`]+)[\"'`]/);\n if (descMatch?.[1]) {\n result.description = descMatch[1];\n }\n\n return result;\n } catch {\n return {};\n }\n}\n\n/**\n * Check if a path segment is a route group (organizational only, not part of URL)\n */\nfunction isRouteGroup(segment: string): boolean {\n return segment.startsWith(\"(\") && segment.endsWith(\")\");\n}\n\n/**\n * Check if a path segment is an optional catch-all (e.g., [[...slug]])\n * These segments match the base path without any additional segments\n */\nfunction isOptionalCatchAll(segment: string): boolean {\n return segment.startsWith(\"[[...\") && segment.endsWith(\"]]\");\n}\n\n/**\n * Check if a path segment is a required catch-all (e.g., [...slug])\n * These segments require at least one additional path segment\n */\nfunction isRequiredCatchAll(segment: string): boolean {\n return segment.startsWith(\"[...\") && segment.endsWith(\"]\") && !segment.startsWith(\"[[\");\n}\n\n/**\n * Check if a path segment is a dynamic segment (e.g., [id])\n */\nfunction isDynamicSegment(segment: string): boolean {\n return (\n segment.startsWith(\"[\") &&\n segment.endsWith(\"]\") &&\n !isOptionalCatchAll(segment) &&\n !isRequiredCatchAll(segment)\n );\n}\n\n/**\n * Normalize a dynamic segment for display\n * e.g., [id] \u2192 :id, [postId] \u2192 :postId\n */\nfunction normalizeDynamicSegment(segment: string): string {\n // Extract the parameter name from [name] format\n const paramName = segment.slice(1, -1);\n return `:${paramName}`;\n}\n\n/**\n * Normalize a route path for display (excludes route groups and handles dynamic segments)\n */\nfunction normalizeRoutePath(segments: string[]): string {\n // Filter out route groups - they don't appear in URLs\n // Filter out optional catch-all segments - they match the base path\n const urlSegments = segments\n .filter((seg) => !isRouteGroup(seg) && !isOptionalCatchAll(seg))\n .map((seg) => {\n if (isDynamicSegment(seg)) {\n return normalizeDynamicSegment(seg);\n }\n if (isRequiredCatchAll(seg)) {\n // For required catch-all, extract the name: [...slug] \u2192 :slug+\n const paramName = seg.slice(4, -1);\n return `:${paramName}+`;\n }\n return seg;\n });\n if (urlSegments.length === 0) return \"/\";\n return `/${urlSegments.join(\"/\")}`;\n}\n\n/**\n * Check if file is a page file\n */\nfunction isPageFile(filename: string): boolean {\n return filename === \"page.tsx\" || filename === \"page.js\";\n}\n\n/**\n * Recursively scan a directory for Next.js page routes\n */\nfunction scanAppDirectory(dirPath: string, segments: string[] = []): RouteInfo[] {\n const routes: RouteInfo[] = [];\n\n try {\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Skip special Next.js directories and api routes\n if (entry.name.startsWith(\"_\") || entry.name === \"api\") {\n continue;\n }\n\n // Recursively scan subdirectories\n routes.push(...scanAppDirectory(fullPath, [...segments, entry.name]));\n } else if (entry.isFile() && isPageFile(entry.name)) {\n const metadata = extractMetadata(fullPath);\n\n routes.push({\n path: normalizeRoutePath(segments),\n title: metadata.title,\n description: metadata.description,\n });\n }\n }\n } catch (error) {\n console.error(`Error scanning directory ${dirPath}:`, error);\n }\n\n return routes;\n}\n\n/**\n * File discovery helpers for legacy `pages/` directories\n */\nconst VALID_PAGE_EXTENSIONS = new Set([\".js\", \".jsx\", \".ts\", \".tsx\", \".md\", \".mdx\"]);\nconst SPECIAL_PAGE_FILENAMES = new Set([\n \"_app\",\n \"_document\",\n \"_error\",\n \"404\",\n \"500\",\n \"middleware\",\n \"_middleware\",\n]);\n\nfunction scanPagesDirectory(dirPath: string, segments: string[] = []): RouteInfo[] {\n const routes: RouteInfo[] = [];\n\n try {\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Skip special directories and api routes\n if (entry.name.startsWith(\"_\") || entry.name === \"api\") {\n continue;\n }\n\n routes.push(...scanPagesDirectory(fullPath, [...segments, entry.name]));\n } else if (entry.isFile()) {\n const extension = path.extname(entry.name);\n if (!VALID_PAGE_EXTENSIONS.has(extension)) {\n continue;\n }\n\n const baseName = entry.name.slice(0, -extension.length);\n if (!baseName || SPECIAL_PAGE_FILENAMES.has(baseName) || baseName.startsWith(\"_\")) {\n continue;\n }\n\n const routeSegments = buildPagesSegments(segments, baseName);\n const metadata = extractMetadata(fullPath);\n\n routes.push({\n path: normalizeRoutePath(routeSegments),\n title: metadata.title,\n description: metadata.description,\n });\n }\n }\n } catch (error) {\n console.error(`Error scanning directory ${dirPath}:`, error);\n }\n\n return routes;\n}\n\nfunction buildPagesSegments(segments: string[], baseName: string): string[] {\n const routeSegments = [...segments];\n if (baseName !== \"index\") {\n routeSegments.push(baseName);\n }\n return routeSegments;\n}\n\ntype NormalizedScanOptions = {\n directoryType: \"app\" | \"pages\";\n};\n\nfunction normalizeScanOptions(options?: ScanRoutesOptions): NormalizedScanOptions {\n return {\n directoryType: options?.directoryType ?? \"app\",\n };\n}\n\n/**\n * Scan a Next.js app or pages directory and return page route information\n */\nexport function scanRoutes(directory: string, options?: ScanRoutesOptions): RouteInfo[] {\n const normalizedOptions = normalizeScanOptions(options);\n const targetDir = path.resolve(process.cwd(), directory);\n\n if (!fs.existsSync(targetDir)) {\n throw new Error(`App directory not found: ${targetDir}`);\n }\n\n const routes =\n normalizedOptions.directoryType === \"pages\"\n ? scanPagesDirectory(targetDir, [])\n : scanAppDirectory(targetDir, []);\n\n return routes.sort((a: RouteInfo, b: RouteInfo) => a.path.localeCompare(b.path));\n}\n", "import type { RouteInfo, RouteSource } from \"@yak-io/javascript/server\";\n\n/**\n * Configuration for creating a route manifest adapter\n */\nexport type RouteManifestAdapterConfig = {\n /**\n * The routes from the generated manifest.\n * Import from the generated file: `import { routes } from \"@/yak.routes\"`\n */\n routes: RouteInfo[];\n\n /**\n * Optional ID for this route source (default: \"manifest\")\n */\n id?: string;\n\n /**\n * List of allowed route paths, e.g. [\"/\", \"/docs\", \"/pricing\"].\n * Supports exact matches and prefix patterns ending with `*`.\n * If provided, only these routes will be included.\n * If omitted, all routes are allowed by default.\n *\n * @example\n * ```ts\n * allowedRoutes: [\"/docs/*\", \"/pricing\", \"/\"]\n * ```\n */\n allowedRoutes?: string[];\n\n /**\n * List of disallowed route paths, e.g. [\"/admin/*\", \"/internal/*\"].\n * Supports exact matches and prefix patterns ending with `*`.\n * These routes will be excluded even if they would otherwise be allowed.\n * Applied after allowedRoutes filtering.\n *\n * @example\n * ```ts\n * disallowedRoutes: [\"/admin/*\", \"/settings/billing\"]\n * ```\n */\n disallowedRoutes?: string[];\n};\n\n/**\n * Check if a route path matches a pattern.\n * Supports exact matches and prefix patterns ending with `*`.\n */\nfunction matchesPattern(pattern: string, routePath: string): boolean {\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, -1); // Remove the *\n return routePath === prefix.slice(0, -1) || routePath.startsWith(prefix);\n }\n return pattern === routePath;\n}\n\n/**\n * Filter routes based on allowed and disallowed patterns.\n */\nfunction filterRoutes(\n routes: RouteInfo[],\n allowedRoutes?: string[],\n disallowedRoutes?: string[]\n): RouteInfo[] {\n return routes.filter((route) => {\n const path = route.path;\n\n // If allowedRoutes is set, route must match at least one pattern\n if (allowedRoutes && allowedRoutes.length > 0) {\n const isAllowed = allowedRoutes.some((pattern) => matchesPattern(pattern, path));\n if (!isAllowed) {\n return false;\n }\n }\n\n // If disallowedRoutes is set, route must not match any pattern\n if (disallowedRoutes && disallowedRoutes.length > 0) {\n const isDisallowed = disallowedRoutes.some((pattern) => matchesPattern(pattern, path));\n if (isDisallowed) {\n return false;\n }\n }\n\n return true;\n });\n}\n\n/**\n * Create a route source adapter from an imported route manifest.\n *\n * This adapter allows you to use generated route manifests with optional\n * filtering to control which routes are exposed to the chatbot.\n *\n * @example Basic usage - all routes\n * ```ts\n * import { createNextYakHandler } from \"@yak-io/nextjs/server\";\n * import { createRouteManifestAdapter } from \"@yak-io/nextjs/server\";\n * import { routes } from \"@/yak.routes\";\n *\n * export const { GET, POST } = createNextYakHandler({\n * routes: createRouteManifestAdapter({ routes }),\n * });\n * ```\n *\n * @example With filtering\n * ```ts\n * import { createNextYakHandler } from \"@yak-io/nextjs/server\";\n * import { createRouteManifestAdapter } from \"@yak-io/nextjs/server\";\n * import { routes } from \"@/yak.routes\";\n *\n * export const { GET, POST } = createNextYakHandler({\n * routes: createRouteManifestAdapter({\n * routes,\n * allowedRoutes: [\"/docs/*\", \"/pricing\", \"/\"],\n * disallowedRoutes: [\"/docs/internal/*\"],\n * }),\n * });\n * ```\n */\nexport function createRouteManifestAdapter(config: RouteManifestAdapterConfig): RouteSource {\n const { routes, id = \"manifest\", allowedRoutes, disallowedRoutes } = config;\n\n return {\n id,\n getRoutes: async () => {\n return filterRoutes(routes, allowedRoutes, disallowedRoutes);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["fs", "path", "path", "path"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yak-io/nextjs",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Next.js SDK for embedding yak chatbot with route manifest generation",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -31,18 +31,20 @@
31
31
  "LICENSE"
32
32
  ],
33
33
  "sideEffects": false,
34
- "main": "./dist/index.client.js",
34
+ "main": "./dist/index.client.cjs",
35
35
  "module": "./dist/index.client.js",
36
36
  "types": "./dist/index.client.d.ts",
37
37
  "exports": {
38
38
  "./client": {
39
39
  "types": "./dist/index.client.d.ts",
40
40
  "import": "./dist/index.client.js",
41
+ "require": "./dist/index.client.cjs",
41
42
  "default": "./dist/index.client.js"
42
43
  },
43
44
  "./server": {
44
45
  "types": "./dist/index.server.d.ts",
45
46
  "import": "./dist/index.server.js",
47
+ "require": "./dist/index.server.cjs",
46
48
  "default": "./dist/index.server.js"
47
49
  },
48
50
  "./package.json": "./package.json"
@@ -61,8 +63,8 @@
61
63
  "yak-nextjs": "./dist/cli/generate-manifest.js"
62
64
  },
63
65
  "dependencies": {
64
- "@yak-io/javascript": "0.10.1",
65
- "@yak-io/react": "0.11.1"
66
+ "@yak-io/javascript": "0.11.0",
67
+ "@yak-io/react": "0.12.0"
66
68
  },
67
69
  "peerDependencies": {
68
70
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -81,7 +83,7 @@
81
83
  },
82
84
  "homepage": "https://docs.yak.io/docs/sdks/nextjs",
83
85
  "scripts": {
84
- "build": "tsc",
86
+ "build": "node ../../scripts/build-package.mjs && tsc --emitDeclarationOnly",
85
87
  "check-types": "tsc --noEmit",
86
88
  "test": "vitest run",
87
89
  "lint": "biome lint ./src --fix",
@@ -1,57 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- import { YakProvider as CoreYakProvider, } from "@yak-io/react";
4
- import { useRouter } from "next/navigation";
5
- import { useCallback, useMemo } from "react";
6
- function isAbsoluteUrl(path) {
7
- return /^https?:\/\//i.test(path);
8
- }
9
- /**
10
- * Default getConfig that fetches from /api/yak
11
- */
12
- async function defaultGetConfig() {
13
- const res = await fetch("/api/yak", { credentials: "include" });
14
- if (!res.ok) {
15
- throw new Error(`Failed to fetch config: ${res.status}`);
16
- }
17
- return res.json();
18
- }
19
- /**
20
- * Default onToolCall that POSTs to /api/yak
21
- */
22
- async function defaultOnToolCall(name, args) {
23
- const res = await fetch("/api/yak", {
24
- method: "POST",
25
- headers: { "Content-Type": "application/json" },
26
- credentials: "include",
27
- body: JSON.stringify({ name, args }),
28
- });
29
- const data = await res.json();
30
- if (!data.ok) {
31
- throw new Error(data.error ?? "Tool execution failed");
32
- }
33
- return data.result;
34
- }
35
- /**
36
- * Next-aware YakProvider that falls back to client-side navigation
37
- * and provides sensible defaults for getConfig and onToolCall.
38
- *
39
- * By default:
40
- * - `getConfig` fetches from `/api/yak` (GET)
41
- * - `onToolCall` POSTs to `/api/yak`
42
- *
43
- * These defaults assume you've set up the API handler via `createNextYakHandler`.
44
- */
45
- export function YakProvider(props) {
46
- const router = useRouter();
47
- const handleRedirect = useCallback((path) => {
48
- if (isAbsoluteUrl(path)) {
49
- window.location.assign(path);
50
- return;
51
- }
52
- router.push(path);
53
- }, [router]);
54
- const getConfig = useMemo(() => props.getConfig ?? defaultGetConfig, [props.getConfig]);
55
- const onToolCall = useMemo(() => props.onToolCall ?? defaultOnToolCall, [props.onToolCall]);
56
- return (_jsx(CoreYakProvider, { ...props, getConfig: getConfig, onToolCall: onToolCall, onRedirect: props.onRedirect ?? handleRedirect }));
57
- }
@@ -1,4 +0,0 @@
1
- "use client";
2
- import { useYak } from "@yak-io/react";
3
- // Re-export useYak for convenience
4
- export { useYak };
@@ -1 +0,0 @@
1
- export { createNextYakConfigHandler } from "./createNextYakHandler.js";
@@ -1,161 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { createYakConfigHandler, createYakHandler, createYakToolsHandler, } from "@yak-io/javascript/server";
4
- import { scanRoutes } from "./scan-routes.js";
5
- /**
6
- * Default paths to check for pre-built route manifests (JSON files).
7
- * These are read via fs.readFileSync at runtime.
8
- *
9
- * Priority:
10
- * 1. ./src/yak-routes-manifest.json - Default output, bundled by Next.js file tracing
11
- * 2. ./yak-routes-manifest.json - Root fallback
12
- * 3. ./public/yak-routes-manifest.json - Legacy location (doesn't work on Vercel serverless)
13
- */
14
- const DEFAULT_MANIFEST_PATHS = [
15
- "./src/yak-routes-manifest.json",
16
- "./yak-routes-manifest.json",
17
- "./public/yak-routes-manifest.json",
18
- ];
19
- /**
20
- * Load a pre-built route manifest from disk (JSON file).
21
- *
22
- * This works in traditional Node.js deployments where the filesystem is available.
23
- * For Vercel serverless, the manifest must be imported directly in your route file.
24
- *
25
- * Generate the manifest at build time using:
26
- * yak-nextjs generate-manifest
27
- *
28
- * @param manifestPath - Path to the manifest JSON file (default: tries common locations)
29
- * @returns Route manifest or null if not found
30
- */
31
- export function loadRouteManifest(manifestPath) {
32
- const pathsToTry = manifestPath ? [manifestPath] : [...DEFAULT_MANIFEST_PATHS];
33
- for (const relativePath of pathsToTry) {
34
- try {
35
- const fullPath = path.resolve(process.cwd(), relativePath);
36
- if (fs.existsSync(fullPath)) {
37
- const content = fs.readFileSync(fullPath, "utf-8");
38
- return JSON.parse(content);
39
- }
40
- }
41
- catch {
42
- // Continue to next path
43
- }
44
- }
45
- return null;
46
- }
47
- /**
48
- * Load routes from a pre-built manifest.
49
- * Throws if manifest is not found.
50
- */
51
- export function loadRoutes(manifestPath) {
52
- const manifest = loadRouteManifest(manifestPath);
53
- if (!manifest) {
54
- const pathsChecked = manifestPath ? manifestPath : [...DEFAULT_MANIFEST_PATHS].join(", ");
55
- throw new Error(`Route manifest not found. Checked: ${pathsChecked}\n\n` +
56
- "In production environments (like Vercel), route scanning requires a pre-built manifest.\n" +
57
- "Generate it at build time by adding to your build script:\n\n" +
58
- " yak-nextjs generate-manifest\n\n" +
59
- "Or provide routes explicitly in your handler configuration.");
60
- }
61
- return manifest.routes;
62
- }
63
- /**
64
- * Create a unified Next.js App Router handler backed by the core primitives
65
- */
66
- export function createNextYakHandler(config) {
67
- return createYakHandler({
68
- routes: resolveRouteSources(config),
69
- tools: resolveToolSources(config),
70
- });
71
- }
72
- /**
73
- * Attempt to load routes from filesystem or fetch from public URL.
74
- *
75
- * In development: scans the app directory directly
76
- * In production (Vercel, etc.): falls back to pre-built manifest via filesystem or HTTP fetch
77
- */
78
- async function tryLoadRoutes(appDir) {
79
- const targetDir = path.resolve(process.cwd(), appDir);
80
- // If the app directory exists, scan it directly (development mode)
81
- if (fs.existsSync(targetDir)) {
82
- return scanRoutes(appDir);
83
- }
84
- // In production, the source directory doesn't exist - try loading from manifest file
85
- const manifest = loadRouteManifest();
86
- if (manifest) {
87
- return manifest.routes;
88
- }
89
- // Neither source nor manifest available - provide helpful error
90
- throw new Error("Route manifest not found.\n\n" +
91
- "Generate the manifest at build time by adding to package.json:\n" +
92
- ` "prebuild": "yak-nextjs generate-manifest"\n\n` +
93
- "The manifest will be created at ./src/yak-routes-manifest.json and automatically\n" +
94
- "bundled with your serverless function.\n\n" +
95
- "Alternatively, provide routes explicitly:\n" +
96
- ` createNextYakHandler({ routes: [{ path: "/", title: "Home" }] })`);
97
- }
98
- function resolveRouteSources(config) {
99
- if (config.routes) {
100
- return config.routes;
101
- }
102
- if (config.getRoutes) {
103
- return config.getRoutes;
104
- }
105
- return async () => {
106
- const routes = await tryLoadRoutes(config.appDir ?? "./src/app");
107
- return applyRouteFilters(routes, config.routeFilter);
108
- };
109
- }
110
- function applyRouteFilters(routes, filter) {
111
- if (!filter) {
112
- return routes;
113
- }
114
- const { include = [], exclude = [] } = filter;
115
- return routes.filter((route) => {
116
- const path = route.path;
117
- if (include.length > 0 && !include.some((pattern) => matches(pattern, path))) {
118
- return false;
119
- }
120
- if (exclude.some((pattern) => matches(pattern, path))) {
121
- return false;
122
- }
123
- return true;
124
- });
125
- }
126
- function matches(pattern, value) {
127
- pattern.lastIndex = 0;
128
- return pattern.test(value);
129
- }
130
- function resolveToolSources(config) {
131
- if (config.tools) {
132
- return config.tools;
133
- }
134
- if (config.getTools) {
135
- return {
136
- getTools: config.getTools,
137
- executeTool: config.executeTool,
138
- };
139
- }
140
- if (config.executeTool) {
141
- throw new Error("'executeTool' was provided without a matching tool manifest source");
142
- }
143
- return undefined;
144
- }
145
- export function createNextYakConfigHandler(config) {
146
- return createYakConfigHandler({
147
- routes: resolveRouteSources(config),
148
- tools: config.tools ?? (config.getTools ? { getTools: config.getTools } : undefined),
149
- });
150
- }
151
- export function createNextYakToolsHandler(config) {
152
- if (!config.tools && !config.getTools) {
153
- throw new Error("createNextYakToolsHandler requires either 'tools' or 'getTools'");
154
- }
155
- const toolSource = config.tools ?? {
156
- // getTools is guaranteed to be defined here since we check above
157
- getTools: config.getTools,
158
- executeTool: config.executeTool,
159
- };
160
- return createYakToolsHandler({ tools: toolSource });
161
- }
@@ -1 +0,0 @@
1
- export { createNextYakToolsHandler } from "./createNextYakHandler.js";