@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.
Files changed (3) hide show
  1. package/README.md +121 -0
  2. package/index.ts +263 -0
  3. 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
+ }