@zubyjs/next 1.0.83
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/README.md +446 -0
- package/index.d.ts +77 -0
- package/index.js +300 -0
- package/nextApiRequest.d.ts +6 -0
- package/nextApiRequest.js +79 -0
- package/nextApiResponse.d.ts +26 -0
- package/nextApiResponse.js +135 -0
- package/nft.d.ts +31 -0
- package/nft.js +158 -0
- package/package.json +41 -0
- package/types.d.ts +125 -0
- package/types.js +1 -0
- package/utils.d.ts +23 -0
- package/utils.js +336 -0
package/nft.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency tracing using @vercel/nft
|
|
3
|
+
* Finds all imports and dependencies of API route files
|
|
4
|
+
* and updates imports to point to original folders
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, resolve, relative } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
/**
|
|
9
|
+
* Trace all imports in a file and create a map for rewriting them
|
|
10
|
+
* This analyzes the wrapper file location and creates relative paths
|
|
11
|
+
* back to the original import locations
|
|
12
|
+
*/
|
|
13
|
+
export async function createImportMap(apiFilePath, wrapperFilePath) {
|
|
14
|
+
const importMap = {};
|
|
15
|
+
try {
|
|
16
|
+
const fs = await import('fs/promises');
|
|
17
|
+
const content = await fs.readFile(apiFilePath, 'utf-8');
|
|
18
|
+
// Parse import/require statements
|
|
19
|
+
const importRegex = /(?:import|require)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/g;
|
|
20
|
+
let match;
|
|
21
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
22
|
+
const importPath = match[1];
|
|
23
|
+
// Skip external packages and absolute paths
|
|
24
|
+
if (importPath.startsWith('@') ||
|
|
25
|
+
importPath.startsWith('/') ||
|
|
26
|
+
!importPath.includes('.') ||
|
|
27
|
+
importPath.includes('zuby')) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Resolve the import path relative to the API file
|
|
31
|
+
const resolvedPath = resolveImportPath(apiFilePath, importPath);
|
|
32
|
+
if (resolvedPath) {
|
|
33
|
+
// Calculate the correct relative path from wrapper to the actual import
|
|
34
|
+
const wrapperDir = dirname(wrapperFilePath);
|
|
35
|
+
const correctRelativePath = relative(wrapperDir, resolvedPath);
|
|
36
|
+
// Normalize for cross-platform compatibility
|
|
37
|
+
const normalizedPath = correctRelativePath.replace(/\\/g, '/');
|
|
38
|
+
importMap[importPath] = normalizedPath.startsWith('.')
|
|
39
|
+
? normalizedPath
|
|
40
|
+
: `./${normalizedPath}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.warn(`Failed to create import map for ${apiFilePath}:`, error);
|
|
46
|
+
}
|
|
47
|
+
return importMap;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve import path relative to the importing file
|
|
51
|
+
*/
|
|
52
|
+
export function resolveImportPath(fromFile, importPath) {
|
|
53
|
+
const fromDir = dirname(fromFile);
|
|
54
|
+
// Try the import path as-is
|
|
55
|
+
let resolved = resolve(fromDir, importPath);
|
|
56
|
+
if (existsSync(resolved))
|
|
57
|
+
return resolved;
|
|
58
|
+
// Try with .ts extension
|
|
59
|
+
if (existsSync(`${resolved}.ts`))
|
|
60
|
+
return `${resolved}.ts`;
|
|
61
|
+
if (existsSync(`${resolved}.tsx`))
|
|
62
|
+
return `${resolved}.tsx`;
|
|
63
|
+
if (existsSync(`${resolved}.js`))
|
|
64
|
+
return `${resolved}.js`;
|
|
65
|
+
if (existsSync(`${resolved}.jsx`))
|
|
66
|
+
return `${resolved}.jsx`;
|
|
67
|
+
if (existsSync(`${resolved}.mjs`))
|
|
68
|
+
return `${resolved}.mjs`;
|
|
69
|
+
if (existsSync(`${resolved}.cjs`))
|
|
70
|
+
return `${resolved}.cjs`;
|
|
71
|
+
// Try as directory with index file
|
|
72
|
+
if (existsSync(`${resolved}/index.ts`))
|
|
73
|
+
return `${resolved}/index.ts`;
|
|
74
|
+
if (existsSync(`${resolved}/index.tsx`))
|
|
75
|
+
return `${resolved}/index.tsx`;
|
|
76
|
+
if (existsSync(`${resolved}/index.js`))
|
|
77
|
+
return `${resolved}/index.js`;
|
|
78
|
+
if (existsSync(`${resolved}/index.jsx`))
|
|
79
|
+
return `${resolved}/index.jsx`;
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Rewrite imports in wrapper content to point to original locations
|
|
84
|
+
* Updates relative imports to reference the correct paths from zuby-pages
|
|
85
|
+
*/
|
|
86
|
+
export function rewriteImportsInWrapper(wrapperContent, importMap) {
|
|
87
|
+
let updatedContent = wrapperContent;
|
|
88
|
+
for (const [originalPath, correctPath] of Object.entries(importMap)) {
|
|
89
|
+
// Match different import/require patterns
|
|
90
|
+
const patterns = [
|
|
91
|
+
new RegExp(`from\\s+['"]${escapeRegex(originalPath)}['"]`, 'g'),
|
|
92
|
+
new RegExp(`require\\s*\\(\\s*['"]${escapeRegex(originalPath)}['"]\\s*\\)`, 'g'),
|
|
93
|
+
new RegExp(`import\\s+['"]${escapeRegex(originalPath)}['"]`, 'g'),
|
|
94
|
+
];
|
|
95
|
+
for (const pattern of patterns) {
|
|
96
|
+
updatedContent = updatedContent.replace(pattern, (match) => {
|
|
97
|
+
return match.replace(originalPath, correctPath);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return updatedContent;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Escape special regex characters
|
|
105
|
+
*/
|
|
106
|
+
function escapeRegex(str) {
|
|
107
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all relative imports from a file
|
|
111
|
+
*/
|
|
112
|
+
export async function getRelativeImports(filePath) {
|
|
113
|
+
const imports = [];
|
|
114
|
+
try {
|
|
115
|
+
const fs = await import('fs/promises');
|
|
116
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
117
|
+
// Parse import/require statements
|
|
118
|
+
const importRegex = /(?:import|require)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/g;
|
|
119
|
+
let match;
|
|
120
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
121
|
+
const importPath = match[1];
|
|
122
|
+
// Only include relative imports
|
|
123
|
+
if (importPath.startsWith('.')) {
|
|
124
|
+
imports.push(importPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.warn(`Could not read ${filePath}:`, error);
|
|
130
|
+
}
|
|
131
|
+
return imports;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get all external dependencies (not relative imports)
|
|
135
|
+
*/
|
|
136
|
+
export async function getExternalDependencies(filePath) {
|
|
137
|
+
const external = [];
|
|
138
|
+
try {
|
|
139
|
+
const fs = await import('fs/promises');
|
|
140
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
141
|
+
// Parse import/require statements
|
|
142
|
+
const importRegex = /(?:import|require)\s*\(?\s*['"]([^'"]+)['"]\s*\)?/g;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
145
|
+
const importPath = match[1];
|
|
146
|
+
// Only include external packages (not relative imports or zuby)
|
|
147
|
+
if (!importPath.startsWith('.') &&
|
|
148
|
+
!importPath.startsWith('/') &&
|
|
149
|
+
!importPath.includes('zuby')) {
|
|
150
|
+
external.push(importPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.warn(`Could not read ${filePath}:`, error);
|
|
156
|
+
}
|
|
157
|
+
return external;
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zubyjs/next",
|
|
3
|
+
"version": "1.0.83",
|
|
4
|
+
"description": "Zuby.js Next.js compatibility plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"release": "cd ./dist && npm publish --access public && cd ..",
|
|
9
|
+
"bump-version": "npm version patch",
|
|
10
|
+
"build": "rm -rf dist/ stage/ && mkdir dist && tsc && cp -rf package.json README.md stage/next/src/* dist/ && rm -rf stage/",
|
|
11
|
+
"push-build": "npm run build && cd dist && yalc push --force && cd ..",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:coverage": "vitest run --coverage"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"directory": "dist",
|
|
17
|
+
"linkDirectory": true
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"zuby": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://gitlab.com/futrou/zuby.js/-/issues",
|
|
24
|
+
"email": "zuby@futrou.com"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://gitlab.com/futrou/zuby.js.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://zubyjs.com",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"zuby-plugin",
|
|
34
|
+
"zuby",
|
|
35
|
+
"next",
|
|
36
|
+
"seo"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
/**
|
|
4
|
+
* Next.js API request object for compatibility
|
|
5
|
+
* Represents an incoming HTTP request in an API route
|
|
6
|
+
*/
|
|
7
|
+
export interface NextApiRequest {
|
|
8
|
+
/**
|
|
9
|
+
* The request URL
|
|
10
|
+
*/
|
|
11
|
+
url?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The request method (GET, POST, PUT, DELETE, PATCH, etc.)
|
|
14
|
+
*/
|
|
15
|
+
method?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Query parameters from the URL
|
|
18
|
+
* @example { id: '123', name: 'test' }
|
|
19
|
+
*/
|
|
20
|
+
query: Record<string, string | string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Request cookies
|
|
23
|
+
* @example { sessionId: 'abc123' }
|
|
24
|
+
*/
|
|
25
|
+
cookies: Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Request headers
|
|
28
|
+
*/
|
|
29
|
+
headers: Record<string, string | string[] | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* The request body
|
|
32
|
+
*/
|
|
33
|
+
body?: any;
|
|
34
|
+
/**
|
|
35
|
+
* The base URL
|
|
36
|
+
*/
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* The path after removing query string
|
|
40
|
+
*/
|
|
41
|
+
pathname?: string;
|
|
42
|
+
/**
|
|
43
|
+
* HTTP version
|
|
44
|
+
*/
|
|
45
|
+
httpVersion?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The IP address of the requester
|
|
48
|
+
*/
|
|
49
|
+
socket?: {
|
|
50
|
+
remoteAddress?: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generic Next.js API response object for compatibility
|
|
55
|
+
* Represents the response object in an API route with typed body
|
|
56
|
+
* @template TBody The type of the response body
|
|
57
|
+
*/
|
|
58
|
+
export interface NextApiResponse<TBody = any> {
|
|
59
|
+
/**
|
|
60
|
+
* HTTP status code
|
|
61
|
+
*/
|
|
62
|
+
statusCode: number;
|
|
63
|
+
/**
|
|
64
|
+
* Response headers
|
|
65
|
+
*/
|
|
66
|
+
headers: Record<string, string | string[] | number>;
|
|
67
|
+
/**
|
|
68
|
+
* Set the status code (chainable)
|
|
69
|
+
*/
|
|
70
|
+
status(code: number): this;
|
|
71
|
+
/**
|
|
72
|
+
* Set a header (chainable)
|
|
73
|
+
*/
|
|
74
|
+
setHeader(name: string, value: string | string[] | number): this;
|
|
75
|
+
/**
|
|
76
|
+
* Get a header
|
|
77
|
+
*/
|
|
78
|
+
getHeader(name: string): string | string[] | number | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Check if header was sent
|
|
81
|
+
*/
|
|
82
|
+
headersSent: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Send a JSON response
|
|
85
|
+
*/
|
|
86
|
+
json(body: TBody): void;
|
|
87
|
+
/**
|
|
88
|
+
* Send a text response
|
|
89
|
+
*/
|
|
90
|
+
send(body: any): void;
|
|
91
|
+
/**
|
|
92
|
+
* Send a specific status with body
|
|
93
|
+
*/
|
|
94
|
+
end(body?: any): void;
|
|
95
|
+
/**
|
|
96
|
+
* Write data to the response
|
|
97
|
+
*/
|
|
98
|
+
write(chunk: string | Buffer): void;
|
|
99
|
+
/**
|
|
100
|
+
* Redirect to a URL
|
|
101
|
+
*/
|
|
102
|
+
redirect(statusOrUrl: string | number, url?: string): void;
|
|
103
|
+
/**
|
|
104
|
+
* The actual response body
|
|
105
|
+
*/
|
|
106
|
+
_responseBody?: any;
|
|
107
|
+
/**
|
|
108
|
+
* Whether response has been sent
|
|
109
|
+
*/
|
|
110
|
+
_isResponseSent?: boolean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Next.js API handler type for single method handlers
|
|
114
|
+
* @template TBody The type of the response body
|
|
115
|
+
* @example
|
|
116
|
+
* export default function handler(req: NextApiRequest, res: NextApiResponse<string>) {
|
|
117
|
+
* return new Response('Hello world');
|
|
118
|
+
* }
|
|
119
|
+
*/
|
|
120
|
+
export type NextApiHandler<TBody = any> = (req: NextApiRequest, res: NextApiResponse<TBody>) => Response | Promise<Response> | void | Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Next.js API method handler for specific HTTP methods
|
|
123
|
+
* @template TBody The type of the response body
|
|
124
|
+
*/
|
|
125
|
+
export type NextApiMethodHandler<TBody = any> = (req: NextApiRequest, res: NextApiResponse<TBody>) => Response | Promise<Response> | void | Promise<void>;
|
package/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/utils.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TemplateFile } from 'zuby/templates/types.js';
|
|
2
|
+
export interface NextPage extends TemplateFile {
|
|
3
|
+
hasGetStaticProps?: boolean;
|
|
4
|
+
hasGetServerSideProps?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Scan the Next.js pages/api directory and convert to Zuby handler paths
|
|
8
|
+
* @param pagesDir The pages directory path
|
|
9
|
+
* @returns Array of TemplateFile objects for handlers
|
|
10
|
+
*/
|
|
11
|
+
export declare function scanNextApiPages(pagesDir: string): Promise<TemplateFile[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Scan the Next.js pages directory for React pages (excluding api subdirectory)
|
|
14
|
+
* @param pagesDir The pages directory path
|
|
15
|
+
* @returns Array of NextPage objects
|
|
16
|
+
*/
|
|
17
|
+
export declare function scanNextPages(pagesDir: string): Promise<NextPage[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Convert Next.js API route module to Zuby handler format
|
|
20
|
+
* Wraps default export and method exports (get, post, put, delete, patch)
|
|
21
|
+
* to return Response objects compatible with Zuby.js
|
|
22
|
+
*/
|
|
23
|
+
export declare function wrapNextApiModule(module: any): any;
|
package/utils.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { relative } from 'path';
|
|
2
|
+
import { readdir, stat } from 'fs/promises';
|
|
3
|
+
/**
|
|
4
|
+
* Scan the Next.js pages/api directory and convert to Zuby handler paths
|
|
5
|
+
* @param pagesDir The pages directory path
|
|
6
|
+
* @returns Array of TemplateFile objects for handlers
|
|
7
|
+
*/
|
|
8
|
+
export async function scanNextApiPages(pagesDir) {
|
|
9
|
+
const apiDir = `${pagesDir}/api`;
|
|
10
|
+
const handlers = [];
|
|
11
|
+
try {
|
|
12
|
+
await scanDirectory(apiDir, apiDir, handlers, 'handler');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// API directory might not exist, which is fine
|
|
16
|
+
if (error?.code !== 'ENOENT') {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return handlers;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Scan the Next.js pages directory for React pages (excluding api subdirectory)
|
|
24
|
+
* @param pagesDir The pages directory path
|
|
25
|
+
* @returns Array of NextPage objects
|
|
26
|
+
*/
|
|
27
|
+
export async function scanNextPages(pagesDir) {
|
|
28
|
+
const pages = [];
|
|
29
|
+
try {
|
|
30
|
+
await scanPageDirectory(pagesDir, pagesDir, pages);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Pages directory might not exist, which is fine
|
|
34
|
+
if (error?.code !== 'ENOENT') {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return pages;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Recursively scan a directory for API route files or page files
|
|
42
|
+
*/
|
|
43
|
+
async function scanDirectory(currentDir, baseDir, items, type = 'handler') {
|
|
44
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const fullPath = `${currentDir}/${entry.name}`;
|
|
47
|
+
const stats = await stat(fullPath);
|
|
48
|
+
if (stats.isDirectory()) {
|
|
49
|
+
await scanDirectory(fullPath, baseDir, items, type);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const relativePath = relative(baseDir, fullPath);
|
|
53
|
+
const routePath = `/${relativePath
|
|
54
|
+
.replace(/\\/g, '/')
|
|
55
|
+
.replace(/\.(tsx?|jsx?)$/, '')
|
|
56
|
+
.replace(/\/index$/, '')
|
|
57
|
+
.replace(/\[(.+?)\]/g, ':$1')}`;
|
|
58
|
+
items.push({
|
|
59
|
+
path: routePath,
|
|
60
|
+
filename: fullPath,
|
|
61
|
+
templateType: type,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively scan the pages directory for React page components
|
|
68
|
+
* Excludes the api subdirectory
|
|
69
|
+
*/
|
|
70
|
+
async function scanPageDirectory(currentDir, baseDir, pages) {
|
|
71
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
// Skip api directory
|
|
74
|
+
if (entry.isDirectory() && entry.name === 'api') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const fullPath = `${currentDir}/${entry.name}`;
|
|
78
|
+
const stats = await stat(fullPath);
|
|
79
|
+
if (stats.isDirectory()) {
|
|
80
|
+
await scanPageDirectory(fullPath, baseDir, pages);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const content = await readFileContent(fullPath);
|
|
84
|
+
const hasGetStaticProps = /export\s+(async\s+)?function\s+getStaticProps|export\s+const\s+getStaticProps/.test(content);
|
|
85
|
+
const hasGetServerSideProps = /export\s+(async\s+)?function\s+getServerSideProps|export\s+const\s+getServerSideProps/.test(content);
|
|
86
|
+
const relativePath = relative(baseDir, fullPath);
|
|
87
|
+
const routePath = `/${relativePath
|
|
88
|
+
.replace(/\\/g, '/')
|
|
89
|
+
.replace(/\.(tsx?)$/, '')
|
|
90
|
+
.replace(/\/index$/, '')}`;
|
|
91
|
+
pages.push({
|
|
92
|
+
path: routePath,
|
|
93
|
+
filename: fullPath,
|
|
94
|
+
templateType: 'page',
|
|
95
|
+
hasGetStaticProps,
|
|
96
|
+
hasGetServerSideProps,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read file content as string
|
|
103
|
+
*/
|
|
104
|
+
async function readFileContent(filePath) {
|
|
105
|
+
try {
|
|
106
|
+
const fs = await import('fs/promises');
|
|
107
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Convert Next.js API route module to Zuby handler format
|
|
115
|
+
* Wraps default export and method exports (get, post, put, delete, patch)
|
|
116
|
+
* to return Response objects compatible with Zuby.js
|
|
117
|
+
*/
|
|
118
|
+
export function wrapNextApiModule(module) {
|
|
119
|
+
const wrapped = {
|
|
120
|
+
__nextApiHandlers: true,
|
|
121
|
+
};
|
|
122
|
+
// Handle default export (all methods)
|
|
123
|
+
if (typeof module.default === 'function') {
|
|
124
|
+
wrapped.default = wrapHandler(module.default);
|
|
125
|
+
}
|
|
126
|
+
// Handle method-specific exports
|
|
127
|
+
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
128
|
+
for (const method of httpMethods) {
|
|
129
|
+
if (typeof module[method] === 'function') {
|
|
130
|
+
wrapped[method.toUpperCase()] = wrapHandler(module[method]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return wrapped;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Wrap a Next.js API handler to return a Response object
|
|
137
|
+
*/
|
|
138
|
+
function wrapHandler(handler) {
|
|
139
|
+
return async (context) => {
|
|
140
|
+
const req = createNextApiRequest(context);
|
|
141
|
+
const res = createNextApiResponse();
|
|
142
|
+
try {
|
|
143
|
+
const result = await handler(req, res);
|
|
144
|
+
// If handler returns a Response, use it
|
|
145
|
+
if (result instanceof Response) {
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
// If response was explicitly sent, use that
|
|
149
|
+
if (res._isResponseSent && res._responseBody !== undefined) {
|
|
150
|
+
const body = res._responseBody;
|
|
151
|
+
const contentType = res.headers['content-type'] || 'application/json';
|
|
152
|
+
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
|
|
153
|
+
status: res.statusCode,
|
|
154
|
+
headers: normalizeHeaders(res.headers),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Default: return empty 200 response
|
|
158
|
+
return new Response(null, {
|
|
159
|
+
status: res.statusCode || 200,
|
|
160
|
+
headers: normalizeHeaders(res.headers),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Return error response
|
|
165
|
+
return new Response(JSON.stringify({
|
|
166
|
+
error: 'Internal Server Error',
|
|
167
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
168
|
+
}), {
|
|
169
|
+
status: 500,
|
|
170
|
+
headers: { 'content-type': 'application/json' },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Create a Next.js API request object from Zuby context
|
|
177
|
+
*/
|
|
178
|
+
function createNextApiRequest(context) {
|
|
179
|
+
const url = new URL(context.url || '/', 'http://localhost');
|
|
180
|
+
const query = {};
|
|
181
|
+
// Parse query parameters
|
|
182
|
+
url.searchParams.forEach((value, key) => {
|
|
183
|
+
if (query[key]) {
|
|
184
|
+
if (Array.isArray(query[key])) {
|
|
185
|
+
query[key].push(value);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
query[key] = [query[key], value];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
query[key] = value;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// Add path parameters
|
|
196
|
+
if (context.params) {
|
|
197
|
+
Object.assign(query, context.params);
|
|
198
|
+
}
|
|
199
|
+
const req = {
|
|
200
|
+
url: context.url || '/',
|
|
201
|
+
method: context.request?.method || 'GET',
|
|
202
|
+
query,
|
|
203
|
+
cookies: parseCookies(context.request?.headers?.get('cookie')),
|
|
204
|
+
headers: headersToRecord(context.request?.headers),
|
|
205
|
+
baseUrl: `${url.protocol}//${url.host}`,
|
|
206
|
+
pathname: url.pathname,
|
|
207
|
+
body: context.body,
|
|
208
|
+
};
|
|
209
|
+
// Add socket info for client IP
|
|
210
|
+
if (context.clientAddress) {
|
|
211
|
+
req.socket = { remoteAddress: context.clientAddress };
|
|
212
|
+
}
|
|
213
|
+
return req;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create a Next.js API response object
|
|
217
|
+
*/
|
|
218
|
+
function createNextApiResponse() {
|
|
219
|
+
const res = {
|
|
220
|
+
statusCode: 200,
|
|
221
|
+
headers: {},
|
|
222
|
+
headersSent: false,
|
|
223
|
+
_responseBody: undefined,
|
|
224
|
+
_isResponseSent: false,
|
|
225
|
+
status(code) {
|
|
226
|
+
this.statusCode = code;
|
|
227
|
+
return this;
|
|
228
|
+
},
|
|
229
|
+
setHeader(name, value) {
|
|
230
|
+
this.headers[name.toLowerCase()] = value;
|
|
231
|
+
return this;
|
|
232
|
+
},
|
|
233
|
+
getHeader(name) {
|
|
234
|
+
return this.headers[name.toLowerCase()];
|
|
235
|
+
},
|
|
236
|
+
json(body) {
|
|
237
|
+
this.setHeader('content-type', 'application/json');
|
|
238
|
+
this._responseBody = JSON.stringify(body);
|
|
239
|
+
this._isResponseSent = true;
|
|
240
|
+
return this;
|
|
241
|
+
},
|
|
242
|
+
send(body) {
|
|
243
|
+
this._responseBody = body;
|
|
244
|
+
this._isResponseSent = true;
|
|
245
|
+
if (typeof body === 'string') {
|
|
246
|
+
this.setHeader('content-type', 'text/plain');
|
|
247
|
+
}
|
|
248
|
+
else if (typeof body === 'object') {
|
|
249
|
+
this.setHeader('content-type', 'application/json');
|
|
250
|
+
this._responseBody = JSON.stringify(body);
|
|
251
|
+
}
|
|
252
|
+
return this;
|
|
253
|
+
},
|
|
254
|
+
end(body) {
|
|
255
|
+
if (body !== undefined) {
|
|
256
|
+
this.send(body);
|
|
257
|
+
}
|
|
258
|
+
this._isResponseSent = true;
|
|
259
|
+
return this;
|
|
260
|
+
},
|
|
261
|
+
write(chunk) {
|
|
262
|
+
if (!this._responseBody) {
|
|
263
|
+
this._responseBody = '';
|
|
264
|
+
}
|
|
265
|
+
this._responseBody += chunk;
|
|
266
|
+
return true;
|
|
267
|
+
},
|
|
268
|
+
redirect(statusOrUrl, url) {
|
|
269
|
+
if (typeof statusOrUrl === 'string') {
|
|
270
|
+
this.statusCode = 302;
|
|
271
|
+
this._responseBody = statusOrUrl;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.statusCode = statusOrUrl;
|
|
275
|
+
this._responseBody = url;
|
|
276
|
+
}
|
|
277
|
+
this.setHeader('location', typeof statusOrUrl === 'string' ? statusOrUrl : url || '');
|
|
278
|
+
this._isResponseSent = true;
|
|
279
|
+
return this;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
return res;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Parse cookies from cookie header string
|
|
286
|
+
*/
|
|
287
|
+
function parseCookies(cookieHeader) {
|
|
288
|
+
const cookies = {};
|
|
289
|
+
if (!cookieHeader)
|
|
290
|
+
return cookies;
|
|
291
|
+
cookieHeader.split(';').forEach((cookie) => {
|
|
292
|
+
const [name, value] = cookie.trim().split('=');
|
|
293
|
+
if (name && value) {
|
|
294
|
+
cookies[name] = decodeURIComponent(value);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return cookies;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Convert Headers object to Record
|
|
301
|
+
*/
|
|
302
|
+
function headersToRecord(headers) {
|
|
303
|
+
const record = {};
|
|
304
|
+
if (!headers)
|
|
305
|
+
return record;
|
|
306
|
+
headers.forEach((value, key) => {
|
|
307
|
+
const lowerKey = key.toLowerCase();
|
|
308
|
+
if (record[lowerKey]) {
|
|
309
|
+
if (Array.isArray(record[lowerKey])) {
|
|
310
|
+
record[lowerKey].push(value);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
record[lowerKey] = [record[lowerKey], value];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
record[lowerKey] = value;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
return record;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Normalize headers to Response headers format
|
|
324
|
+
*/
|
|
325
|
+
function normalizeHeaders(headers) {
|
|
326
|
+
const normalized = {};
|
|
327
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
328
|
+
if (Array.isArray(value)) {
|
|
329
|
+
normalized[key] = value.join(', ');
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
normalized[key] = String(value);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return normalized;
|
|
336
|
+
}
|