@convex-dev/static-hosting 0.1.2-beta.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.
- package/LICENSE +201 -0
- package/README.md +333 -0
- package/dist/cli/deploy.d.ts +16 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +324 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/index.d.ts +15 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +95 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +9 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +181 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/next-build.d.ts +24 -0
- package/dist/cli/next-build.d.ts.map +1 -0
- package/dist/cli/next-build.js +569 -0
- package/dist/cli/next-build.js.map +1 -0
- package/dist/cli/setup.d.ts +9 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +157 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/upload.d.ts +15 -0
- package/dist/cli/upload.d.ts.map +1 -0
- package/dist/cli/upload.js +436 -0
- package/dist/cli/upload.js.map +1 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +142 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +475 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/next.d.ts +38 -0
- package/dist/client/next.d.ts.map +1 -0
- package/dist/client/next.js +175 -0
- package/dist/client/next.js.map +1 -0
- package/dist/client/nextAdapter.d.ts +4 -0
- package/dist/client/nextAdapter.d.ts.map +1 -0
- package/dist/client/nextAdapter.js +9 -0
- package/dist/client/nextAdapter.js.map +1 -0
- package/dist/component/_generated/api.d.ts +34 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +73 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +88 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +210 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +27 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +20 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/react/index.d.ts +80 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +138 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +120 -0
- package/src/cli/deploy.ts +375 -0
- package/src/cli/index.ts +104 -0
- package/src/cli/init.ts +181 -0
- package/src/cli/next-build.ts +707 -0
- package/src/cli/setup.ts +190 -0
- package/src/cli/upload.ts +521 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +67 -0
- package/src/client/index.ts +553 -0
- package/src/client/next.ts +223 -0
- package/src/client/nextAdapter.ts +17 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +50 -0
- package/src/component/_generated/component.ts +104 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.test.ts +110 -0
- package/src/component/lib.ts +228 -0
- package/src/component/schema.ts +21 -0
- package/src/component/setup.test.ts +11 -0
- package/src/react/index.tsx +184 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI tool to build and prepare a Next.js app for Convex deployment.
|
|
4
|
+
*
|
|
5
|
+
* This tool:
|
|
6
|
+
* 1. Runs `next build` (output: standalone)
|
|
7
|
+
* 2. Collects server-side files from the standalone build
|
|
8
|
+
* 3. Generates `convex/_generatedNextServer.ts` with embedded file contents
|
|
9
|
+
* 4. Uploads static assets (.next/static/) to Convex storage
|
|
10
|
+
* 5. Ensures convex.json has node.externalPackages: ["next"]
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* npx @convex-dev/static-hosting next-build [options]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --skip-build Skip running `next build`
|
|
17
|
+
* --component <name> Convex component name (default: staticHosting)
|
|
18
|
+
* --convex-dir <path> Path to convex/ directory (default: ./convex)
|
|
19
|
+
* --prod Upload statics to production deployment
|
|
20
|
+
* --skip-upload Skip uploading static files
|
|
21
|
+
* --help Show help
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
readFileSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
existsSync,
|
|
29
|
+
statSync,
|
|
30
|
+
mkdirSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
import { join, relative, extname, resolve, dirname } from "node:path";
|
|
33
|
+
import { randomUUID } from "node:crypto";
|
|
34
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
35
|
+
|
|
36
|
+
// MIME type mapping
|
|
37
|
+
const MIME_TYPES: Record<string, string> = {
|
|
38
|
+
".html": "text/html; charset=utf-8",
|
|
39
|
+
".js": "application/javascript; charset=utf-8",
|
|
40
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
41
|
+
".css": "text/css; charset=utf-8",
|
|
42
|
+
".json": "application/json; charset=utf-8",
|
|
43
|
+
".png": "image/png",
|
|
44
|
+
".jpg": "image/jpeg",
|
|
45
|
+
".jpeg": "image/jpeg",
|
|
46
|
+
".gif": "image/gif",
|
|
47
|
+
".svg": "image/svg+xml",
|
|
48
|
+
".ico": "image/x-icon",
|
|
49
|
+
".webp": "image/webp",
|
|
50
|
+
".woff": "font/woff",
|
|
51
|
+
".woff2": "font/woff2",
|
|
52
|
+
".ttf": "font/ttf",
|
|
53
|
+
".txt": "text/plain; charset=utf-8",
|
|
54
|
+
".map": "application/json",
|
|
55
|
+
".webmanifest": "application/manifest+json",
|
|
56
|
+
".xml": "application/xml",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function getMimeType(filePath: string): string {
|
|
60
|
+
return MIME_TYPES[extname(filePath).toLowerCase()] || "application/octet-stream";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// File extensions considered text (embedded as UTF-8 strings)
|
|
64
|
+
const TEXT_EXTENSIONS = new Set([
|
|
65
|
+
".js",
|
|
66
|
+
".cjs",
|
|
67
|
+
".mjs",
|
|
68
|
+
".json",
|
|
69
|
+
".html",
|
|
70
|
+
".css",
|
|
71
|
+
".txt",
|
|
72
|
+
".xml",
|
|
73
|
+
".rsc",
|
|
74
|
+
".meta",
|
|
75
|
+
".map",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// Top-level packages to exclude from embedded node_modules
|
|
79
|
+
// (not needed for SSR, saves significant bundle space)
|
|
80
|
+
const EXCLUDED_MODULES = new Set([
|
|
81
|
+
"@img", // Sharp native image binaries (~16MB)
|
|
82
|
+
"sharp", // Image optimization (~244KB but needs @img)
|
|
83
|
+
"typescript", // TypeScript compiler (~9MB) — not needed at runtime
|
|
84
|
+
"caniuse-lite", // Browser compat data (~2.4MB) — not needed for SSR
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Directories to exclude from server file collection
|
|
88
|
+
const EXCLUDED_DIRS = new Set(["static", "cache", "diagnostics", "trace"]);
|
|
89
|
+
|
|
90
|
+
interface ParsedArgs {
|
|
91
|
+
skipBuild: boolean;
|
|
92
|
+
component: string; // File name for convex run (e.g. "staticHosting")
|
|
93
|
+
componentApi: string; // Component API name from components.xxx (e.g. "staticHosting")
|
|
94
|
+
convexDir: string;
|
|
95
|
+
prod: boolean;
|
|
96
|
+
skipUpload: boolean;
|
|
97
|
+
forceColdStart: boolean;
|
|
98
|
+
help: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
102
|
+
const result: ParsedArgs = {
|
|
103
|
+
skipBuild: false,
|
|
104
|
+
component: "staticHosting",
|
|
105
|
+
componentApi: "staticHosting",
|
|
106
|
+
convexDir: "./convex",
|
|
107
|
+
prod: false,
|
|
108
|
+
skipUpload: false,
|
|
109
|
+
forceColdStart: false,
|
|
110
|
+
help: false,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < argv.length; i++) {
|
|
114
|
+
const arg = argv[i];
|
|
115
|
+
if (arg === "--help" || arg === "-h") {
|
|
116
|
+
result.help = true;
|
|
117
|
+
} else if (arg === "--skip-build") {
|
|
118
|
+
result.skipBuild = true;
|
|
119
|
+
} else if (arg === "--force-cold-start") {
|
|
120
|
+
result.forceColdStart = true;
|
|
121
|
+
} else if (arg === "--component" || arg === "-c") {
|
|
122
|
+
result.component = argv[++i] || result.component;
|
|
123
|
+
} else if (arg === "--component-api") {
|
|
124
|
+
result.componentApi = argv[++i] || result.componentApi;
|
|
125
|
+
} else if (arg === "--convex-dir") {
|
|
126
|
+
result.convexDir = argv[++i] || result.convexDir;
|
|
127
|
+
} else if (arg === "--prod") {
|
|
128
|
+
result.prod = true;
|
|
129
|
+
} else if (arg === "--skip-upload") {
|
|
130
|
+
result.skipUpload = true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function showHelp(): void {
|
|
138
|
+
console.log(`
|
|
139
|
+
Usage: npx @convex-dev/static-hosting next-build [options]
|
|
140
|
+
|
|
141
|
+
Build a Next.js app and prepare it for Convex deployment.
|
|
142
|
+
|
|
143
|
+
This command:
|
|
144
|
+
1. Runs \`next build\` (output: standalone)
|
|
145
|
+
2. Embeds server files into a generated Convex action
|
|
146
|
+
3. Uploads static assets to Convex storage
|
|
147
|
+
4. Configures convex.json for Next.js
|
|
148
|
+
|
|
149
|
+
Options:
|
|
150
|
+
--skip-build Skip running \`next build\` (use existing .next/)
|
|
151
|
+
-c, --component <name> Convex component name (default: staticHosting)
|
|
152
|
+
--convex-dir <path> Path to convex/ directory (default: ./convex)
|
|
153
|
+
--prod Upload statics to production deployment
|
|
154
|
+
--skip-upload Skip uploading static files (upload later)
|
|
155
|
+
-h, --help Show this help message
|
|
156
|
+
|
|
157
|
+
Examples:
|
|
158
|
+
# Full build and upload
|
|
159
|
+
npx @convex-dev/static-hosting next-build --prod
|
|
160
|
+
|
|
161
|
+
# Skip build, just regenerate and upload
|
|
162
|
+
npx @convex-dev/static-hosting next-build --skip-build --prod
|
|
163
|
+
|
|
164
|
+
# Generate only, upload later
|
|
165
|
+
npx @convex-dev/static-hosting next-build --skip-upload
|
|
166
|
+
|
|
167
|
+
After running, deploy your Convex backend:
|
|
168
|
+
npx convex deploy
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// File collection
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
interface CollectedFile {
|
|
177
|
+
relativePath: string; // e.g. ".next/BUILD_ID" or "public/favicon.ico"
|
|
178
|
+
content: Buffer;
|
|
179
|
+
isText: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isTextFile(filePath: string): boolean {
|
|
183
|
+
return TEXT_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function collectFilesRecursive(
|
|
187
|
+
dir: string,
|
|
188
|
+
baseDir: string,
|
|
189
|
+
prefix: string,
|
|
190
|
+
excludeDirs: Set<string>,
|
|
191
|
+
files: CollectedFile[],
|
|
192
|
+
): void {
|
|
193
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
194
|
+
const fullPath = join(dir, entry.name);
|
|
195
|
+
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
// Skip excluded directories (only at top level under .next/)
|
|
198
|
+
const relFromBase = relative(baseDir, fullPath);
|
|
199
|
+
const topDir = relFromBase.split("/")[0];
|
|
200
|
+
if (excludeDirs.has(topDir)) continue;
|
|
201
|
+
|
|
202
|
+
// Skip .nft.json trace directories
|
|
203
|
+
if (entry.name.endsWith(".nft.json")) continue;
|
|
204
|
+
|
|
205
|
+
collectFilesRecursive(fullPath, baseDir, prefix, new Set(), files);
|
|
206
|
+
} else if (entry.isFile()) {
|
|
207
|
+
// Skip .nft.json trace files
|
|
208
|
+
if (entry.name.endsWith(".nft.json")) continue;
|
|
209
|
+
|
|
210
|
+
const relPath = prefix + "/" + relative(baseDir, fullPath);
|
|
211
|
+
files.push({
|
|
212
|
+
relativePath: relPath,
|
|
213
|
+
content: readFileSync(fullPath),
|
|
214
|
+
isText: isTextFile(fullPath),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function collectBuildFiles(standaloneDir: string): CollectedFile[] {
|
|
221
|
+
const files: CollectedFile[] = [];
|
|
222
|
+
const standaloneDotNext = join(standaloneDir, ".next");
|
|
223
|
+
|
|
224
|
+
if (!existsSync(standaloneDotNext)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Standalone build not found at ${standaloneDotNext}.\n` +
|
|
227
|
+
"Make sure your next.config has output: 'standalone'.",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Collect server-side files from .next/standalone/.next/
|
|
232
|
+
// (excluding static/, cache/, diagnostics/ and .nft.json files)
|
|
233
|
+
collectFilesRecursive(
|
|
234
|
+
standaloneDotNext,
|
|
235
|
+
standaloneDotNext,
|
|
236
|
+
".next",
|
|
237
|
+
EXCLUDED_DIRS,
|
|
238
|
+
files,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Collect public/ files if they exist
|
|
242
|
+
const publicDir = join(standaloneDir, "public");
|
|
243
|
+
if (existsSync(publicDir)) {
|
|
244
|
+
collectFilesRecursive(publicDir, publicDir, "public", new Set(), files);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Collect standalone node_modules (pruned by Next.js, minus dead weight)
|
|
248
|
+
const modulesDir = join(standaloneDir, "node_modules");
|
|
249
|
+
if (existsSync(modulesDir)) {
|
|
250
|
+
collectFilesRecursive(
|
|
251
|
+
modulesDir,
|
|
252
|
+
modulesDir,
|
|
253
|
+
"node_modules",
|
|
254
|
+
EXCLUDED_MODULES,
|
|
255
|
+
files,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Add a minimal package.json for the work directory
|
|
260
|
+
files.push({
|
|
261
|
+
relativePath: "package.json",
|
|
262
|
+
content: Buffer.from('{"type":"commonjs"}'),
|
|
263
|
+
isText: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return files;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Code generation
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
function generateServerFile(
|
|
274
|
+
files: CollectedFile[],
|
|
275
|
+
outputPath: string,
|
|
276
|
+
forceColdStart: boolean = false,
|
|
277
|
+
): void {
|
|
278
|
+
const textEntries: string[] = [];
|
|
279
|
+
const binaryEntries: string[] = [];
|
|
280
|
+
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const key = JSON.stringify(file.relativePath);
|
|
283
|
+
if (file.isText) {
|
|
284
|
+
const value = JSON.stringify(file.content.toString("utf-8"));
|
|
285
|
+
textEntries.push(` ${key}: ${value},`);
|
|
286
|
+
} else {
|
|
287
|
+
const value = JSON.stringify(file.content.toString("base64"));
|
|
288
|
+
binaryEntries.push(` ${key}: ${value},`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const handlerPreamble = forceColdStart
|
|
293
|
+
? ` // FORCE_COLD_START: wipe /tmp and handler cache on every request
|
|
294
|
+
clearColdStartCache();
|
|
295
|
+
`
|
|
296
|
+
: "";
|
|
297
|
+
|
|
298
|
+
const code = `"use node";
|
|
299
|
+
/* eslint-disable */
|
|
300
|
+
/* This file is auto-generated by @convex-dev/static-hosting next-build. Do not edit. */
|
|
301
|
+
import { internalActionGeneric } from "convex/server";
|
|
302
|
+
import { v } from "convex/values";
|
|
303
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
304
|
+
import { join, dirname } from "node:path";
|
|
305
|
+
import { createRequire } from "node:module";
|
|
306
|
+
import { toReqRes, toFetchResponse } from "fetch-to-node";
|
|
307
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
308
|
+
${forceColdStart ? 'import { rmSync } from "node:fs";' : ""}
|
|
309
|
+
|
|
310
|
+
type NodeRequestHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
311
|
+
|
|
312
|
+
const WORK_DIR = "/tmp/next-app";
|
|
313
|
+
|
|
314
|
+
const BUILD_FILES: Record<string, string> = {
|
|
315
|
+
${textEntries.join("\n")}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const BINARY_FILES: Record<string, string> = {
|
|
319
|
+
${binaryEntries.join("\n")}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let cachedHandler: NodeRequestHandler | null = null;
|
|
323
|
+
${forceColdStart ? `
|
|
324
|
+
function clearColdStartCache(): void {
|
|
325
|
+
try { rmSync(WORK_DIR, { recursive: true, force: true }); } catch {}
|
|
326
|
+
cachedHandler = null;
|
|
327
|
+
// Clear require cache for /tmp modules
|
|
328
|
+
for (const key of Object.keys(require.cache)) {
|
|
329
|
+
if (key.startsWith(WORK_DIR)) delete require.cache[key];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
` : ""}
|
|
333
|
+
function ensureFilesWritten(): void {
|
|
334
|
+
if (existsSync(join(WORK_DIR, ".next", "BUILD_ID"))) return;
|
|
335
|
+
const t0 = Date.now();
|
|
336
|
+
for (const [relPath, content] of Object.entries(BUILD_FILES)) {
|
|
337
|
+
const fullPath = join(WORK_DIR, relPath);
|
|
338
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
339
|
+
writeFileSync(fullPath, content);
|
|
340
|
+
}
|
|
341
|
+
for (const [relPath, b64] of Object.entries(BINARY_FILES)) {
|
|
342
|
+
const fullPath = join(WORK_DIR, relPath);
|
|
343
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
344
|
+
writeFileSync(fullPath, Buffer.from(b64, "base64"));
|
|
345
|
+
}
|
|
346
|
+
console.log(\`[next] Wrote \${Object.keys(BUILD_FILES).length + Object.keys(BINARY_FILES).length} files to /tmp in \${Date.now() - t0}ms\`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function bootNextServer(): Promise<NodeRequestHandler> {
|
|
350
|
+
const t0 = Date.now();
|
|
351
|
+
|
|
352
|
+
// Write all embedded files (app + node_modules) to /tmp
|
|
353
|
+
ensureFilesWritten();
|
|
354
|
+
|
|
355
|
+
const config = JSON.parse(
|
|
356
|
+
BUILD_FILES[".next/required-server-files.json"],
|
|
357
|
+
).config;
|
|
358
|
+
|
|
359
|
+
process.chdir(WORK_DIR);
|
|
360
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config);
|
|
361
|
+
|
|
362
|
+
// Load NextServer from the embedded node_modules
|
|
363
|
+
const appRequire = createRequire(join(WORK_DIR, "package.json"));
|
|
364
|
+
const NextServer = appRequire("next/dist/server/next-server").default;
|
|
365
|
+
const server = new NextServer({
|
|
366
|
+
dir: WORK_DIR,
|
|
367
|
+
dev: false,
|
|
368
|
+
conf: config,
|
|
369
|
+
});
|
|
370
|
+
await server.prepare();
|
|
371
|
+
|
|
372
|
+
console.log(\`[next] Server ready in \${Date.now() - t0}ms\`);
|
|
373
|
+
return server.getRequestHandler() as NodeRequestHandler;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export const handle = internalActionGeneric({
|
|
377
|
+
args: {
|
|
378
|
+
url: v.string(),
|
|
379
|
+
method: v.string(),
|
|
380
|
+
headers: v.array(v.array(v.string())),
|
|
381
|
+
body: v.optional(v.bytes()),
|
|
382
|
+
},
|
|
383
|
+
returns: v.object({
|
|
384
|
+
status: v.number(),
|
|
385
|
+
headers: v.array(v.array(v.string())),
|
|
386
|
+
body: v.bytes(),
|
|
387
|
+
}),
|
|
388
|
+
handler: async (ctx, args) => {
|
|
389
|
+
${handlerPreamble} if (!cachedHandler) {
|
|
390
|
+
cachedHandler = await bootNextServer();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const hasBody = !["GET", "HEAD"].includes(args.method);
|
|
394
|
+
const request = new Request(args.url, {
|
|
395
|
+
method: args.method,
|
|
396
|
+
headers: args.headers as [string, string][],
|
|
397
|
+
body: hasBody && args.body ? args.body : undefined,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const { req, res } = toReqRes(request);
|
|
401
|
+
await cachedHandler(req, res);
|
|
402
|
+
if (!res.writableEnded) res.end();
|
|
403
|
+
const response = await toFetchResponse(res);
|
|
404
|
+
|
|
405
|
+
const responseBody = await response.arrayBuffer();
|
|
406
|
+
const responseHeaders: string[][] = [];
|
|
407
|
+
response.headers.forEach((value: string, key: string) => {
|
|
408
|
+
responseHeaders.push([key, value]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
status: response.status,
|
|
413
|
+
headers: responseHeaders,
|
|
414
|
+
body: responseBody,
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
421
|
+
writeFileSync(outputPath, code);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Static file upload
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
let useProd = false;
|
|
429
|
+
|
|
430
|
+
function convexRunAsync(
|
|
431
|
+
functionPath: string,
|
|
432
|
+
args: Record<string, unknown> = {},
|
|
433
|
+
): Promise<string> {
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
const cmdArgs = [
|
|
436
|
+
"convex",
|
|
437
|
+
"run",
|
|
438
|
+
functionPath,
|
|
439
|
+
JSON.stringify(args),
|
|
440
|
+
"--typecheck=disable",
|
|
441
|
+
"--codegen=disable",
|
|
442
|
+
];
|
|
443
|
+
if (useProd) cmdArgs.push("--prod");
|
|
444
|
+
execFile("npx", cmdArgs, { encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
445
|
+
if (error) {
|
|
446
|
+
console.error("Convex run failed:", stderr || stdout);
|
|
447
|
+
reject(error);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
resolve(stdout.trim());
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
interface StaticFile {
|
|
456
|
+
path: string; // e.g. "/_next/static/chunks/main-xxx.js"
|
|
457
|
+
localPath: string;
|
|
458
|
+
contentType: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function collectStaticFiles(staticDir: string): StaticFile[] {
|
|
462
|
+
const files: StaticFile[] = [];
|
|
463
|
+
|
|
464
|
+
function walk(dir: string): void {
|
|
465
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
466
|
+
const fullPath = join(dir, entry.name);
|
|
467
|
+
if (entry.isDirectory()) {
|
|
468
|
+
walk(fullPath);
|
|
469
|
+
} else if (entry.isFile()) {
|
|
470
|
+
const relPath = relative(staticDir, fullPath).replace(/\\/g, "/");
|
|
471
|
+
files.push({
|
|
472
|
+
path: `/_next/static/${relPath}`,
|
|
473
|
+
localPath: fullPath,
|
|
474
|
+
contentType: getMimeType(fullPath),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
walk(staticDir);
|
|
481
|
+
return files;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function uploadSingleFile(
|
|
485
|
+
file: StaticFile,
|
|
486
|
+
componentName: string,
|
|
487
|
+
deploymentId: string,
|
|
488
|
+
): Promise<void> {
|
|
489
|
+
const content = readFileSync(file.localPath);
|
|
490
|
+
|
|
491
|
+
// Generate upload URL
|
|
492
|
+
const uploadUrlOutput = await convexRunAsync(
|
|
493
|
+
`${componentName}:generateUploadUrl`,
|
|
494
|
+
);
|
|
495
|
+
const uploadUrl = JSON.parse(uploadUrlOutput);
|
|
496
|
+
|
|
497
|
+
// Upload file
|
|
498
|
+
const response = await fetch(uploadUrl, {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: { "Content-Type": file.contentType },
|
|
501
|
+
body: content,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const { storageId } = (await response.json()) as { storageId: string };
|
|
505
|
+
|
|
506
|
+
// Record the asset
|
|
507
|
+
await convexRunAsync(`${componentName}:recordAsset`, {
|
|
508
|
+
path: file.path,
|
|
509
|
+
storageId,
|
|
510
|
+
contentType: file.contentType,
|
|
511
|
+
deploymentId,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function uploadStaticFiles(
|
|
516
|
+
staticDir: string,
|
|
517
|
+
componentName: string,
|
|
518
|
+
deploymentId: string,
|
|
519
|
+
concurrency: number = 5,
|
|
520
|
+
): Promise<void> {
|
|
521
|
+
const files = collectStaticFiles(staticDir);
|
|
522
|
+
if (files.length === 0) {
|
|
523
|
+
console.log(" No static files to upload.");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const total = files.length;
|
|
528
|
+
let completed = 0;
|
|
529
|
+
let failed = false;
|
|
530
|
+
|
|
531
|
+
const pending = new Set<Promise<void>>();
|
|
532
|
+
const iterator = files[Symbol.iterator]();
|
|
533
|
+
|
|
534
|
+
function enqueue(): Promise<void> | undefined {
|
|
535
|
+
if (failed) return;
|
|
536
|
+
const next = iterator.next();
|
|
537
|
+
if (next.done) return;
|
|
538
|
+
const file = next.value;
|
|
539
|
+
|
|
540
|
+
const task = uploadSingleFile(file, componentName, deploymentId).then(
|
|
541
|
+
() => {
|
|
542
|
+
completed++;
|
|
543
|
+
console.log(` [${completed}/${total}] ${file.path}`);
|
|
544
|
+
pending.delete(task);
|
|
545
|
+
},
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
task.catch(() => {
|
|
549
|
+
failed = true;
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
pending.add(task);
|
|
553
|
+
return task;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Fill initial pool
|
|
557
|
+
for (let i = 0; i < concurrency && i < total; i++) {
|
|
558
|
+
void enqueue();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Process remaining
|
|
562
|
+
while (pending.size > 0) {
|
|
563
|
+
await Promise.race(pending);
|
|
564
|
+
if (failed) {
|
|
565
|
+
await Promise.allSettled(pending);
|
|
566
|
+
throw new Error("Static file upload failed");
|
|
567
|
+
}
|
|
568
|
+
void enqueue();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Garbage collect old assets and set deployment
|
|
572
|
+
const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, {
|
|
573
|
+
currentDeploymentId: deploymentId,
|
|
574
|
+
});
|
|
575
|
+
const gcResult = JSON.parse(gcOutput);
|
|
576
|
+
const deletedCount =
|
|
577
|
+
typeof gcResult === "number" ? gcResult : gcResult.deleted;
|
|
578
|
+
if (deletedCount > 0) {
|
|
579
|
+
console.log(
|
|
580
|
+
` Cleaned up ${deletedCount} old file(s) from previous deployments`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
// convex.json configuration
|
|
587
|
+
// ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
function ensureConvexJson(convexDir: string): void {
|
|
590
|
+
// Ensure convex.json exists (Convex needs it for the functions directory)
|
|
591
|
+
const projectRoot = resolve(dirname(convexDir));
|
|
592
|
+
const convexJsonPath = join(projectRoot, "convex.json");
|
|
593
|
+
|
|
594
|
+
if (!existsSync(convexJsonPath)) {
|
|
595
|
+
writeFileSync(convexJsonPath, JSON.stringify({ functions: "convex" }, null, 2) + "\n");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
// Main
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
|
|
603
|
+
async function main(): Promise<void> {
|
|
604
|
+
const args = parseArgs(process.argv.slice(2));
|
|
605
|
+
|
|
606
|
+
if (args.help) {
|
|
607
|
+
showHelp();
|
|
608
|
+
process.exit(0);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
useProd = args.prod;
|
|
612
|
+
const convexDir = resolve(args.convexDir);
|
|
613
|
+
|
|
614
|
+
// Step 1: Run next build
|
|
615
|
+
if (!args.skipBuild) {
|
|
616
|
+
console.log("Building Next.js app...");
|
|
617
|
+
const buildResult = spawnSync("npx", ["next", "build"], {
|
|
618
|
+
stdio: "inherit",
|
|
619
|
+
});
|
|
620
|
+
if (buildResult.status !== 0) {
|
|
621
|
+
console.error("Next.js build failed.");
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
console.log("");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Step 2: Verify standalone output exists
|
|
628
|
+
const standaloneDir = resolve(".next/standalone");
|
|
629
|
+
if (!existsSync(standaloneDir)) {
|
|
630
|
+
console.error(
|
|
631
|
+
'Error: .next/standalone not found. Make sure next.config has output: "standalone".',
|
|
632
|
+
);
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Step 3: Collect build files
|
|
637
|
+
console.log("Collecting build files...");
|
|
638
|
+
const buildFiles = collectBuildFiles(standaloneDir);
|
|
639
|
+
const textCount = buildFiles.filter((f) => f.isText).length;
|
|
640
|
+
const binaryCount = buildFiles.filter((f) => !f.isText).length;
|
|
641
|
+
const totalSize = buildFiles.reduce((sum, f) => sum + f.content.length, 0);
|
|
642
|
+
console.log(
|
|
643
|
+
` ${buildFiles.length} files (${textCount} text, ${binaryCount} binary, ${(totalSize / 1024).toFixed(0)} KB total)`,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// Step 4: Generate the server file
|
|
647
|
+
const outputPath = join(convexDir, "_generatedNextServer.ts");
|
|
648
|
+
console.log(`\nGenerating ${relative(process.cwd(), outputPath)}...`);
|
|
649
|
+
generateServerFile(buildFiles, outputPath, args.forceColdStart);
|
|
650
|
+
if (args.forceColdStart) {
|
|
651
|
+
console.log(" ⚠ FORCE_COLD_START enabled — every request will cold boot");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const generatedSize = statSync(outputPath).size;
|
|
655
|
+
console.log(` Generated file: ${(generatedSize / 1024).toFixed(0)} KB`);
|
|
656
|
+
|
|
657
|
+
if (generatedSize > 30 * 1024 * 1024) {
|
|
658
|
+
console.warn(
|
|
659
|
+
"\n WARNING: Generated file exceeds 30 MB. It may exceed Convex's 32 MB bundle limit.",
|
|
660
|
+
);
|
|
661
|
+
console.warn(
|
|
662
|
+
" Consider reducing the number of pages or using dynamic imports.",
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Step 5: Ensure convex.json exists
|
|
667
|
+
ensureConvexJson(convexDir);
|
|
668
|
+
|
|
669
|
+
// Step 6: Upload static assets to Convex storage
|
|
670
|
+
if (!args.skipUpload) {
|
|
671
|
+
const envLabel = args.prod ? "production" : "development";
|
|
672
|
+
const deploymentId = randomUUID();
|
|
673
|
+
|
|
674
|
+
let staticDir = join(standaloneDir, ".next", "static");
|
|
675
|
+
if (!existsSync(staticDir)) {
|
|
676
|
+
staticDir = resolve(".next/static");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (existsSync(staticDir)) {
|
|
680
|
+
console.log(`\nUploading static assets to ${envLabel}...`);
|
|
681
|
+
try {
|
|
682
|
+
await uploadStaticFiles(staticDir, args.component, deploymentId);
|
|
683
|
+
console.log("\nStatic assets uploaded.");
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error("\nFailed to upload static assets:", error);
|
|
686
|
+
console.error(
|
|
687
|
+
"Make sure your Convex backend is deployed. You can upload later with:",
|
|
688
|
+
);
|
|
689
|
+
console.error(
|
|
690
|
+
" npx @convex-dev/static-hosting next-build --skip-build --prod",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
console.log("\nNo static directory found, skipping upload.");
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
console.log("\nSkipping uploads (--skip-upload).");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
console.log("\nDone! Next steps:");
|
|
701
|
+
console.log(" npx convex deploy");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
main().catch((error) => {
|
|
705
|
+
console.error("Error:", error);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
});
|