@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.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +333 -0
  3. package/dist/cli/deploy.d.ts +16 -0
  4. package/dist/cli/deploy.d.ts.map +1 -0
  5. package/dist/cli/deploy.js +324 -0
  6. package/dist/cli/deploy.js.map +1 -0
  7. package/dist/cli/index.d.ts +15 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +95 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/init.d.ts +9 -0
  12. package/dist/cli/init.d.ts.map +1 -0
  13. package/dist/cli/init.js +181 -0
  14. package/dist/cli/init.js.map +1 -0
  15. package/dist/cli/next-build.d.ts +24 -0
  16. package/dist/cli/next-build.d.ts.map +1 -0
  17. package/dist/cli/next-build.js +569 -0
  18. package/dist/cli/next-build.js.map +1 -0
  19. package/dist/cli/setup.d.ts +9 -0
  20. package/dist/cli/setup.d.ts.map +1 -0
  21. package/dist/cli/setup.js +157 -0
  22. package/dist/cli/setup.js.map +1 -0
  23. package/dist/cli/upload.d.ts +15 -0
  24. package/dist/cli/upload.d.ts.map +1 -0
  25. package/dist/cli/upload.js +436 -0
  26. package/dist/cli/upload.js.map +1 -0
  27. package/dist/client/_generated/_ignore.d.ts +1 -0
  28. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  29. package/dist/client/_generated/_ignore.js +3 -0
  30. package/dist/client/_generated/_ignore.js.map +1 -0
  31. package/dist/client/index.d.ts +142 -0
  32. package/dist/client/index.d.ts.map +1 -0
  33. package/dist/client/index.js +475 -0
  34. package/dist/client/index.js.map +1 -0
  35. package/dist/client/next.d.ts +38 -0
  36. package/dist/client/next.d.ts.map +1 -0
  37. package/dist/client/next.js +175 -0
  38. package/dist/client/next.js.map +1 -0
  39. package/dist/client/nextAdapter.d.ts +4 -0
  40. package/dist/client/nextAdapter.d.ts.map +1 -0
  41. package/dist/client/nextAdapter.js +9 -0
  42. package/dist/client/nextAdapter.js.map +1 -0
  43. package/dist/component/_generated/api.d.ts +34 -0
  44. package/dist/component/_generated/api.d.ts.map +1 -0
  45. package/dist/component/_generated/api.js +31 -0
  46. package/dist/component/_generated/api.js.map +1 -0
  47. package/dist/component/_generated/component.d.ts +73 -0
  48. package/dist/component/_generated/component.d.ts.map +1 -0
  49. package/dist/component/_generated/component.js +11 -0
  50. package/dist/component/_generated/component.js.map +1 -0
  51. package/dist/component/_generated/dataModel.d.ts +46 -0
  52. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  53. package/dist/component/_generated/dataModel.js +11 -0
  54. package/dist/component/_generated/dataModel.js.map +1 -0
  55. package/dist/component/_generated/server.d.ts +121 -0
  56. package/dist/component/_generated/server.d.ts.map +1 -0
  57. package/dist/component/_generated/server.js +78 -0
  58. package/dist/component/_generated/server.js.map +1 -0
  59. package/dist/component/convex.config.d.ts +3 -0
  60. package/dist/component/convex.config.d.ts.map +1 -0
  61. package/dist/component/convex.config.js +3 -0
  62. package/dist/component/convex.config.js.map +1 -0
  63. package/dist/component/lib.d.ts +88 -0
  64. package/dist/component/lib.d.ts.map +1 -0
  65. package/dist/component/lib.js +210 -0
  66. package/dist/component/lib.js.map +1 -0
  67. package/dist/component/schema.d.ts +27 -0
  68. package/dist/component/schema.d.ts.map +1 -0
  69. package/dist/component/schema.js +20 -0
  70. package/dist/component/schema.js.map +1 -0
  71. package/dist/react/index.d.ts +80 -0
  72. package/dist/react/index.d.ts.map +1 -0
  73. package/dist/react/index.js +138 -0
  74. package/dist/react/index.js.map +1 -0
  75. package/package.json +120 -0
  76. package/src/cli/deploy.ts +375 -0
  77. package/src/cli/index.ts +104 -0
  78. package/src/cli/init.ts +181 -0
  79. package/src/cli/next-build.ts +707 -0
  80. package/src/cli/setup.ts +190 -0
  81. package/src/cli/upload.ts +521 -0
  82. package/src/client/_generated/_ignore.ts +1 -0
  83. package/src/client/index.test.ts +67 -0
  84. package/src/client/index.ts +553 -0
  85. package/src/client/next.ts +223 -0
  86. package/src/client/nextAdapter.ts +17 -0
  87. package/src/client/setup.test.ts +26 -0
  88. package/src/component/_generated/api.ts +50 -0
  89. package/src/component/_generated/component.ts +104 -0
  90. package/src/component/_generated/dataModel.ts +60 -0
  91. package/src/component/_generated/server.ts +161 -0
  92. package/src/component/convex.config.ts +3 -0
  93. package/src/component/lib.test.ts +110 -0
  94. package/src/component/lib.ts +228 -0
  95. package/src/component/schema.ts +21 -0
  96. package/src/component/setup.test.ts +11 -0
  97. package/src/react/index.tsx +184 -0
  98. package/src/test.ts +18 -0
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Interactive setup wizard for Convex Static Hosting.
4
+ *
5
+ * Usage:
6
+ * npx @convex-dev/static-hosting setup
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { createInterface } from "readline";
11
+ import { join } from "path";
12
+
13
+ const rl = createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout,
16
+ });
17
+
18
+ function prompt(question: string): Promise<string> {
19
+ return new Promise((resolve) => {
20
+ rl.question(question, (answer) => resolve(answer.trim()));
21
+ });
22
+ }
23
+
24
+ function success(msg: string): void {
25
+ console.log(`✓ ${msg}`);
26
+ }
27
+
28
+ function skip(msg: string): void {
29
+ console.log(`· ${msg}`);
30
+ }
31
+
32
+ /**
33
+ * Create convex/convex.config.ts
34
+ */
35
+ function createConvexConfig(): boolean {
36
+ const configPath = join(process.cwd(), "convex", "convex.config.ts");
37
+
38
+ if (existsSync(configPath)) {
39
+ const existing = readFileSync(configPath, "utf-8");
40
+ if (existing.includes("staticHosting")) {
41
+ skip("convex/convex.config.ts (already configured)");
42
+ return false;
43
+ }
44
+ // File exists but doesn't have our component - tell user to add manually
45
+ console.log("\n⚠️ convex/convex.config.ts exists. Please add manually:");
46
+ console.log(' import staticHosting from "@convex-dev/static-hosting/convex.config";');
47
+ console.log(" app.use(staticHosting);\n");
48
+ return false;
49
+ }
50
+
51
+ writeFileSync(
52
+ configPath,
53
+ `import { defineApp } from "convex/server";
54
+ import staticHosting from "@convex-dev/static-hosting/convex.config";
55
+
56
+ const app = defineApp();
57
+ app.use(staticHosting);
58
+
59
+ export default app;
60
+ `
61
+ );
62
+ success("Created convex/convex.config.ts");
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Create convex/staticHosting.ts
68
+ */
69
+ function createStaticHostingFile(): boolean {
70
+ const filePath = join(process.cwd(), "convex", "staticHosting.ts");
71
+
72
+ if (existsSync(filePath)) {
73
+ skip("convex/staticHosting.ts (already exists)");
74
+ return false;
75
+ }
76
+
77
+ writeFileSync(
78
+ filePath,
79
+ `import { components } from "./_generated/api";
80
+ import {
81
+ exposeUploadApi,
82
+ exposeDeploymentQuery,
83
+ } from "@convex-dev/static-hosting";
84
+
85
+ // Internal functions for secure uploads (CLI only)
86
+ export const { generateUploadUrl, recordAsset, gcOldAssets, listAssets } =
87
+ exposeUploadApi(components.staticHosting);
88
+
89
+ // Public query for live reload notifications
90
+ export const { getCurrentDeployment } =
91
+ exposeDeploymentQuery(components.staticHosting);
92
+ `
93
+ );
94
+ success("Created convex/staticHosting.ts");
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * Create convex/http.ts
100
+ */
101
+ function createHttpFile(): boolean {
102
+ const filePath = join(process.cwd(), "convex", "http.ts");
103
+
104
+ if (existsSync(filePath)) {
105
+ const existing = readFileSync(filePath, "utf-8");
106
+ if (existing.includes("registerStaticRoutes")) {
107
+ skip("convex/http.ts (already configured)");
108
+ return false;
109
+ }
110
+ console.log("\n⚠️ convex/http.ts exists. Please add manually:");
111
+ console.log(' import { registerStaticRoutes } from "@convex-dev/static-hosting";');
112
+ console.log(" registerStaticRoutes(http, components.staticHosting);\n");
113
+ return false;
114
+ }
115
+
116
+ writeFileSync(
117
+ filePath,
118
+ `import { httpRouter } from "convex/server";
119
+ import { registerStaticRoutes } from "@convex-dev/static-hosting";
120
+ import { components } from "./_generated/api";
121
+
122
+ const http = httpRouter();
123
+
124
+ // Serve static files at root with SPA fallback
125
+ registerStaticRoutes(http, components.staticHosting);
126
+
127
+ export default http;
128
+ `
129
+ );
130
+ success("Created convex/http.ts");
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * Update package.json with deploy script
136
+ */
137
+ function updatePackageJson(): boolean {
138
+ const pkgPath = join(process.cwd(), "package.json");
139
+
140
+ if (!existsSync(pkgPath)) {
141
+ console.log("⚠️ No package.json found");
142
+ return false;
143
+ }
144
+
145
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
146
+ if (!pkg.scripts) pkg.scripts = {};
147
+
148
+ if (pkg.scripts.deploy) {
149
+ skip("package.json deploy script (already exists)");
150
+ return false;
151
+ }
152
+
153
+ pkg.scripts.deploy = "npx @convex-dev/static-hosting deploy";
154
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
155
+ success("Added deploy script to package.json");
156
+ return true;
157
+ }
158
+
159
+ async function main(): Promise<void> {
160
+ console.log("\n🚀 Convex Static Hosting Setup\n");
161
+
162
+ // Check for convex directory
163
+ if (!existsSync("convex")) {
164
+ mkdirSync("convex");
165
+ success("Created convex/ directory");
166
+ }
167
+
168
+ console.log("Creating files...\n");
169
+
170
+ // Create the Convex files
171
+ createConvexConfig();
172
+ createStaticHostingFile();
173
+ createHttpFile();
174
+ updatePackageJson();
175
+
176
+ // Next steps
177
+ console.log("\n✨ Setup complete!\n");
178
+ console.log("Next steps:\n");
179
+ console.log(" 1. npx convex dev # Generate types");
180
+ console.log(" 2. npm run deploy # Deploy everything\n");
181
+ console.log("Your app will be at: https://<deployment>.convex.site\n");
182
+
183
+ rl.close();
184
+ }
185
+
186
+ main().catch((err) => {
187
+ console.error("Setup failed:", err);
188
+ rl.close();
189
+ process.exit(1);
190
+ });
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool to upload static files to Convex storage.
4
+ *
5
+ * Usage:
6
+ * npx @convex-dev/static-hosting upload [options]
7
+ *
8
+ * Options:
9
+ * --dist <path> Path to dist directory (default: ./dist)
10
+ * --component <name> Convex component with upload functions (default: staticHosting)
11
+ * --prod Deploy to production deployment
12
+ * --help Show help
13
+ */
14
+
15
+ import { readFileSync, readdirSync, existsSync } from "fs";
16
+ import { join, relative, extname, resolve } from "path";
17
+ import { randomUUID } from "crypto";
18
+ import { execSync, execFile, spawnSync } from "child_process";
19
+
20
+ // MIME type mapping
21
+ const MIME_TYPES: Record<string, string> = {
22
+ ".html": "text/html; charset=utf-8",
23
+ ".js": "application/javascript; charset=utf-8",
24
+ ".mjs": "application/javascript; charset=utf-8",
25
+ ".css": "text/css; charset=utf-8",
26
+ ".json": "application/json; charset=utf-8",
27
+ ".png": "image/png",
28
+ ".jpg": "image/jpeg",
29
+ ".jpeg": "image/jpeg",
30
+ ".gif": "image/gif",
31
+ ".svg": "image/svg+xml",
32
+ ".ico": "image/x-icon",
33
+ ".webp": "image/webp",
34
+ ".woff": "font/woff",
35
+ ".woff2": "font/woff2",
36
+ ".ttf": "font/ttf",
37
+ ".txt": "text/plain; charset=utf-8",
38
+ ".map": "application/json",
39
+ ".webmanifest": "application/manifest+json",
40
+ ".xml": "application/xml",
41
+ };
42
+
43
+ function getMimeType(path: string): string {
44
+ return MIME_TYPES[extname(path).toLowerCase()] || "application/octet-stream";
45
+ }
46
+
47
+ interface ParsedArgs {
48
+ dist: string;
49
+ component: string;
50
+ prod: boolean;
51
+ build: boolean;
52
+ cdn: boolean;
53
+ cdnDeleteFunction: string;
54
+ concurrency: number;
55
+ help: boolean;
56
+ }
57
+
58
+ function parseArgs(args: string[]): ParsedArgs {
59
+ const result: ParsedArgs = {
60
+ dist: "./dist",
61
+ component: "staticHosting",
62
+ prod: false, // Default to dev, use --prod for production
63
+ build: false,
64
+ cdn: false,
65
+ cdnDeleteFunction: "",
66
+ concurrency: 5,
67
+ help: false,
68
+ };
69
+
70
+ for (let i = 0; i < args.length; i++) {
71
+ const arg = args[i];
72
+ if (arg === "--help" || arg === "-h") {
73
+ result.help = true;
74
+ } else if (arg === "--dist" || arg === "-d") {
75
+ result.dist = args[++i] || result.dist;
76
+ } else if (arg === "--component" || arg === "-c") {
77
+ result.component = args[++i] || result.component;
78
+ } else if (arg === "--prod") {
79
+ result.prod = true;
80
+ } else if (arg === "--no-prod" || arg === "--dev") {
81
+ result.prod = false;
82
+ } else if (arg === "--build" || arg === "-b") {
83
+ result.build = true;
84
+ } else if (arg === "--cdn") {
85
+ result.cdn = true;
86
+ } else if (arg === "--cdn-delete-function") {
87
+ result.cdnDeleteFunction = args[++i] || result.cdnDeleteFunction;
88
+ } else if (arg === "--concurrency" || arg === "-j") {
89
+ const val = parseInt(args[++i], 10);
90
+ if (val > 0) result.concurrency = val;
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ function showHelp(): void {
98
+ console.log(`
99
+ Usage: npx @convex-dev/static-hosting upload [options]
100
+
101
+ Upload static files from a dist directory to Convex storage.
102
+
103
+ Options:
104
+ -d, --dist <path> Path to dist directory (default: ./dist)
105
+ -c, --component <name> Convex component with upload functions (default: staticHosting)
106
+ --prod Deploy to production deployment
107
+ -b, --build Run 'npm run build' with correct VITE_CONVEX_URL before uploading
108
+ --cdn Upload non-HTML assets to convex-fs CDN instead of Convex storage
109
+ --cdn-delete-function <name> Convex function to delete CDN blobs (default: <component>:deleteCdnBlobs)
110
+ -j, --concurrency <n> Number of parallel uploads (default: 5)
111
+ -h, --help Show this help message
112
+
113
+ Examples:
114
+ # Upload to Convex storage
115
+ npx @convex-dev/static-hosting upload
116
+ npx @convex-dev/static-hosting upload --dist ./build --prod
117
+ npx @convex-dev/static-hosting upload --build --prod
118
+
119
+ # Upload with CDN (non-HTML files served from CDN)
120
+ npx @convex-dev/static-hosting upload --cdn --prod
121
+ `);
122
+ }
123
+
124
+ // Global flag for production mode
125
+ let useProd = true;
126
+
127
+ function _convexRun(
128
+ functionPath: string,
129
+ args: Record<string, unknown> = {},
130
+ ): string {
131
+ const argsJson = JSON.stringify(args);
132
+ const prodFlag = useProd ? "--prod" : "";
133
+ const cmd = `npx convex run "${functionPath}" '${argsJson}' ${prodFlag} --typecheck=disable --codegen=disable`;
134
+ try {
135
+ const result = execSync(cmd, {
136
+ encoding: "utf-8",
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ });
139
+ return result.trim();
140
+ } catch (error) {
141
+ const execError = error as { stderr?: string; stdout?: string };
142
+ console.error("Convex run failed:", execError.stderr || execError.stdout);
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ function convexRunAsync(
148
+ functionPath: string,
149
+ args: Record<string, unknown> = {},
150
+ ): Promise<string> {
151
+ return new Promise((resolve, reject) => {
152
+ const cmdArgs = [
153
+ "convex",
154
+ "run",
155
+ functionPath,
156
+ JSON.stringify(args),
157
+ "--typecheck=disable",
158
+ "--codegen=disable",
159
+ ];
160
+ if (useProd) cmdArgs.push("--prod");
161
+ execFile("npx", cmdArgs, { encoding: "utf-8" }, (error, stdout, stderr) => {
162
+ if (error) {
163
+ console.error("Convex run failed:", stderr || stdout);
164
+ reject(error);
165
+ return;
166
+ }
167
+ resolve(stdout.trim());
168
+ });
169
+ });
170
+ }
171
+
172
+ async function uploadWithConcurrency(
173
+ files: Array<{ path: string; localPath: string; contentType: string }>,
174
+ componentName: string,
175
+ deploymentId: string,
176
+ useCdn: boolean,
177
+ siteUrl: string | null,
178
+ concurrency: number,
179
+ ): Promise<void> {
180
+ const total = files.length;
181
+
182
+ // Separate CDN and storage files
183
+ const cdnFiles: typeof files = [];
184
+ const storageFiles: typeof files = [];
185
+ for (const file of files) {
186
+ const isHtml = file.contentType.startsWith("text/html");
187
+ if (useCdn && !isHtml && siteUrl) {
188
+ cdnFiles.push(file);
189
+ } else {
190
+ storageFiles.push(file);
191
+ }
192
+ }
193
+
194
+ // Upload storage files using batch operations
195
+ let completed = 0;
196
+ const allAssets: Array<{
197
+ path: string;
198
+ storageId?: string;
199
+ blobId?: string;
200
+ contentType: string;
201
+ deploymentId: string;
202
+ }> = [];
203
+
204
+ if (storageFiles.length > 0) {
205
+ // Step 1: Generate all upload URLs in one batch call
206
+ console.log(` Generating ${storageFiles.length} upload URLs...`);
207
+ const urlsOutput = await convexRunAsync(
208
+ `${componentName}:generateUploadUrls`,
209
+ { count: storageFiles.length },
210
+ );
211
+ const uploadUrls: string[] = JSON.parse(urlsOutput);
212
+
213
+ // Step 2: Upload all files in parallel via fetch
214
+ const storageIds: string[] = new Array(storageFiles.length);
215
+ const pending = new Set<Promise<void>>();
216
+
217
+ for (let i = 0; i < storageFiles.length; i++) {
218
+ const idx = i;
219
+ const file = storageFiles[idx];
220
+ const task = (async () => {
221
+ const content = readFileSync(file.localPath);
222
+ const response = await fetch(uploadUrls[idx], {
223
+ method: "POST",
224
+ headers: { "Content-Type": file.contentType },
225
+ body: content,
226
+ });
227
+ const { storageId } = (await response.json()) as { storageId: string };
228
+ storageIds[idx] = storageId;
229
+ completed++;
230
+ const isHtml = file.contentType.startsWith("text/html");
231
+ console.log(` [${completed}/${total}] ${file.path} (${isHtml ? "storage/html" : "storage"})`);
232
+ })().then(() => { pending.delete(task); });
233
+ pending.add(task);
234
+ if (pending.size >= concurrency) {
235
+ await Promise.race(pending);
236
+ }
237
+ }
238
+ await Promise.all(pending);
239
+
240
+ for (let i = 0; i < storageFiles.length; i++) {
241
+ allAssets.push({
242
+ path: storageFiles[i].path,
243
+ storageId: storageIds[i],
244
+ contentType: storageFiles[i].contentType,
245
+ deploymentId,
246
+ });
247
+ }
248
+ }
249
+
250
+ // Upload CDN files (still uses per-file calls since CDN has its own upload endpoint)
251
+ if (cdnFiles.length > 0 && siteUrl) {
252
+ const pending = new Set<Promise<void>>();
253
+ for (const file of cdnFiles) {
254
+ const task = (async () => {
255
+ const content = readFileSync(file.localPath);
256
+ const uploadResponse = await fetch(`${siteUrl}/fs/upload`, {
257
+ method: "POST",
258
+ headers: { "Content-Type": file.contentType },
259
+ body: content,
260
+ });
261
+ if (!uploadResponse.ok) {
262
+ throw new Error(
263
+ `CDN upload failed for ${file.path}: ${uploadResponse.status}`,
264
+ );
265
+ }
266
+ const { blobId } = (await uploadResponse.json()) as { blobId: string };
267
+ allAssets.push({
268
+ path: file.path,
269
+ blobId,
270
+ contentType: file.contentType,
271
+ deploymentId,
272
+ });
273
+ completed++;
274
+ console.log(` [${completed}/${total}] ${file.path} (cdn)`);
275
+ })().then(() => { pending.delete(task); });
276
+ pending.add(task);
277
+ if (pending.size >= concurrency) {
278
+ await Promise.race(pending);
279
+ }
280
+ }
281
+ await Promise.all(pending);
282
+ }
283
+
284
+ // Step 3: Record all assets in one batch call
285
+ if (allAssets.length > 0) {
286
+ console.log(" Recording assets...");
287
+ // recordAssets only handles storageId assets; CDN assets need individual recording
288
+ const storageAssets = allAssets.filter((a) => a.storageId);
289
+ const cdnAssets = allAssets.filter((a) => a.blobId);
290
+
291
+ if (storageAssets.length > 0) {
292
+ await convexRunAsync(`${componentName}:recordAssets`, {
293
+ assets: storageAssets.map((a) => ({
294
+ path: a.path,
295
+ storageId: a.storageId!,
296
+ contentType: a.contentType,
297
+ deploymentId: a.deploymentId,
298
+ })),
299
+ });
300
+ }
301
+
302
+ // CDN assets still need individual recording (they use blobId not storageId)
303
+ for (const asset of cdnAssets) {
304
+ await convexRunAsync(`${componentName}:recordAsset`, {
305
+ path: asset.path,
306
+ blobId: asset.blobId,
307
+ contentType: asset.contentType,
308
+ deploymentId: asset.deploymentId,
309
+ });
310
+ }
311
+ }
312
+ }
313
+
314
+ function collectFiles(
315
+ dir: string,
316
+ baseDir: string,
317
+ ): Array<{ path: string; localPath: string; contentType: string }> {
318
+ const files: Array<{
319
+ path: string;
320
+ localPath: string;
321
+ contentType: string;
322
+ }> = [];
323
+
324
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
325
+ const fullPath = join(dir, entry.name);
326
+ if (entry.isDirectory()) {
327
+ files.push(...collectFiles(fullPath, baseDir));
328
+ } else if (entry.isFile()) {
329
+ files.push({
330
+ path: "/" + relative(baseDir, fullPath).replace(/\\/g, "/"),
331
+ localPath: fullPath,
332
+ contentType: getMimeType(fullPath),
333
+ });
334
+ }
335
+ }
336
+ return files;
337
+ }
338
+
339
+ async function main(): Promise<void> {
340
+ const args = parseArgs(process.argv.slice(2));
341
+
342
+ if (args.help) {
343
+ showHelp();
344
+ process.exit(0);
345
+ }
346
+
347
+ // Set global prod flag
348
+ useProd = args.prod;
349
+
350
+ // Run build if requested
351
+ if (args.build) {
352
+ let convexUrl: string | null = null;
353
+
354
+ if (useProd) {
355
+ // Get production URL from convex dashboard
356
+ try {
357
+ const result = execSync("npx convex dashboard --prod --no-open", {
358
+ stdio: "pipe",
359
+ encoding: "utf-8",
360
+ });
361
+ const match = result.match(/dashboard\.convex\.dev\/d\/([a-z0-9-]+)/i);
362
+ if (match) {
363
+ convexUrl = `https://${match[1]}.convex.cloud`;
364
+ }
365
+ } catch {
366
+ console.error("Could not get production Convex URL.");
367
+ console.error(
368
+ "Make sure you have deployed to production: npx convex deploy",
369
+ );
370
+ process.exit(1);
371
+ }
372
+ } else {
373
+ // Get dev URL from .env.local
374
+ if (existsSync(".env.local")) {
375
+ const envContent = readFileSync(".env.local", "utf-8");
376
+ const match = envContent.match(/(?:VITE_)?CONVEX_URL=(.+)/);
377
+ if (match) {
378
+ convexUrl = match[1].trim();
379
+ }
380
+ }
381
+ }
382
+
383
+ if (!convexUrl) {
384
+ console.error("Could not determine Convex URL for build.");
385
+ process.exit(1);
386
+ }
387
+
388
+ const envLabel = useProd ? "production" : "development";
389
+ console.log(`🔨 Building for ${envLabel}...`);
390
+ console.log(` VITE_CONVEX_URL=${convexUrl}`);
391
+ console.log("");
392
+
393
+ const buildResult = spawnSync("npm", ["run", "build"], {
394
+ stdio: "inherit",
395
+ env: { ...process.env, VITE_CONVEX_URL: convexUrl },
396
+ });
397
+
398
+ if (buildResult.status !== 0) {
399
+ console.error("Build failed.");
400
+ process.exit(1);
401
+ }
402
+
403
+ console.log("");
404
+ }
405
+
406
+ const distDir = resolve(args.dist);
407
+ const componentName = args.component;
408
+ const useCdn = args.cdn;
409
+
410
+ // Convex storage deployment
411
+
412
+ if (!existsSync(distDir)) {
413
+ console.error(`Error: dist directory not found: ${distDir}`);
414
+ console.error(
415
+ "Run your build command first (e.g., 'npm run build' or add --build flag)",
416
+ );
417
+ process.exit(1);
418
+ }
419
+
420
+ // If CDN mode, we need the site URL for uploading to convex-fs
421
+ let siteUrl: string | null = null;
422
+ if (useCdn) {
423
+ siteUrl = getConvexSiteUrl(useProd);
424
+ if (!siteUrl) {
425
+ console.error("Error: Could not determine Convex site URL for CDN uploads.");
426
+ console.error("Make sure your Convex deployment is running.");
427
+ process.exit(1);
428
+ }
429
+ }
430
+
431
+ const deploymentId = randomUUID();
432
+ const files = collectFiles(distDir, distDir);
433
+
434
+ const envLabel = useProd ? "production" : "development";
435
+ console.log(`🚀 Deploying to ${envLabel} environment`);
436
+ if (useCdn) {
437
+ console.log("☁️ CDN mode: non-HTML assets will be uploaded to convex-fs");
438
+ }
439
+ console.log("🔒 Using secure internal functions (requires Convex CLI auth)");
440
+ console.log(
441
+ `Uploading ${files.length} files with deployment ID: ${deploymentId}`,
442
+ );
443
+ console.log(`Component: ${componentName}`);
444
+ console.log("");
445
+
446
+ try {
447
+ await uploadWithConcurrency(
448
+ files,
449
+ componentName,
450
+ deploymentId,
451
+ useCdn,
452
+ siteUrl,
453
+ args.concurrency,
454
+ );
455
+ } catch {
456
+ console.error("Upload failed.");
457
+ process.exit(1);
458
+ }
459
+
460
+ console.log("");
461
+
462
+ // Garbage collect old files
463
+ const gcOutput = await convexRunAsync(`${componentName}:gcOldAssets`, {
464
+ currentDeploymentId: deploymentId,
465
+ });
466
+ const gcResult = JSON.parse(gcOutput);
467
+
468
+ // Handle both old format (number) and new format ({ deleted, blobIds })
469
+ const deletedCount = typeof gcResult === "number" ? gcResult : gcResult.deleted;
470
+ const oldBlobIds: string[] = typeof gcResult === "object" && gcResult.blobIds ? gcResult.blobIds : [];
471
+
472
+ if (deletedCount > 0) {
473
+ console.log(`Cleaned up ${deletedCount} old storage file(s) from previous deployments`);
474
+ }
475
+
476
+ // Clean up old CDN blobs if any
477
+ if (oldBlobIds.length > 0) {
478
+ const cdnDeleteFn = args.cdnDeleteFunction || `${componentName}:deleteCdnBlobs`;
479
+ try {
480
+ await convexRunAsync(cdnDeleteFn, { blobIds: oldBlobIds });
481
+ console.log(`Cleaned up ${oldBlobIds.length} old CDN blob(s) from previous deployments`);
482
+ } catch {
483
+ console.warn(`Warning: Could not delete old CDN blobs. Make sure ${cdnDeleteFn} is defined.`);
484
+ }
485
+ }
486
+
487
+ console.log("");
488
+ console.log("✨ Upload complete!");
489
+
490
+ // Show the deployment URL
491
+ const deployedSiteUrl = getConvexSiteUrl(useProd);
492
+ if (deployedSiteUrl) {
493
+ console.log("");
494
+ console.log(`Your app is now available at: ${deployedSiteUrl}`);
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Get the Convex site URL (.convex.site) from the cloud URL
500
+ */
501
+ function getConvexSiteUrl(prod: boolean): string | null {
502
+ try {
503
+ const envFlag = prod ? "--prod" : "";
504
+ const result = execSync(`npx convex env get CONVEX_CLOUD_URL ${envFlag}`, {
505
+ stdio: "pipe",
506
+ encoding: "utf-8",
507
+ });
508
+ const cloudUrl = result.trim();
509
+ if (cloudUrl && cloudUrl.includes(".convex.cloud")) {
510
+ return cloudUrl.replace(".convex.cloud", ".convex.site");
511
+ }
512
+ } catch {
513
+ // Ignore errors
514
+ }
515
+ return null;
516
+ }
517
+
518
+ main().catch((error) => {
519
+ console.error("Upload failed:", error);
520
+ process.exit(1);
521
+ });
@@ -0,0 +1 @@
1
+ // This is only here so convex-test can detect a _generated folder