@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/index.js ADDED
@@ -0,0 +1,300 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { resolve, relative, sep } from 'path';
4
+ import { scanNextApiPages, scanNextPages } from './utils.js';
5
+ import { createImportMap, rewriteImportsInWrapper } from './nft.js';
6
+ export const ZUBY_PAGES_DIR = './zuby-pages';
7
+ /**
8
+ * Zuby.js Next.js compatibility plugin
9
+ * Converts Next.js pages and API routes to Zuby.js handlers
10
+ *
11
+ * Features:
12
+ * - Automatically scans ./pages directory
13
+ * - Converts ./pages/api routes to Zuby handlers
14
+ * - Converts React page components to Zuby pages
15
+ * - Supports Next.js API route syntax (get, post, put, delete, etc.)
16
+ * - Provides NextApiRequest and NextApiResponse compatibility objects
17
+ * - Handles both default and method-specific exports
18
+ * - Supports getStaticProps (prerender = true) and getServerSideProps (prerender = false)
19
+ * - Passes page props through context
20
+ *
21
+ * @param options Plugin options
22
+ * @returns ZubyPlugin
23
+ *
24
+ * @example
25
+ * // pages/api/hello.ts
26
+ * export default function handler(req, res) {
27
+ * return new Response('Hello from Zuby!');
28
+ * }
29
+ *
30
+ * // pages/api/posts/[id].ts
31
+ * export async function get(req, res) {
32
+ * const post = await fetchPost(req.query.id);
33
+ * return new Response(JSON.stringify(post));
34
+ * }
35
+ *
36
+ * export async function post(req, res) {
37
+ * const newPost = await createPost(req.body);
38
+ * return new Response(JSON.stringify(newPost), { status: 201 });
39
+ * }
40
+ *
41
+ * // pages/index.tsx with static generation
42
+ * export default function HomePage(props) {
43
+ * return <h1>Posts: {props.count}</h1>;
44
+ * }
45
+ *
46
+ * export async function getStaticProps() {
47
+ * return { props: { count: 42 } };
48
+ * }
49
+ *
50
+ * // pages/about.tsx with server-side rendering
51
+ * export default function AboutPage(props) {
52
+ * return <h1>About - User: {props.user}</h1>;
53
+ * }
54
+ *
55
+ * export async function getServerSideProps(context) {
56
+ * return { props: { user: 'John' } };
57
+ * }
58
+ */
59
+ export default ({ autoDetectApiRoutes = true, pagesDir = './pages', traceDependencies = true, } = {}) => ({
60
+ name: 'zuby-next-plugin',
61
+ description: 'Next.js compatibility plugin',
62
+ buildStep: true,
63
+ hooks: {
64
+ 'zuby:config:setup': async ({ config, logger, addHandler }) => {
65
+ // Validate configuration
66
+ if (config.srcPagesDir && config.srcPagesDir !== './pages') {
67
+ logger?.error(`[zuby-next-plugin] The srcPagesDir zuby.config.mjs option cannot be used together with the next.js pages router. Please keep srcPagesDir to the default './pages'.`);
68
+ return;
69
+ }
70
+ // Create zuby-pages directory
71
+ if (!existsSync(ZUBY_PAGES_DIR)) {
72
+ await mkdir(ZUBY_PAGES_DIR, { recursive: true });
73
+ }
74
+ config.srcPagesDir = ZUBY_PAGES_DIR;
75
+ const nextPagesDir = resolve(config.srcDir || '.', pagesDir);
76
+ // Scan and convert Next.js pages/api if enabled
77
+ if (autoDetectApiRoutes) {
78
+ try {
79
+ const apiHandlers = await scanNextApiPages(nextPagesDir);
80
+ for (const handler of apiHandlers) {
81
+ // Create wrapper file for each handler
82
+ // Convert /api/hello to api/hello.ts to maintain Next.js structure
83
+ const handlerPathNormalized = handler.path
84
+ .replace(/^\//, '') // Remove leading slash
85
+ .replace(/\//g, sep); // Convert / to platform separator
86
+ const wrapperPath = resolve(ZUBY_PAGES_DIR, handlerPathNormalized + '.ts');
87
+ // Calculate relative path from wrapper to original handler file
88
+ const relativePathToHandler = relative(resolve(wrapperPath, '..'), handler.filename);
89
+ logger?.info(`[zuby-next-plugin] Generating wrapper for ${handler.path}`);
90
+ let wrapperContent = generateHandlerWrapper(handler.path, relativePathToHandler);
91
+ // Trace and rewrite imports if enabled
92
+ if (traceDependencies) {
93
+ try {
94
+ const importMap = await createImportMap(handler.filename, wrapperPath);
95
+ wrapperContent = rewriteImportsInWrapper(wrapperContent, importMap);
96
+ if (Object.keys(importMap).length > 0) {
97
+ logger?.info(`[zuby-next-plugin] Rewritten imports for ${handler.path}: ${Object.keys(importMap).length} mappings`);
98
+ }
99
+ }
100
+ catch (error) {
101
+ logger?.warn(`[zuby-next-plugin] Failed to trace dependencies for ${handler.path}: ${error}`);
102
+ }
103
+ }
104
+ // Ensure directory exists
105
+ const wrapperDir = resolve(wrapperPath, '..');
106
+ if (!existsSync(wrapperDir)) {
107
+ await mkdir(wrapperDir, { recursive: true });
108
+ }
109
+ await writeFile(wrapperPath, wrapperContent);
110
+ // Register handler with Zuby
111
+ addHandler(wrapperPath, handler.path);
112
+ logger?.info(`[zuby-next-plugin] Converted API route: ${handler.path}`);
113
+ }
114
+ }
115
+ catch (error) {
116
+ if (error?.code !== 'ENOENT') {
117
+ logger?.warn(`[zuby-next-plugin] Failed to scan API routes: ${error}`);
118
+ }
119
+ }
120
+ }
121
+ // Scan and convert Next.js pages
122
+ try {
123
+ const pages = await scanNextPages(nextPagesDir);
124
+ for (const page of pages) {
125
+ logger?.info(`[zuby-next-plugin] Generating wrapper for page ${page.path}`);
126
+ // Create wrapper file for each page
127
+ const pagePathNormalized = page.path
128
+ .replace(/^\//, '') // Remove leading slash
129
+ .replace(/\//g, sep); // Convert / to platform separator
130
+ const wrapperPath = resolve(ZUBY_PAGES_DIR, pagePathNormalized + '.ts');
131
+ // Calculate relative path from wrapper to original page file
132
+ const relativePathToPage = relative(resolve(wrapperPath, '..'), page.filename);
133
+ const hasStaticProps = page.hasGetStaticProps || false;
134
+ const hasServerSideProps = page.hasGetServerSideProps || false;
135
+ logger?.info(`[zuby-next-plugin] Generating wrapper for page ${page.path}`);
136
+ let wrapperContent = generatePageWrapper(page.path, relativePathToPage, hasStaticProps, hasServerSideProps);
137
+ // Trace and rewrite imports if enabled
138
+ if (traceDependencies) {
139
+ try {
140
+ const importMap = await createImportMap(page.filename, wrapperPath);
141
+ wrapperContent = rewriteImportsInWrapper(wrapperContent, importMap);
142
+ if (Object.keys(importMap).length > 0) {
143
+ logger?.info(`[zuby-next-plugin] Rewritten imports for ${page.path}: ${Object.keys(importMap).length} mappings`);
144
+ }
145
+ }
146
+ catch (error) {
147
+ logger?.warn(`[zuby-next-plugin] Failed to trace dependencies for ${page.path}: ${error}`);
148
+ }
149
+ }
150
+ // Ensure directory exists
151
+ const wrapperDir = resolve(wrapperPath, '..');
152
+ if (!existsSync(wrapperDir)) {
153
+ await mkdir(wrapperDir, { recursive: true });
154
+ }
155
+ await writeFile(wrapperPath, wrapperContent);
156
+ // Register page with Zuby
157
+ addHandler(wrapperPath, page.path);
158
+ logger?.info(`[zuby-next-plugin] Converted page: ${page.path}`);
159
+ }
160
+ }
161
+ catch (error) {
162
+ if (error?.code !== 'ENOENT') {
163
+ logger?.warn(`[zuby-next-plugin] Failed to scan pages: ${error}`);
164
+ }
165
+ }
166
+ },
167
+ },
168
+ });
169
+ /**
170
+ * Generate a wrapper file for Next.js API routes
171
+ * This wrapper converts Next.js handlers to Zuby handler format
172
+ */
173
+ function generateHandlerWrapper(nextApiPath, importPath) {
174
+ // Normalize path for cross-platform compatibility and ensure it's relative
175
+ const normalizedPath = importPath
176
+ .replace(/\\/g, '/') // Convert backslashes to forward slashes
177
+ .replace(/\.tsx?$/, '') // Remove .ts or .tsx extension
178
+ .replace(/^(?!\.\/|\.\.\/|\/)/, './'); // Add ./ prefix if not already present
179
+ return `import nextApiModule from '${normalizedPath}';
180
+ import { createNextApiRequest } from '@zubyjs/next/nextApiRequest.js';
181
+ import { createNextApiResponse, nextApiResponseToResponse } from '@zubyjs/next/nextApiResponse.js';
182
+ import type { PageContext } from 'zuby';
183
+
184
+ async function wrapHandler(handler: Function, context: any) {
185
+ const req = createNextApiRequest(context);
186
+ const res = createNextApiResponse();
187
+
188
+ try {
189
+ const result = await handler(req, res);
190
+
191
+ if (result instanceof Response) {
192
+ return result;
193
+ }
194
+
195
+ return nextApiResponseToResponse(res);
196
+ } catch (error) {
197
+ return new Response(
198
+ JSON.stringify({
199
+ error: 'Internal Server Error',
200
+ message: error instanceof Error ? error.message : 'Unknown error',
201
+ }),
202
+ {
203
+ status: 500,
204
+ headers: { 'content-type': 'application/json' },
205
+ },
206
+ );
207
+ }
208
+ }
209
+
210
+ export default async (context: PageContext) => {
211
+ const method = context.request?.method?.toLowerCase() || 'get';
212
+
213
+ // Try method-specific handler first
214
+ if (typeof nextApiModule[method] === 'function') {
215
+ return wrapHandler(nextApiModule[method], context);
216
+ }
217
+
218
+ // Fall back to default handler
219
+ if (typeof nextApiModule.default === 'function') {
220
+ return wrapHandler(nextApiModule.default, context);
221
+ }
222
+
223
+ if (typeof nextApiModule === 'function') {
224
+ return wrapHandler(nextApiModule, context);
225
+ }
226
+
227
+ return new Response('Method not allowed', { status: 405 });
228
+ };
229
+ `;
230
+ }
231
+ /**
232
+ * Generate a wrapper file for Next.js pages with getStaticProps or getServerSideProps
233
+ * This wrapper creates a handler that renders the React component
234
+ */
235
+ function generatePageWrapper(pagePath, importPath, hasStaticProps, hasServerSideProps) {
236
+ // Normalize path for cross-platform compatibility and ensure it's relative
237
+ const normalizedPath = importPath
238
+ .replace(/\\/g, '/') // Convert backslashes to forward slashes
239
+ .replace(/\.tsx?$/, '') // Remove .ts or .tsx extension
240
+ .replace(/^(?!\.\/|\.\.\/|\/)/, './'); // Add ./ prefix if not already present
241
+ const prenderMode = hasStaticProps ? 'true' : 'false';
242
+ return `import pageModule from '${normalizedPath}';
243
+ import type { PageContext } from 'zuby';
244
+
245
+ // Re-export the React component as default
246
+ export const Component = pageModule.default;
247
+
248
+ // Export prerender setting based on getStaticProps/getServerSideProps
249
+ export const prerender = ${prenderMode};
250
+
251
+ // Page handler that calls getStaticProps or getServerSideProps if available
252
+ export default async (context: PageContext) => {
253
+ let props: Record<string, any> = {};
254
+
255
+ // Call getStaticProps if available
256
+ if (typeof pageModule.getStaticProps === 'function') {
257
+ try {
258
+ const result = await pageModule.getStaticProps({
259
+ params: context.params || {},
260
+ });
261
+ if (result && result.props) {
262
+ props = result.props;
263
+ }
264
+ } catch (error) {
265
+ console.error('[zuby-next-plugin] Error in getStaticProps:', error);
266
+ }
267
+ }
268
+
269
+ // Call getServerSideProps if available
270
+ if (typeof pageModule.getServerSideProps === 'function') {
271
+ try {
272
+ const result = await pageModule.getServerSideProps({
273
+ params: context.params || {},
274
+ req: {
275
+ headers: Object.fromEntries(
276
+ context.request?.headers?.entries?.() || []
277
+ ),
278
+ method: context.request?.method || 'GET',
279
+ url: context.url || '/',
280
+ } as any,
281
+ res: {} as any,
282
+ });
283
+ if (result && result.props) {
284
+ props = result.props;
285
+ }
286
+ } catch (error) {
287
+ console.error('[zuby-next-plugin] Error in getServerSideProps:', error);
288
+ }
289
+ }
290
+
291
+ // Store props in context for the page to access
292
+ context.props = props;
293
+
294
+ // Return empty response - the component will be rendered by Zuby
295
+ return new Response(null, { status: 200 });
296
+ };
297
+ `;
298
+ }
299
+ export { createNextApiRequest } from './nextApiRequest.js';
300
+ export { createNextApiResponse, nextApiResponseToResponse, getResponseState } from './nextApiResponse.js';
@@ -0,0 +1,6 @@
1
+ import type { NextApiRequest } from './types.js';
2
+ /**
3
+ * Create a NextApiRequest object from Zuby PageContext
4
+ * Parses query parameters, cookies, headers, and body from the request
5
+ */
6
+ export declare function createNextApiRequest(context: any): NextApiRequest;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Create a NextApiRequest object from Zuby PageContext
3
+ * Parses query parameters, cookies, headers, and body from the request
4
+ */
5
+ export function createNextApiRequest(context) {
6
+ const url = new URL(context.url || '/', 'http://localhost');
7
+ const query = {};
8
+ // Parse query parameters from URL
9
+ url.searchParams.forEach((value, key) => {
10
+ if (query[key]) {
11
+ if (Array.isArray(query[key])) {
12
+ query[key].push(value);
13
+ }
14
+ else {
15
+ query[key] = [query[key], value];
16
+ }
17
+ }
18
+ else {
19
+ query[key] = value;
20
+ }
21
+ });
22
+ // Add path parameters from context
23
+ if (context.params) {
24
+ Object.assign(query, context.params);
25
+ }
26
+ const req = {
27
+ url: context.url || '/',
28
+ method: context.request?.method || 'GET',
29
+ query,
30
+ cookies: parseCookies(context.request?.headers?.get('cookie')),
31
+ headers: headersToRecord(context.request?.headers),
32
+ baseUrl: `${url.protocol}//${url.host}`,
33
+ pathname: url.pathname,
34
+ body: context.body,
35
+ };
36
+ // Add socket info for client IP if available
37
+ if (context.clientAddress) {
38
+ req.socket = { remoteAddress: context.clientAddress };
39
+ }
40
+ return req;
41
+ }
42
+ /**
43
+ * Parse cookies from cookie header string
44
+ */
45
+ function parseCookies(cookieHeader) {
46
+ const cookies = {};
47
+ if (!cookieHeader)
48
+ return cookies;
49
+ cookieHeader.split(';').forEach((cookie) => {
50
+ const [name, value] = cookie.trim().split('=');
51
+ if (name && value) {
52
+ cookies[name] = decodeURIComponent(value);
53
+ }
54
+ });
55
+ return cookies;
56
+ }
57
+ /**
58
+ * Convert Headers object to Record
59
+ */
60
+ function headersToRecord(headers) {
61
+ const record = {};
62
+ if (!headers)
63
+ return record;
64
+ headers.forEach((value, key) => {
65
+ const lowerKey = key.toLowerCase();
66
+ if (record[lowerKey]) {
67
+ if (Array.isArray(record[lowerKey])) {
68
+ record[lowerKey].push(value);
69
+ }
70
+ else {
71
+ record[lowerKey] = [record[lowerKey], value];
72
+ }
73
+ }
74
+ else {
75
+ record[lowerKey] = value;
76
+ }
77
+ });
78
+ return record;
79
+ }
@@ -0,0 +1,26 @@
1
+ import type { NextApiResponse } from './types.js';
2
+ /**
3
+ * Internal response state for tracking what's been sent
4
+ */
5
+ interface InternalResponseState {
6
+ statusCode: number;
7
+ headers: Record<string, string | string[] | number>;
8
+ body?: any;
9
+ sent: boolean;
10
+ }
11
+ /**
12
+ * Create a NextApiResponse object with optional body type
13
+ * Tracks response state and provides methods compatible with Next.js API responses
14
+ * @template TBody The type of the response body
15
+ */
16
+ export declare function createNextApiResponse<TBody = any>(): NextApiResponse<TBody>;
17
+ /**
18
+ * Get the internal state from a response object
19
+ * Used to convert the response into a Response object
20
+ */
21
+ export declare function getResponseState(res: NextApiResponse): InternalResponseState;
22
+ /**
23
+ * Convert a NextApiResponse to a Response object for Zuby
24
+ */
25
+ export declare function nextApiResponseToResponse(res: NextApiResponse): Response;
26
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Create a NextApiResponse object with optional body type
3
+ * Tracks response state and provides methods compatible with Next.js API responses
4
+ * @template TBody The type of the response body
5
+ */
6
+ export function createNextApiResponse() {
7
+ const state = {
8
+ statusCode: 200,
9
+ headers: {},
10
+ sent: false,
11
+ };
12
+ const res = {
13
+ statusCode: state.statusCode,
14
+ headers: state.headers,
15
+ headersSent: false,
16
+ status(code) {
17
+ state.statusCode = code;
18
+ this.statusCode = code;
19
+ return this;
20
+ },
21
+ setHeader(name, value) {
22
+ const lowerName = name.toLowerCase();
23
+ state.headers[lowerName] = value;
24
+ this.headers[lowerName] = value;
25
+ return this;
26
+ },
27
+ getHeader(name) {
28
+ const lowerName = name.toLowerCase();
29
+ return state.headers[lowerName];
30
+ },
31
+ json(body) {
32
+ this.setHeader('content-type', 'application/json');
33
+ state.body = JSON.stringify(body);
34
+ state.sent = true;
35
+ this.headersSent = true;
36
+ return this;
37
+ },
38
+ send(body) {
39
+ state.body = body;
40
+ state.sent = true;
41
+ this.headersSent = true;
42
+ if (typeof body === 'string') {
43
+ this.setHeader('content-type', 'text/plain');
44
+ }
45
+ else if (typeof body === 'object') {
46
+ this.setHeader('content-type', 'application/json');
47
+ state.body = JSON.stringify(body);
48
+ }
49
+ return this;
50
+ },
51
+ end(body) {
52
+ if (body !== undefined) {
53
+ this.send(body);
54
+ }
55
+ state.sent = true;
56
+ this.headersSent = true;
57
+ return this;
58
+ },
59
+ write(chunk) {
60
+ if (!state.body) {
61
+ state.body = '';
62
+ }
63
+ state.body += chunk;
64
+ return true;
65
+ },
66
+ redirect(statusOrUrl, url) {
67
+ if (typeof statusOrUrl === 'string') {
68
+ state.statusCode = 302;
69
+ state.body = statusOrUrl;
70
+ this.statusCode = 302;
71
+ this.setHeader('location', statusOrUrl);
72
+ }
73
+ else {
74
+ state.statusCode = statusOrUrl;
75
+ this.statusCode = statusOrUrl;
76
+ this.setHeader('location', url || '');
77
+ state.body = url;
78
+ }
79
+ state.sent = true;
80
+ this.headersSent = true;
81
+ return this;
82
+ },
83
+ };
84
+ // Attach internal state to response object for later retrieval
85
+ res.__state__ = state;
86
+ return res;
87
+ }
88
+ /**
89
+ * Get the internal state from a response object
90
+ * Used to convert the response into a Response object
91
+ */
92
+ export function getResponseState(res) {
93
+ return res.__state__ || {
94
+ statusCode: res.statusCode || 200,
95
+ headers: res.headers || {},
96
+ sent: false,
97
+ };
98
+ }
99
+ /**
100
+ * Convert a NextApiResponse to a Response object for Zuby
101
+ */
102
+ export function nextApiResponseToResponse(res) {
103
+ const state = getResponseState(res);
104
+ let body = null;
105
+ if (state.body !== undefined) {
106
+ if (typeof state.body === 'string') {
107
+ body = state.body;
108
+ }
109
+ else if (Buffer.isBuffer(state.body)) {
110
+ body = Buffer.from(state.body);
111
+ }
112
+ else {
113
+ body = JSON.stringify(state.body);
114
+ }
115
+ }
116
+ return new Response(body, {
117
+ status: state.statusCode || 200,
118
+ headers: normalizeHeaders(state.headers),
119
+ });
120
+ }
121
+ /**
122
+ * Normalize headers to Response headers format
123
+ */
124
+ function normalizeHeaders(headers) {
125
+ const normalized = {};
126
+ Object.entries(headers).forEach(([key, value]) => {
127
+ if (Array.isArray(value)) {
128
+ normalized[key] = value.join(', ');
129
+ }
130
+ else {
131
+ normalized[key] = String(value);
132
+ }
133
+ });
134
+ return normalized;
135
+ }
package/nft.d.ts ADDED
@@ -0,0 +1,31 @@
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
+ export interface ImportMap {
7
+ [importPath: string]: string;
8
+ }
9
+ /**
10
+ * Trace all imports in a file and create a map for rewriting them
11
+ * This analyzes the wrapper file location and creates relative paths
12
+ * back to the original import locations
13
+ */
14
+ export declare function createImportMap(apiFilePath: string, wrapperFilePath: string): Promise<ImportMap>;
15
+ /**
16
+ * Resolve import path relative to the importing file
17
+ */
18
+ export declare function resolveImportPath(fromFile: string, importPath: string): string | null;
19
+ /**
20
+ * Rewrite imports in wrapper content to point to original locations
21
+ * Updates relative imports to reference the correct paths from zuby-pages
22
+ */
23
+ export declare function rewriteImportsInWrapper(wrapperContent: string, importMap: ImportMap): string;
24
+ /**
25
+ * Get all relative imports from a file
26
+ */
27
+ export declare function getRelativeImports(filePath: string): Promise<string[]>;
28
+ /**
29
+ * Get all external dependencies (not relative imports)
30
+ */
31
+ export declare function getExternalDependencies(filePath: string): Promise<string[]>;