@just-be/deploy 0.1.1 ā 0.2.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/README.md +10 -3
- package/index.ts +126 -57
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ bunx @just-be/deploy
|
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
The interactive mode will ask you for:
|
|
36
|
+
|
|
36
37
|
- Subdomain name
|
|
37
38
|
- R2 path prefix
|
|
38
39
|
- Local directory to upload
|
|
@@ -58,11 +59,13 @@ bunx @just-be/deploy --subdomain=NAME --path=PATH --dir=DIR [--spa] [--fallback=
|
|
|
58
59
|
### Examples
|
|
59
60
|
|
|
60
61
|
**Interactive deployment:**
|
|
62
|
+
|
|
61
63
|
```bash
|
|
62
64
|
bunx @just-be/deploy
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
**Deploy a static site:**
|
|
68
|
+
|
|
66
69
|
```bash
|
|
67
70
|
bunx @just-be/deploy \
|
|
68
71
|
--subdomain=portfolio \
|
|
@@ -71,6 +74,7 @@ bunx @just-be/deploy \
|
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
**Deploy a single-page application:**
|
|
77
|
+
|
|
74
78
|
```bash
|
|
75
79
|
bunx @just-be/deploy \
|
|
76
80
|
--subdomain=myapp \
|
|
@@ -80,6 +84,7 @@ bunx @just-be/deploy \
|
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
**Deploy with custom 404 page:**
|
|
87
|
+
|
|
83
88
|
```bash
|
|
84
89
|
bunx @just-be/deploy \
|
|
85
90
|
--subdomain=docs \
|
|
@@ -90,9 +95,9 @@ bunx @just-be/deploy \
|
|
|
90
95
|
|
|
91
96
|
## How It Works
|
|
92
97
|
|
|
93
|
-
1. **Parses arguments** using
|
|
98
|
+
1. **Parses arguments** using a custom argument parser
|
|
94
99
|
2. **Prompts for missing values** if any required arguments are not provided
|
|
95
|
-
3. **Validates configuration** using Zod schemas from `@just-be/wildcard
|
|
100
|
+
3. **Validates configuration** using Zod schemas from `@just-be/wildcard`
|
|
96
101
|
4. **Scans** the target directory for all files
|
|
97
102
|
5. **Uploads** each file to R2 at `content-bucket/{path}/{relative-path}`
|
|
98
103
|
6. **Creates** a KV entry with routing configuration for the subdomain
|
|
@@ -101,6 +106,7 @@ bunx @just-be/deploy \
|
|
|
101
106
|
## Configuration
|
|
102
107
|
|
|
103
108
|
The script expects your wildcard service to use:
|
|
109
|
+
|
|
104
110
|
- **R2 Bucket**: `content-bucket`
|
|
105
111
|
- **KV Binding**: `ROUTING_RULES`
|
|
106
112
|
- **Wrangler Config**: `services/wildcard/wrangler.toml`
|
|
@@ -108,13 +114,14 @@ The script expects your wildcard service to use:
|
|
|
108
114
|
## Validation
|
|
109
115
|
|
|
110
116
|
Configuration is validated using Zod schemas to ensure:
|
|
117
|
+
|
|
111
118
|
- SPA mode and fallback file are not used together
|
|
112
119
|
- R2 path is not empty
|
|
113
120
|
- All required fields are present
|
|
114
121
|
|
|
115
122
|
## Related Packages
|
|
116
123
|
|
|
117
|
-
- [`@just-be/wildcard
|
|
124
|
+
- [`@just-be/wildcard`](../wildcard) - Shared Zod schemas and routing handlers for wildcard subdomain configuration
|
|
118
125
|
|
|
119
126
|
## License
|
|
120
127
|
|
package/index.ts
CHANGED
|
@@ -15,13 +15,21 @@
|
|
|
15
15
|
import { $ } from "bun";
|
|
16
16
|
import { readdir, stat } from "fs/promises";
|
|
17
17
|
import { join, relative } from "path";
|
|
18
|
-
import {
|
|
18
|
+
import { confirm, intro, outro, spinner, text } from "@clack/prompts";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { StaticConfigSchema, type StaticConfig, isValidSubdomain } from "@just-be/wildcard";
|
|
21
21
|
|
|
22
22
|
const BUCKET_NAME = "content-bucket";
|
|
23
23
|
const WRANGLER_CONFIG = "services/wildcard/wrangler.toml";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Run wrangler command using bunx to ensure it resolves from package dependencies
|
|
27
|
+
*/
|
|
28
|
+
function wrangler(command: TemplateStringsArray, ...values: unknown[]) {
|
|
29
|
+
const cmd = String.raw(command, ...values);
|
|
30
|
+
return $`bunx wrangler ${cmd}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
/**
|
|
26
34
|
* Schema for CLI arguments
|
|
27
35
|
*/
|
|
@@ -36,7 +44,7 @@ const CliArgsSchema = z.object({
|
|
|
36
44
|
type CliArgs = z.infer<typeof CliArgsSchema>;
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
|
-
* Parse command-line arguments from Bun.argv
|
|
47
|
+
* Parse command-line arguments from Bun.argv using a custom parser
|
|
40
48
|
*/
|
|
41
49
|
function parseCliArgs(): CliArgs {
|
|
42
50
|
const args: Record<string, string | boolean> = {};
|
|
@@ -61,32 +69,6 @@ interface DeployConfig extends Omit<StaticConfig, "type"> {
|
|
|
61
69
|
dir: string;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
/**
|
|
65
|
-
* Prompt user for input
|
|
66
|
-
*/
|
|
67
|
-
async function prompt(question: string, defaultValue?: string): Promise<string> {
|
|
68
|
-
const rl = createInterface({
|
|
69
|
-
input: process.stdin,
|
|
70
|
-
output: process.stdout,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return new Promise((resolve) => {
|
|
74
|
-
const promptText = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
75
|
-
rl.question(promptText, (answer) => {
|
|
76
|
-
rl.close();
|
|
77
|
-
resolve(answer.trim() || defaultValue || "");
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Prompt user for confirmation
|
|
84
|
-
*/
|
|
85
|
-
async function confirm(question: string): Promise<boolean> {
|
|
86
|
-
const answer = await prompt(`${question} (y/n)`, "n");
|
|
87
|
-
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
72
|
/**
|
|
91
73
|
* Parse command-line arguments
|
|
92
74
|
*/
|
|
@@ -97,33 +79,64 @@ async function getConfig(): Promise<DeployConfig> {
|
|
|
97
79
|
const needsInteractive = !values.subdomain || !values.path || !values.dir;
|
|
98
80
|
|
|
99
81
|
if (needsInteractive) {
|
|
100
|
-
|
|
82
|
+
intro("Interactive deployment setup");
|
|
101
83
|
}
|
|
102
84
|
|
|
103
85
|
let subdomain =
|
|
104
|
-
values.subdomain ||
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
values.subdomain ||
|
|
87
|
+
((await text({
|
|
88
|
+
message: "Subdomain name (e.g., 'myapp' for myapp.just-be.dev)",
|
|
89
|
+
validate: (value) => {
|
|
90
|
+
if (!value) return "Subdomain is required";
|
|
91
|
+
if (!isValidSubdomain(value as string)) {
|
|
92
|
+
return "Invalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters.";
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
})) as string);
|
|
96
|
+
|
|
97
|
+
// Validate subdomain format (in case it came from CLI args)
|
|
107
98
|
if (!isValidSubdomain(subdomain)) {
|
|
108
99
|
console.error(
|
|
109
|
-
"\
|
|
100
|
+
"\nInvalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters."
|
|
110
101
|
);
|
|
111
102
|
process.exit(1);
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
const path =
|
|
115
|
-
|
|
105
|
+
const path =
|
|
106
|
+
values.path ||
|
|
107
|
+
((await text({
|
|
108
|
+
message: "R2 path prefix (e.g., 'apps/myapp')",
|
|
109
|
+
validate: (value) => {
|
|
110
|
+
if (!value) return "Path is required";
|
|
111
|
+
},
|
|
112
|
+
})) as string);
|
|
113
|
+
|
|
114
|
+
const dir =
|
|
115
|
+
values.dir ||
|
|
116
|
+
((await text({
|
|
117
|
+
message: "Local directory to upload",
|
|
118
|
+
placeholder: "./dist",
|
|
119
|
+
defaultValue: "./dist",
|
|
120
|
+
})) as string);
|
|
116
121
|
|
|
117
122
|
let spa = values.spa;
|
|
118
123
|
let fallback = values.fallback;
|
|
119
124
|
|
|
120
125
|
// Only prompt for spa/fallback if not provided
|
|
121
126
|
if (needsInteractive && spa === undefined && fallback === undefined) {
|
|
122
|
-
spa = await confirm(
|
|
127
|
+
spa = (await confirm({
|
|
128
|
+
message: "Enable SPA mode? (all routes serve index.html)",
|
|
129
|
+
})) as boolean;
|
|
123
130
|
if (!spa) {
|
|
124
|
-
const useFallback = await confirm(
|
|
131
|
+
const useFallback = (await confirm({
|
|
132
|
+
message: "Use a custom fallback file for 404s?",
|
|
133
|
+
})) as boolean;
|
|
125
134
|
if (useFallback) {
|
|
126
|
-
fallback = await
|
|
135
|
+
fallback = (await text({
|
|
136
|
+
message: "Fallback file name",
|
|
137
|
+
placeholder: "404.html",
|
|
138
|
+
defaultValue: "404.html",
|
|
139
|
+
})) as string;
|
|
127
140
|
}
|
|
128
141
|
}
|
|
129
142
|
}
|
|
@@ -138,7 +151,7 @@ async function getConfig(): Promise<DeployConfig> {
|
|
|
138
151
|
|
|
139
152
|
const result = StaticConfigSchema.safeParse(staticConfig);
|
|
140
153
|
if (!result.success) {
|
|
141
|
-
console.error("\
|
|
154
|
+
console.error("\nInvalid configuration:");
|
|
142
155
|
console.error(result.error.format());
|
|
143
156
|
process.exit(1);
|
|
144
157
|
}
|
|
@@ -176,9 +189,28 @@ async function findFiles(dir: string): Promise<string[]> {
|
|
|
176
189
|
|
|
177
190
|
/**
|
|
178
191
|
* Upload a file to R2
|
|
192
|
+
* @returns true if upload succeeded, false otherwise
|
|
179
193
|
*/
|
|
180
|
-
async function uploadToR2(localPath: string, r2Key: string): Promise<
|
|
181
|
-
|
|
194
|
+
async function uploadToR2(localPath: string, r2Key: string): Promise<boolean> {
|
|
195
|
+
try {
|
|
196
|
+
await wrangler`r2 object put ${BUCKET_NAME}/${r2Key} --file ${localPath}`;
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error(`\nFailed to upload ${localPath}:`, error);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Validate KV access by attempting to list keys
|
|
206
|
+
*/
|
|
207
|
+
async function validateKVAccess(): Promise<boolean> {
|
|
208
|
+
try {
|
|
209
|
+
await wrangler`kv key list --binding ROUTING_RULES --config ${WRANGLER_CONFIG}`.quiet();
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
182
214
|
}
|
|
183
215
|
|
|
184
216
|
/**
|
|
@@ -194,7 +226,7 @@ async function createKVEntry(subdomain: string, config: DeployConfig): Promise<v
|
|
|
194
226
|
|
|
195
227
|
const configJson = JSON.stringify(routingConfig);
|
|
196
228
|
|
|
197
|
-
await
|
|
229
|
+
await wrangler`kv key put --binding ROUTING_RULES ${subdomain} ${configJson} --config ${WRANGLER_CONFIG}`;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
232
|
/**
|
|
@@ -203,7 +235,7 @@ async function createKVEntry(subdomain: string, config: DeployConfig): Promise<v
|
|
|
203
235
|
async function deploy() {
|
|
204
236
|
const config = await getConfig();
|
|
205
237
|
|
|
206
|
-
console.log("\
|
|
238
|
+
console.log("\nDeploy Configuration:");
|
|
207
239
|
console.log(` Subdomain: ${config.subdomain}.just-be.dev`);
|
|
208
240
|
console.log(` R2 Path: ${config.path}`);
|
|
209
241
|
console.log(` Local Directory: ${config.dir}`);
|
|
@@ -218,46 +250,83 @@ async function deploy() {
|
|
|
218
250
|
try {
|
|
219
251
|
const dirStat = await stat(config.dir);
|
|
220
252
|
if (!dirStat.isDirectory()) {
|
|
221
|
-
console.error(
|
|
253
|
+
console.error(`Error: ${config.dir} is not a directory`);
|
|
222
254
|
process.exit(1);
|
|
223
255
|
}
|
|
224
256
|
} catch {
|
|
225
|
-
console.error(
|
|
257
|
+
console.error(`Error: Directory ${config.dir} does not exist`);
|
|
226
258
|
process.exit(1);
|
|
227
259
|
}
|
|
228
260
|
|
|
229
261
|
// Find all files to upload
|
|
230
|
-
|
|
262
|
+
const s = spinner();
|
|
263
|
+
s.start(`Scanning files in: ${config.dir}`);
|
|
231
264
|
const filePaths = await findFiles(config.dir);
|
|
232
|
-
|
|
265
|
+
s.stop(`Found ${filePaths.length} files to upload`);
|
|
233
266
|
|
|
234
267
|
if (filePaths.length === 0) {
|
|
235
|
-
console.error("
|
|
268
|
+
console.error("No files found to upload");
|
|
236
269
|
process.exit(1);
|
|
237
270
|
}
|
|
238
271
|
|
|
272
|
+
// Validate KV access before starting uploads
|
|
273
|
+
s.start("Validating KV access");
|
|
274
|
+
const hasKVAccess = await validateKVAccess();
|
|
275
|
+
if (!hasKVAccess) {
|
|
276
|
+
s.stop("Error: Cannot access KV namespace");
|
|
277
|
+
console.error("Check wrangler configuration and permissions.");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
s.stop("KV access validated");
|
|
281
|
+
|
|
239
282
|
// Upload files to R2
|
|
240
283
|
let uploadCount = 0;
|
|
284
|
+
const failedUploads: string[] = [];
|
|
285
|
+
|
|
241
286
|
for (const filePath of filePaths) {
|
|
242
287
|
const relativePath = relative(config.dir, filePath);
|
|
243
288
|
const r2Key = `${config.path}/${relativePath}`;
|
|
244
289
|
|
|
245
|
-
|
|
246
|
-
await uploadToR2(filePath, r2Key);
|
|
247
|
-
|
|
290
|
+
s.start(`Uploading ${relativePath}`);
|
|
291
|
+
const success = await uploadToR2(filePath, r2Key);
|
|
292
|
+
|
|
293
|
+
if (success) {
|
|
294
|
+
uploadCount++;
|
|
295
|
+
s.stop(`Uploaded ${relativePath}`);
|
|
296
|
+
} else {
|
|
297
|
+
s.stop(`Failed to upload ${relativePath}`);
|
|
298
|
+
failedUploads.push(relativePath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Report upload results
|
|
303
|
+
if (failedUploads.length > 0) {
|
|
304
|
+
console.error(`\nFailed to upload ${failedUploads.length} file(s):`);
|
|
305
|
+
for (const file of failedUploads) {
|
|
306
|
+
console.error(` - ${file}`);
|
|
307
|
+
}
|
|
308
|
+
process.exit(1);
|
|
248
309
|
}
|
|
249
310
|
|
|
250
|
-
console.log(`\
|
|
311
|
+
console.log(`\nUploaded ${uploadCount} files to R2\n`);
|
|
251
312
|
|
|
252
313
|
// Create KV entry for routing
|
|
253
|
-
|
|
254
|
-
|
|
314
|
+
s.start(`Creating KV routing entry for subdomain: ${config.subdomain}`);
|
|
315
|
+
try {
|
|
316
|
+
await createKVEntry(config.subdomain, config);
|
|
317
|
+
s.stop("KV routing entry created");
|
|
318
|
+
} catch (error) {
|
|
319
|
+
s.stop("Error: Failed to create KV entry");
|
|
320
|
+
console.error("Files were uploaded but routing is not configured.");
|
|
321
|
+
console.error(`Uploaded files location: ${config.path}`);
|
|
322
|
+
console.error(`Error:`, error);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
255
325
|
|
|
256
|
-
|
|
257
|
-
console.log(`\nš Your site is available at: https://${config.subdomain}.just-be.dev`);
|
|
326
|
+
outro(`Your site is available at: https://${config.subdomain}.just-be.dev`);
|
|
258
327
|
}
|
|
259
328
|
|
|
260
329
|
deploy().catch((error) => {
|
|
261
|
-
console.error("\
|
|
330
|
+
console.error("\nFatal error:", error);
|
|
262
331
|
process.exit(1);
|
|
263
332
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@just-be/deploy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Deploy static files to Cloudflare R2 and configure subdomain routing",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"deploy": "index.ts"
|
|
8
|
+
},
|
|
7
9
|
"files": [
|
|
8
10
|
"index.ts"
|
|
9
11
|
],
|
|
@@ -17,13 +19,18 @@
|
|
|
17
19
|
"license": "MIT",
|
|
18
20
|
"repository": {
|
|
19
21
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/
|
|
22
|
+
"url": "git+https://github.com/just-be-dev/just-be.dev.git",
|
|
21
23
|
"directory": "packages/deploy"
|
|
22
24
|
},
|
|
23
25
|
"engines": {
|
|
24
26
|
"bun": ">=1.0.0"
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
|
-
"@
|
|
29
|
+
"@clack/prompts": "^0.11.0",
|
|
30
|
+
"@just-be/wildcard": "0.1.1",
|
|
31
|
+
"wrangler": "4.48.0"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
28
35
|
}
|
|
29
36
|
}
|