@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,3 +0,0 @@
1
- export { createNextYakConfigHandler, createNextYakHandler, createNextYakToolsHandler, loadRouteManifest, loadRoutes, } from "./createNextYakHandler.js";
2
- export { createRouteManifestAdapter } from "./route-manifest-adapter.js";
3
- export { scanRoutes } from "./scan-routes.js";
@@ -1,75 +0,0 @@
1
- /**
2
- * Check if a route path matches a pattern.
3
- * Supports exact matches and prefix patterns ending with `*`.
4
- */
5
- function matchesPattern(pattern, routePath) {
6
- if (pattern.endsWith("/*")) {
7
- const prefix = pattern.slice(0, -1); // Remove the *
8
- return routePath === prefix.slice(0, -1) || routePath.startsWith(prefix);
9
- }
10
- return pattern === routePath;
11
- }
12
- /**
13
- * Filter routes based on allowed and disallowed patterns.
14
- */
15
- function filterRoutes(routes, allowedRoutes, disallowedRoutes) {
16
- return routes.filter((route) => {
17
- const path = route.path;
18
- // If allowedRoutes is set, route must match at least one pattern
19
- if (allowedRoutes && allowedRoutes.length > 0) {
20
- const isAllowed = allowedRoutes.some((pattern) => matchesPattern(pattern, path));
21
- if (!isAllowed) {
22
- return false;
23
- }
24
- }
25
- // If disallowedRoutes is set, route must not match any pattern
26
- if (disallowedRoutes && disallowedRoutes.length > 0) {
27
- const isDisallowed = disallowedRoutes.some((pattern) => matchesPattern(pattern, path));
28
- if (isDisallowed) {
29
- return false;
30
- }
31
- }
32
- return true;
33
- });
34
- }
35
- /**
36
- * Create a route source adapter from an imported route manifest.
37
- *
38
- * This adapter allows you to use generated route manifests with optional
39
- * filtering to control which routes are exposed to the chatbot.
40
- *
41
- * @example Basic usage - all routes
42
- * ```ts
43
- * import { createNextYakHandler } from "@yak-io/nextjs/server";
44
- * import { createRouteManifestAdapter } from "@yak-io/nextjs/server";
45
- * import { routes } from "@/yak.routes";
46
- *
47
- * export const { GET, POST } = createNextYakHandler({
48
- * routes: createRouteManifestAdapter({ routes }),
49
- * });
50
- * ```
51
- *
52
- * @example With filtering
53
- * ```ts
54
- * import { createNextYakHandler } from "@yak-io/nextjs/server";
55
- * import { createRouteManifestAdapter } from "@yak-io/nextjs/server";
56
- * import { routes } from "@/yak.routes";
57
- *
58
- * export const { GET, POST } = createNextYakHandler({
59
- * routes: createRouteManifestAdapter({
60
- * routes,
61
- * allowedRoutes: ["/docs/*", "/pricing", "/"],
62
- * disallowedRoutes: ["/docs/internal/*"],
63
- * }),
64
- * });
65
- * ```
66
- */
67
- export function createRouteManifestAdapter(config) {
68
- const { routes, id = "manifest", allowedRoutes, disallowedRoutes } = config;
69
- return {
70
- id,
71
- getRoutes: async () => {
72
- return filterRoutes(routes, allowedRoutes, disallowedRoutes);
73
- },
74
- };
75
- }
@@ -1,208 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- /**
4
- * Extract static metadata (title and description) from a Next.js page file.
5
- * Parses `export const metadata = { title: "...", description: "..." }` patterns.
6
- * Does not support dynamic generateMetadata functions.
7
- */
8
- function extractMetadata(filePath) {
9
- try {
10
- const content = fs.readFileSync(filePath, "utf-8");
11
- // Match `export const metadata` object
12
- const metadataMatch = content.match(/export\s+const\s+metadata\s*(?::\s*Metadata\s*)?=\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/s);
13
- if (!metadataMatch) {
14
- return {};
15
- }
16
- const metadataBlock = metadataMatch[1];
17
- const result = {};
18
- // Extract title - handle both single and double quotes
19
- const titleMatch = metadataBlock?.match(/title\s*:\s*["'`]([^"'`]+)["'`]/);
20
- if (titleMatch?.[1]) {
21
- result.title = titleMatch[1];
22
- }
23
- // Extract description - handle both single and double quotes
24
- const descMatch = metadataBlock?.match(/description\s*:\s*["'`]([^"'`]+)["'`]/);
25
- if (descMatch?.[1]) {
26
- result.description = descMatch[1];
27
- }
28
- return result;
29
- }
30
- catch {
31
- return {};
32
- }
33
- }
34
- /**
35
- * Check if a path segment is a route group (organizational only, not part of URL)
36
- */
37
- function isRouteGroup(segment) {
38
- return segment.startsWith("(") && segment.endsWith(")");
39
- }
40
- /**
41
- * Check if a path segment is an optional catch-all (e.g., [[...slug]])
42
- * These segments match the base path without any additional segments
43
- */
44
- function isOptionalCatchAll(segment) {
45
- return segment.startsWith("[[...") && segment.endsWith("]]");
46
- }
47
- /**
48
- * Check if a path segment is a required catch-all (e.g., [...slug])
49
- * These segments require at least one additional path segment
50
- */
51
- function isRequiredCatchAll(segment) {
52
- return segment.startsWith("[...") && segment.endsWith("]") && !segment.startsWith("[[");
53
- }
54
- /**
55
- * Check if a path segment is a dynamic segment (e.g., [id])
56
- */
57
- function isDynamicSegment(segment) {
58
- return (segment.startsWith("[") &&
59
- segment.endsWith("]") &&
60
- !isOptionalCatchAll(segment) &&
61
- !isRequiredCatchAll(segment));
62
- }
63
- /**
64
- * Normalize a dynamic segment for display
65
- * e.g., [id] → :id, [postId] → :postId
66
- */
67
- function normalizeDynamicSegment(segment) {
68
- // Extract the parameter name from [name] format
69
- const paramName = segment.slice(1, -1);
70
- return `:${paramName}`;
71
- }
72
- /**
73
- * Normalize a route path for display (excludes route groups and handles dynamic segments)
74
- */
75
- function normalizeRoutePath(segments) {
76
- // Filter out route groups - they don't appear in URLs
77
- // Filter out optional catch-all segments - they match the base path
78
- const urlSegments = segments
79
- .filter((seg) => !isRouteGroup(seg) && !isOptionalCatchAll(seg))
80
- .map((seg) => {
81
- if (isDynamicSegment(seg)) {
82
- return normalizeDynamicSegment(seg);
83
- }
84
- if (isRequiredCatchAll(seg)) {
85
- // For required catch-all, extract the name: [...slug] → :slug+
86
- const paramName = seg.slice(4, -1);
87
- return `:${paramName}+`;
88
- }
89
- return seg;
90
- });
91
- if (urlSegments.length === 0)
92
- return "/";
93
- return `/${urlSegments.join("/")}`;
94
- }
95
- /**
96
- * Check if file is a page file
97
- */
98
- function isPageFile(filename) {
99
- return filename === "page.tsx" || filename === "page.js";
100
- }
101
- /**
102
- * Recursively scan a directory for Next.js page routes
103
- */
104
- function scanAppDirectory(dirPath, segments = []) {
105
- const routes = [];
106
- try {
107
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
108
- for (const entry of entries) {
109
- const fullPath = path.join(dirPath, entry.name);
110
- if (entry.isDirectory()) {
111
- // Skip special Next.js directories and api routes
112
- if (entry.name.startsWith("_") || entry.name === "api") {
113
- continue;
114
- }
115
- // Recursively scan subdirectories
116
- routes.push(...scanAppDirectory(fullPath, [...segments, entry.name]));
117
- }
118
- else if (entry.isFile() && isPageFile(entry.name)) {
119
- const metadata = extractMetadata(fullPath);
120
- routes.push({
121
- path: normalizeRoutePath(segments),
122
- title: metadata.title,
123
- description: metadata.description,
124
- });
125
- }
126
- }
127
- }
128
- catch (error) {
129
- console.error(`Error scanning directory ${dirPath}:`, error);
130
- }
131
- return routes;
132
- }
133
- /**
134
- * File discovery helpers for legacy `pages/` directories
135
- */
136
- const VALID_PAGE_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]);
137
- const SPECIAL_PAGE_FILENAMES = new Set([
138
- "_app",
139
- "_document",
140
- "_error",
141
- "404",
142
- "500",
143
- "middleware",
144
- "_middleware",
145
- ]);
146
- function scanPagesDirectory(dirPath, segments = []) {
147
- const routes = [];
148
- try {
149
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
150
- for (const entry of entries) {
151
- const fullPath = path.join(dirPath, entry.name);
152
- if (entry.isDirectory()) {
153
- // Skip special directories and api routes
154
- if (entry.name.startsWith("_") || entry.name === "api") {
155
- continue;
156
- }
157
- routes.push(...scanPagesDirectory(fullPath, [...segments, entry.name]));
158
- }
159
- else if (entry.isFile()) {
160
- const extension = path.extname(entry.name);
161
- if (!VALID_PAGE_EXTENSIONS.has(extension)) {
162
- continue;
163
- }
164
- const baseName = entry.name.slice(0, -extension.length);
165
- if (!baseName || SPECIAL_PAGE_FILENAMES.has(baseName) || baseName.startsWith("_")) {
166
- continue;
167
- }
168
- const routeSegments = buildPagesSegments(segments, baseName);
169
- const metadata = extractMetadata(fullPath);
170
- routes.push({
171
- path: normalizeRoutePath(routeSegments),
172
- title: metadata.title,
173
- description: metadata.description,
174
- });
175
- }
176
- }
177
- }
178
- catch (error) {
179
- console.error(`Error scanning directory ${dirPath}:`, error);
180
- }
181
- return routes;
182
- }
183
- function buildPagesSegments(segments, baseName) {
184
- const routeSegments = [...segments];
185
- if (baseName !== "index") {
186
- routeSegments.push(baseName);
187
- }
188
- return routeSegments;
189
- }
190
- function normalizeScanOptions(options) {
191
- return {
192
- directoryType: options?.directoryType ?? "app",
193
- };
194
- }
195
- /**
196
- * Scan a Next.js app or pages directory and return page route information
197
- */
198
- export function scanRoutes(directory, options) {
199
- const normalizedOptions = normalizeScanOptions(options);
200
- const targetDir = path.resolve(process.cwd(), directory);
201
- if (!fs.existsSync(targetDir)) {
202
- throw new Error(`App directory not found: ${targetDir}`);
203
- }
204
- const routes = normalizedOptions.directoryType === "pages"
205
- ? scanPagesDirectory(targetDir, [])
206
- : scanAppDirectory(targetDir, []);
207
- return routes.sort((a, b) => a.path.localeCompare(b.path));
208
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};