@just-be/deploy 0.1.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 +121 -0
- package/index.ts +263 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @just-be/deploy
|
|
2
|
+
|
|
3
|
+
Deploy static files to Cloudflare R2 and configure subdomain routing in KV for wildcard subdomain services.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh/) runtime
|
|
8
|
+
- [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI configured with Cloudflare credentials
|
|
9
|
+
- A Cloudflare Workers wildcard subdomain service with R2 and KV configured
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
No installation needed! Run directly with `bunx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bunx @just-be/deploy
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install globally:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun install -g @just-be/deploy
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Interactive Mode
|
|
28
|
+
|
|
29
|
+
Simply run without arguments to be prompted for all configuration:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bunx @just-be/deploy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The interactive mode will ask you for:
|
|
36
|
+
- Subdomain name
|
|
37
|
+
- R2 path prefix
|
|
38
|
+
- Local directory to upload
|
|
39
|
+
- Whether to enable SPA mode
|
|
40
|
+
- Whether to use a custom fallback file
|
|
41
|
+
|
|
42
|
+
### Command-Line Mode
|
|
43
|
+
|
|
44
|
+
Provide all arguments upfront:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bunx @just-be/deploy --subdomain=NAME --path=PATH --dir=DIR [--spa] [--fallback=FILE]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Arguments
|
|
51
|
+
|
|
52
|
+
- `--subdomain`: Subdomain name (e.g., "myapp" for myapp.just-be.dev)
|
|
53
|
+
- `--path`: R2 path prefix where files will be stored (e.g., "apps/myapp")
|
|
54
|
+
- `--dir`: Local directory to upload
|
|
55
|
+
- `--spa`: (Optional) Enable SPA mode - all routes serve index.html
|
|
56
|
+
- `--fallback`: (Optional) Custom fallback file for 404s (only in non-SPA mode)
|
|
57
|
+
|
|
58
|
+
### Examples
|
|
59
|
+
|
|
60
|
+
**Interactive deployment:**
|
|
61
|
+
```bash
|
|
62
|
+
bunx @just-be/deploy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Deploy a static site:**
|
|
66
|
+
```bash
|
|
67
|
+
bunx @just-be/deploy \
|
|
68
|
+
--subdomain=portfolio \
|
|
69
|
+
--path=sites/portfolio \
|
|
70
|
+
--dir=./dist
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Deploy a single-page application:**
|
|
74
|
+
```bash
|
|
75
|
+
bunx @just-be/deploy \
|
|
76
|
+
--subdomain=myapp \
|
|
77
|
+
--path=apps/myapp \
|
|
78
|
+
--dir=./build \
|
|
79
|
+
--spa
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Deploy with custom 404 page:**
|
|
83
|
+
```bash
|
|
84
|
+
bunx @just-be/deploy \
|
|
85
|
+
--subdomain=docs \
|
|
86
|
+
--path=sites/docs \
|
|
87
|
+
--dir=./out \
|
|
88
|
+
--fallback=404.html
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
1. **Parses arguments** using Node's `util.parseArgs` API
|
|
94
|
+
2. **Prompts for missing values** if any required arguments are not provided
|
|
95
|
+
3. **Validates configuration** using Zod schemas from `@just-be/wildcard-schemas`
|
|
96
|
+
4. **Scans** the target directory for all files
|
|
97
|
+
5. **Uploads** each file to R2 at `content-bucket/{path}/{relative-path}`
|
|
98
|
+
6. **Creates** a KV entry with routing configuration for the subdomain
|
|
99
|
+
7. **Configures** the wildcard service to serve your site at `https://{subdomain}.just-be.dev`
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
The script expects your wildcard service to use:
|
|
104
|
+
- **R2 Bucket**: `content-bucket`
|
|
105
|
+
- **KV Binding**: `ROUTING_RULES`
|
|
106
|
+
- **Wrangler Config**: `services/wildcard/wrangler.toml`
|
|
107
|
+
|
|
108
|
+
## Validation
|
|
109
|
+
|
|
110
|
+
Configuration is validated using Zod schemas to ensure:
|
|
111
|
+
- SPA mode and fallback file are not used together
|
|
112
|
+
- R2 path is not empty
|
|
113
|
+
- All required fields are present
|
|
114
|
+
|
|
115
|
+
## Related Packages
|
|
116
|
+
|
|
117
|
+
- [`@just-be/wildcard-schemas`](../wildcard-schemas) - Shared Zod schemas for configuration validation
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deploy static files to Cloudflare R2 and configure subdomain routing in KV
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bunx @just-be/deploy --subdomain=myapp --path=apps/myapp --dir=./dist
|
|
8
|
+
* bunx @just-be/deploy --subdomain=portfolio --path=sites/portfolio --dir=./build --fallback=404.html
|
|
9
|
+
* bunx @just-be/deploy --subdomain=spa --path=apps/spa --dir=./dist --spa
|
|
10
|
+
*
|
|
11
|
+
* Or run interactively without arguments:
|
|
12
|
+
* bunx @just-be/deploy
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { $ } from "bun";
|
|
16
|
+
import { readdir, stat } from "fs/promises";
|
|
17
|
+
import { join, relative } from "path";
|
|
18
|
+
import { createInterface } from "readline";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { StaticConfigSchema, type StaticConfig, isValidSubdomain } from "@just-be/wildcard";
|
|
21
|
+
|
|
22
|
+
const BUCKET_NAME = "content-bucket";
|
|
23
|
+
const WRANGLER_CONFIG = "services/wildcard/wrangler.toml";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Schema for CLI arguments
|
|
27
|
+
*/
|
|
28
|
+
const CliArgsSchema = z.object({
|
|
29
|
+
subdomain: z.string().optional(),
|
|
30
|
+
path: z.string().optional(),
|
|
31
|
+
dir: z.string().optional(),
|
|
32
|
+
spa: z.boolean().optional(),
|
|
33
|
+
fallback: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type CliArgs = z.infer<typeof CliArgsSchema>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse command-line arguments from Bun.argv
|
|
40
|
+
*/
|
|
41
|
+
function parseCliArgs(): CliArgs {
|
|
42
|
+
const args: Record<string, string | boolean> = {};
|
|
43
|
+
|
|
44
|
+
for (const arg of Bun.argv.slice(2)) {
|
|
45
|
+
if (arg.startsWith("--")) {
|
|
46
|
+
const [key, value] = arg.slice(2).split("=");
|
|
47
|
+
if (value === undefined) {
|
|
48
|
+
// Flag without value (e.g., --spa)
|
|
49
|
+
args[key] = true;
|
|
50
|
+
} else {
|
|
51
|
+
args[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return CliArgsSchema.parse(args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DeployConfig extends Omit<StaticConfig, "type"> {
|
|
60
|
+
subdomain: string;
|
|
61
|
+
dir: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
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
|
+
/**
|
|
91
|
+
* Parse command-line arguments
|
|
92
|
+
*/
|
|
93
|
+
async function getConfig(): Promise<DeployConfig> {
|
|
94
|
+
const values = parseCliArgs();
|
|
95
|
+
|
|
96
|
+
// Interactive mode if any required argument is missing
|
|
97
|
+
const needsInteractive = !values.subdomain || !values.path || !values.dir;
|
|
98
|
+
|
|
99
|
+
if (needsInteractive) {
|
|
100
|
+
console.log("š Interactive deployment setup\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let subdomain =
|
|
104
|
+
values.subdomain || (await prompt("Subdomain name (e.g., 'myapp' for myapp.just-be.dev)"));
|
|
105
|
+
|
|
106
|
+
// Validate subdomain format
|
|
107
|
+
if (!isValidSubdomain(subdomain)) {
|
|
108
|
+
console.error(
|
|
109
|
+
"\nā Invalid subdomain format. Must be alphanumeric with hyphens, 1-63 characters."
|
|
110
|
+
);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const path = values.path || (await prompt("R2 path prefix (e.g., 'apps/myapp')"));
|
|
115
|
+
const dir = values.dir || (await prompt("Local directory to upload", "./dist"));
|
|
116
|
+
|
|
117
|
+
let spa = values.spa;
|
|
118
|
+
let fallback = values.fallback;
|
|
119
|
+
|
|
120
|
+
// Only prompt for spa/fallback if not provided
|
|
121
|
+
if (needsInteractive && spa === undefined && fallback === undefined) {
|
|
122
|
+
spa = await confirm("Enable SPA mode? (all routes serve index.html)");
|
|
123
|
+
if (!spa) {
|
|
124
|
+
const useFallback = await confirm("Use a custom fallback file for 404s?");
|
|
125
|
+
if (useFallback) {
|
|
126
|
+
fallback = await prompt("Fallback file name", "404.html");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate with Zod schema
|
|
132
|
+
const staticConfig: StaticConfig = {
|
|
133
|
+
type: "static",
|
|
134
|
+
path,
|
|
135
|
+
...(spa && { spa }),
|
|
136
|
+
...(fallback && { fallback }),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = StaticConfigSchema.safeParse(staticConfig);
|
|
140
|
+
if (!result.success) {
|
|
141
|
+
console.error("\nā Invalid configuration:");
|
|
142
|
+
console.error(result.error.format());
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
subdomain,
|
|
148
|
+
path,
|
|
149
|
+
dir,
|
|
150
|
+
spa,
|
|
151
|
+
fallback,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Recursively find all files in a directory
|
|
157
|
+
*/
|
|
158
|
+
async function findFiles(dir: string): Promise<string[]> {
|
|
159
|
+
const results: string[] = [];
|
|
160
|
+
|
|
161
|
+
async function walk(currentDir: string): Promise<void> {
|
|
162
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const fullPath = join(currentDir, entry.name);
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
await walk(fullPath);
|
|
167
|
+
} else if (entry.isFile()) {
|
|
168
|
+
results.push(fullPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await walk(dir);
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Upload a file to R2
|
|
179
|
+
*/
|
|
180
|
+
async function uploadToR2(localPath: string, r2Key: string): Promise<void> {
|
|
181
|
+
await $`wrangler r2 object put ${BUCKET_NAME}/${r2Key} --file ${localPath}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create KV entry for subdomain routing
|
|
186
|
+
*/
|
|
187
|
+
async function createKVEntry(subdomain: string, config: DeployConfig): Promise<void> {
|
|
188
|
+
const routingConfig: StaticConfig = {
|
|
189
|
+
type: "static",
|
|
190
|
+
path: config.path,
|
|
191
|
+
...(config.spa && { spa: config.spa }),
|
|
192
|
+
...(config.fallback && { fallback: config.fallback }),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const configJson = JSON.stringify(routingConfig);
|
|
196
|
+
|
|
197
|
+
await $`wrangler kv key put --binding ROUTING_RULES ${subdomain} ${configJson} --config ${WRANGLER_CONFIG}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Main deployment function
|
|
202
|
+
*/
|
|
203
|
+
async function deploy() {
|
|
204
|
+
const config = await getConfig();
|
|
205
|
+
|
|
206
|
+
console.log("\nš¦ Deploy Configuration:");
|
|
207
|
+
console.log(` Subdomain: ${config.subdomain}.just-be.dev`);
|
|
208
|
+
console.log(` R2 Path: ${config.path}`);
|
|
209
|
+
console.log(` Local Directory: ${config.dir}`);
|
|
210
|
+
if (config.spa) {
|
|
211
|
+
console.log(` Mode: SPA (all routes serve index.html)`);
|
|
212
|
+
} else if (config.fallback) {
|
|
213
|
+
console.log(` Fallback: ${config.fallback}`);
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
// Verify directory exists
|
|
218
|
+
try {
|
|
219
|
+
const dirStat = await stat(config.dir);
|
|
220
|
+
if (!dirStat.isDirectory()) {
|
|
221
|
+
console.error(`ā Error: ${config.dir} is not a directory`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
console.error(`ā Error: Directory ${config.dir} does not exist`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Find all files to upload
|
|
230
|
+
console.log(`š Scanning files in: ${config.dir}\n`);
|
|
231
|
+
const filePaths = await findFiles(config.dir);
|
|
232
|
+
console.log(`Found ${filePaths.length} files to upload\n`);
|
|
233
|
+
|
|
234
|
+
if (filePaths.length === 0) {
|
|
235
|
+
console.error("ā No files found to upload");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Upload files to R2
|
|
240
|
+
let uploadCount = 0;
|
|
241
|
+
for (const filePath of filePaths) {
|
|
242
|
+
const relativePath = relative(config.dir, filePath);
|
|
243
|
+
const r2Key = `${config.path}/${relativePath}`;
|
|
244
|
+
|
|
245
|
+
console.log(`ā¬ļø ${relativePath} ā ${r2Key}`);
|
|
246
|
+
await uploadToR2(filePath, r2Key);
|
|
247
|
+
uploadCount++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log(`\nā
Uploaded ${uploadCount} files to R2\n`);
|
|
251
|
+
|
|
252
|
+
// Create KV entry for routing
|
|
253
|
+
console.log(`š§ Creating KV routing entry for subdomain: ${config.subdomain}`);
|
|
254
|
+
await createKVEntry(config.subdomain, config);
|
|
255
|
+
|
|
256
|
+
console.log(`\nā
Deployment complete!`);
|
|
257
|
+
console.log(`\nš Your site is available at: https://${config.subdomain}.just-be.dev`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
deploy().catch((error) => {
|
|
261
|
+
console.error("\nā Fatal error:", error);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@just-be/deploy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deploy static files to Cloudflare R2 and configure subdomain routing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "./index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"cloudflare",
|
|
12
|
+
"r2",
|
|
13
|
+
"deploy",
|
|
14
|
+
"static-site"
|
|
15
|
+
],
|
|
16
|
+
"author": "Justin Bennett",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/justbejyk/just-be.dev.git",
|
|
21
|
+
"directory": "packages/deploy"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"bun": ">=1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@just-be/wildcard": "0.1.0"
|
|
28
|
+
}
|
|
29
|
+
}
|