@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
package/src/cli/setup.ts
ADDED
|
@@ -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
|