@aryanbansal-launch/edge-utils 0.1.11 → 0.1.13

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.
@@ -66,19 +66,64 @@ async function run() {
66
66
  }
67
67
 
68
68
  // 1. Redirects
69
- while (true) {
70
- const addRedirect = await question(`Do you want to add a Redirect?${config.redirects.length > 0 ? ' another?' : ''} (y/n): `);
71
- if (addRedirect.toLowerCase() !== 'y') break;
72
-
73
- const source = await question(' Source path (e.g., /source): ');
74
- const destination = await question(' Destination path (e.g., /destination): ');
75
- const code = await question(' Status code (default 308): ');
76
- config.redirects.push({
77
- source,
78
- destination,
79
- statusCode: parseInt(code) || 308
80
- });
81
- console.log(`${colors.green} ✔ Redirect added.${colors.reset}`);
69
+ const redirectMode = await question(`How would you like to add redirects?\n 1) One by one (interactive)\n 2) Bulk import from CSV file\n 3) Bulk import from JSON file\n 4) Skip\nChoose (1-4): `);
70
+
71
+ if (redirectMode === '1') {
72
+ // Interactive mode
73
+ while (true) {
74
+ const addRedirect = await question(`Do you want to add a Redirect?${config.redirects.length > 0 ? ' another?' : ''} (y/n): `);
75
+ if (addRedirect.toLowerCase() !== 'y') break;
76
+
77
+ const source = await question(' Source path (e.g., /source): ');
78
+ const destination = await question(' Destination path (e.g., /destination): ');
79
+ const code = await question(' Status code (default 308): ');
80
+ config.redirects.push({
81
+ source,
82
+ destination,
83
+ statusCode: parseInt(code) || 308
84
+ });
85
+ console.log(`${colors.green} ✔ Redirect added.${colors.reset}`);
86
+ }
87
+ } else if (redirectMode === '2') {
88
+ // CSV import
89
+ const csvPath = await question(' Enter CSV file path (e.g., ./redirects.csv): ');
90
+ try {
91
+ const csvContent = fs.readFileSync(path.resolve(csvPath), 'utf-8');
92
+ const lines = csvContent.split('\n').filter(line => line.trim());
93
+
94
+ // Skip header if present
95
+ const startIndex = lines[0].toLowerCase().includes('source') ? 1 : 0;
96
+
97
+ for (let i = startIndex; i < lines.length; i++) {
98
+ const [source, destination, statusCode] = lines[i].split(',').map(s => s.trim());
99
+ if (source && destination) {
100
+ config.redirects.push({
101
+ source,
102
+ destination,
103
+ statusCode: parseInt(statusCode) || 308
104
+ });
105
+ }
106
+ }
107
+ console.log(`${colors.green} ✔ Imported ${lines.length - startIndex} redirects from CSV.${colors.reset}`);
108
+ } catch (error) {
109
+ console.log(`${colors.red} ✖ Error reading CSV file: ${error.message}${colors.reset}`);
110
+ }
111
+ } else if (redirectMode === '3') {
112
+ // JSON import
113
+ const jsonPath = await question(' Enter JSON file path (e.g., ./redirects.json): ');
114
+ try {
115
+ const jsonContent = fs.readFileSync(path.resolve(jsonPath), 'utf-8');
116
+ const redirects = JSON.parse(jsonContent);
117
+
118
+ if (Array.isArray(redirects)) {
119
+ config.redirects.push(...redirects);
120
+ console.log(`${colors.green} ✔ Imported ${redirects.length} redirects from JSON.${colors.reset}`);
121
+ } else {
122
+ console.log(`${colors.red} ✖ JSON file must contain an array of redirect objects.${colors.reset}`);
123
+ }
124
+ } catch (error) {
125
+ console.log(`${colors.red} ✖ Error reading JSON file: ${error.message}${colors.reset}`);
126
+ }
82
127
  }
83
128
 
84
129
  // 2. Rewrites
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shortcut for: npx launch-init local
4
+ */
5
+ process.argv.splice(2, 0, 'local');
6
+ import('./launch-init.js').catch((err) => {
7
+ console.error(err);
8
+ process.exit(1);
9
+ });
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Runs `wrangler dev` using the Wrangler bundled with @aryanbansal-launch/edge-utils.
4
+ * Same as `npx wrangler dev`; extra args are passed through (e.g. --port 8788).
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ let wranglerBin;
14
+ try {
15
+ wranglerBin = require.resolve('wrangler/bin/wrangler.js');
16
+ } catch {
17
+ console.error(
18
+ 'Could not resolve wrangler. Reinstall @aryanbansal-launch/edge-utils (it bundles wrangler).'
19
+ );
20
+ process.exit(1);
21
+ }
22
+
23
+ const cwd = process.cwd();
24
+ const wranglerToml = path.join(cwd, 'wrangler.toml');
25
+ if (!fs.existsSync(wranglerToml)) {
26
+ console.error(
27
+ 'No wrangler.toml in the current directory.\n' +
28
+ ` cwd: ${cwd}\n` +
29
+ ' Run this from your project root (where wrangler.toml lives), or run the local wizard first:\n' +
30
+ ' npx launch-edge-local'
31
+ );
32
+ process.exit(1);
33
+ }
34
+
35
+ const extraArgs = process.argv.slice(2);
36
+ const child = spawn(process.execPath, [wranglerBin, 'dev', ...extraArgs], {
37
+ stdio: 'inherit',
38
+ cwd,
39
+ env: process.env,
40
+ });
41
+
42
+ child.on('error', (err) => {
43
+ console.error(err);
44
+ process.exit(1);
45
+ });
46
+
47
+ child.on('exit', (code, signal) => {
48
+ if (signal) {
49
+ process.kill(process.pid, signal);
50
+ return;
51
+ }
52
+ process.exit(code ?? 0);
53
+ });
@@ -66,6 +66,8 @@ ${colors.bright}${colors.green}1. Security & Access Control${colors.reset}
66
66
  password: string,
67
67
  realm?: string
68
68
  }
69
+ ${colors.dim}Hostname match:${colors.reset} URL host, Host header, or X-Forwarded-Host
70
+ (use with local Wrangler + rewriteRequestToOrigin)
69
71
  ${colors.dim}Returns:${colors.reset} Promise<Response> | null
70
72
  ${colors.dim}Example:${colors.reset}
71
73
  const auth = await protectWithBasicAuth(request, {
@@ -148,6 +150,14 @@ ${colors.bright}${colors.green}5. Response Utilities${colors.reset}
148
150
  ${colors.dim}Example:${colors.reset}
149
151
  return passThrough(request);
150
152
 
153
+ ${colors.cyan}rewriteRequestToOrigin(request, backendOrigin)${colors.reset}
154
+ Rewrite request URL to your local app (used by functions/dev-worker.edge.js)
155
+ ${colors.dim}Parameters:${colors.reset}
156
+ - request: Request object
157
+ - backendOrigin: string (e.g. http://127.0.0.1:3000 from BACKEND_URL)
158
+ ${colors.dim}Returns:${colors.reset} Request
159
+ ${colors.dim}Note:${colors.reset} Sets X-Forwarded-Host when host changes (Basic Auth + localhost)
160
+
151
161
  ${colors.bright}${colors.green}6. Configuration${colors.reset}
152
162
 
153
163
  ${colors.cyan}generateLaunchConfig(options)${colors.reset}
@@ -175,6 +185,35 @@ ${colors.dim}──────────────────────
175
185
  ${colors.cyan}npx launch-config${colors.reset}
176
186
  Interactive CLI to manage launch.json
177
187
  Configure: redirects, rewrites, cache priming
188
+ Supports bulk import from CSV/JSON files
189
+
190
+ ${colors.bright}${colors.yellow}🧪 LOCAL TESTING (Wrangler / Miniflare)${colors.reset}
191
+ ${colors.dim}────────────────────────────────────────────────────────────────────${colors.reset}
192
+
193
+ ${colors.cyan}npx create-launch-edge local${colors.reset}
194
+ Interactive wizard: pick a preset (redirect, JSON, basic auth, bots, Next.js RSC)
195
+ Writes or updates functions/[proxy].edge.js; creates dev-worker.edge.js + wrangler.toml if missing
196
+ ${colors.dim}Alias:${colors.reset} ${colors.cyan}npx launch-edge-local${colors.reset} (same as above)
197
+
198
+ ${colors.cyan}npx launch-edge-test-local${colors.reset}
199
+ Starts ${colors.dim}wrangler dev${colors.reset} using the Wrangler bundled with this package
200
+ Run from ${colors.bright}project root${colors.reset} (directory that contains wrangler.toml)
201
+ ${colors.dim}Extra args pass through:${colors.reset} --port 8788
202
+ ${colors.dim}Example:${colors.reset} npx launch-edge-test-local --var BACKEND_URL=http://127.0.0.1:5173
203
+
204
+ ${colors.bright}Typical flow${colors.reset}
205
+ 1. ${colors.cyan}npx create-launch-edge local${colors.reset} → choose preset, confirm overwrite if asked
206
+ 2. Start your app on the port in ${colors.dim}BACKEND_URL${colors.reset} (see wrangler.toml; default 3000)
207
+ 3. ${colors.cyan}npx launch-edge-test-local${colors.reset}
208
+ 4. Open the URL Wrangler prints (often ${colors.dim}http://localhost:8787${colors.reset}) + path from the wizard
209
+
210
+ ${colors.bright}Quick checks${colors.reset}
211
+ ${colors.dim}•${colors.reset} JSON preset: open /api/edge-ping
212
+ ${colors.dim}•${colors.reset} Redirect preset: open the legacy path; expect 301
213
+ ${colors.dim}•${colors.reset} Basic auth preset: browser prompt (e.g. demo / demo); use hostnameIncludes localhost
214
+ ${colors.dim}•${colors.reset} Bots: ${colors.dim}curl -A "GPTBot" http://localhost:8787/${colors.reset} → 403
215
+
216
+ ${colors.bright}Before publishing${colors.reset} ${colors.dim}npm run build${colors.reset} in this package so dist/ matches src/
178
217
 
179
218
  ${colors.cyan}npx launch-help${colors.reset}
180
219
  Display this help guide
@@ -2,18 +2,28 @@
2
2
 
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
+ import readline from 'node:readline/promises';
6
+ import { stdin as input, stdout as output } from 'node:process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const invokedAsLocal = path.basename(__filename) === 'launch-edge-local.js';
5
11
 
6
12
  const functionsDir = path.join(process.cwd(), 'functions');
7
13
  const edgeFile = path.join(functionsDir, '[proxy].edge.js');
14
+ const devWorkerFile = path.join(functionsDir, 'dev-worker.edge.js');
15
+ const wranglerTomlPath = path.join(process.cwd(), 'wrangler.toml');
8
16
 
9
- // Simple ANSI colors for better terminal output
10
17
  const colors = {
11
18
  reset: '\x1b[0m',
12
19
  bright: '\x1b[1m',
20
+ dim: '\x1b[2m',
13
21
  green: '\x1b[32m',
14
22
  yellow: '\x1b[33m',
15
23
  cyan: '\x1b[36m',
16
- blue: '\x1b[34m'
24
+ blue: '\x1b[34m',
25
+ red: '\x1b[31m',
26
+ magenta: '\x1b[35m'
17
27
  };
18
28
 
19
29
  const template = `
@@ -84,12 +94,215 @@ export default async function handler(request, context) {
84
94
  }
85
95
  `.trim();
86
96
 
97
+ const devWorkerTemplate = `
98
+ import { rewriteRequestToOrigin } from "@aryanbansal-launch/edge-utils";
99
+ import handler from "./[proxy].edge.js";
100
+
101
+ /**
102
+ * Wrangler / Miniflare entry: rewrites the request URL to your local app (BACKEND_URL)
103
+ * before running your Launch edge handler. Generated by launch-init.
104
+ */
105
+ export default {
106
+ async fetch(request, env, ctx) {
107
+ const req = rewriteRequestToOrigin(request, env.BACKEND_URL);
108
+ return handler(req, {});
109
+ }
110
+ };
111
+ `.trim();
112
+
113
+ const wranglerTemplate = `name = "launch-edge-local-dev"
114
+ main = "functions/dev-worker.edge.js"
115
+ compatibility_date = "2024-01-01"
116
+
117
+ [vars]
118
+ BACKEND_URL = "http://127.0.0.1:3000"
119
+ `;
120
+
121
+ /** Presets for: npx launch-init local */
122
+ const LOCAL_PRESETS = [
123
+ {
124
+ id: 'redirect',
125
+ title: 'Redirect — path match → new path (301)',
126
+ testPath: '/legacy-demo',
127
+ hint: 'Open /legacy-demo — you should be redirected to /redirect-target',
128
+ template: `
129
+ import { passThrough, redirectIfMatch } from "@aryanbansal-launch/edge-utils";
130
+
131
+ /** Local test: redirect preset (generated by launch-init local) */
132
+ export default async function handler(request, context) {
133
+ const redirectResponse = redirectIfMatch(request, {
134
+ path: "/legacy-demo",
135
+ to: "/redirect-target",
136
+ status: 301
137
+ });
138
+ if (redirectResponse) return redirectResponse;
139
+
140
+ return passThrough(request);
141
+ }
142
+ `.trim()
143
+ },
144
+ {
145
+ id: 'json',
146
+ title: 'JSON — edge-only API route',
147
+ testPath: '/api/edge-ping',
148
+ hint: 'Open /api/edge-ping — JSON is served from the Worker (no backend needed for this path)',
149
+ template: `
150
+ import { jsonResponse, passThrough } from "@aryanbansal-launch/edge-utils";
151
+
152
+ /** Local test: JSON preset (generated by launch-init local) */
153
+ export default async function handler(request, context) {
154
+ const url = new URL(request.url);
155
+ if (url.pathname === "/api/edge-ping") {
156
+ return jsonResponse({ ok: true, source: "edge-utils", path: url.pathname });
157
+ }
158
+ return passThrough(request);
159
+ }
160
+ `.trim()
161
+ },
162
+ {
163
+ id: 'basic-auth',
164
+ title: 'Basic auth — password gate (uses localhost host)',
165
+ testPath: '/',
166
+ hint: 'Browser will prompt for user/pass (demo / demo). Use hostnameIncludes "localhost" in dev.',
167
+ template: `
168
+ import { passThrough, protectWithBasicAuth } from "@aryanbansal-launch/edge-utils";
169
+
170
+ /** Local test: basic auth preset (generated by launch-init local) */
171
+ export default async function handler(request, context) {
172
+ const authResponse = await protectWithBasicAuth(request, {
173
+ hostnameIncludes: "localhost",
174
+ username: "demo",
175
+ password: "demo",
176
+ realm: "Local edge test"
177
+ });
178
+ if (authResponse && authResponse.status === 401) return authResponse;
179
+
180
+ return passThrough(request);
181
+ }
182
+ `.trim()
183
+ },
184
+ {
185
+ id: 'bots',
186
+ title: 'Block AI crawlers — block common bot user-agents',
187
+ testPath: '/',
188
+ hint: 'Normal browser works. Test with curl -A "GPTBot" http://localhost:8787/ — should be blocked',
189
+ template: `
190
+ import { passThrough, blockAICrawlers } from "@aryanbansal-launch/edge-utils";
191
+
192
+ /** Local test: bot block preset (generated by launch-init local) */
193
+ export default async function handler(request, context) {
194
+ const botResponse = blockAICrawlers(request);
195
+ if (botResponse) return botResponse;
196
+
197
+ return passThrough(request);
198
+ }
199
+ `.trim()
200
+ },
201
+ {
202
+ id: 'rsc',
203
+ title: 'Next.js RSC — strip RSC header on selected paths (advanced)',
204
+ testPath: '/your-rsc-page',
205
+ hint: 'Adjust affectedPaths to match a route in your app; used when RSC headers break local proxy',
206
+ template: `
207
+ import { passThrough, handleNextJS_RSC } from "@aryanbansal-launch/edge-utils";
208
+
209
+ /** Local test: Next.js RSC preset (generated by launch-init local) */
210
+ export default async function handler(request, context) {
211
+ const rscResponse = await handleNextJS_RSC(request, {
212
+ affectedPaths: ["/your-rsc-page"]
213
+ });
214
+ if (rscResponse) return rscResponse;
215
+
216
+ return passThrough(request);
217
+ }
218
+ `.trim()
219
+ }
220
+ ];
221
+
222
+ function printLocalInstructions(preset) {
223
+ console.log(`\n${colors.bright}${colors.green}Local test — next steps${colors.reset}\n`);
224
+ console.log(` ${colors.cyan}1.${colors.reset} Start your app on the same origin as ${colors.cyan}BACKEND_URL${colors.reset} in wrangler.toml (default ${colors.yellow}http://127.0.0.1:3000${colors.reset}).`);
225
+ console.log(` ${colors.cyan}2.${colors.reset} From project root: ${colors.bright}npx launch-edge-test-local${colors.reset}`);
226
+ console.log(` ${colors.dim}(same as ${colors.reset}${colors.bright}npx wrangler dev${colors.reset}${colors.dim}; extra args pass through, e.g. --port 8788)${colors.reset}`);
227
+ console.log(` ${colors.cyan}3.${colors.reset} Open ${colors.bright}http://localhost:8787${preset.testPath}${colors.reset} ${colors.dim}(Wrangler prints the port if different)${colors.reset}`);
228
+ console.log(`\n ${colors.magenta}Try:${colors.reset} ${preset.hint}\n`);
229
+ }
230
+
231
+ async function localWizard() {
232
+ console.log(`\n${colors.bright}${colors.cyan}Edge utilities — test locally (Wrangler / Miniflare)${colors.reset}\n`);
233
+
234
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
235
+ if (!fs.existsSync(packageJsonPath)) {
236
+ console.log(`${colors.red}❌ No package.json here.${colors.reset} Run from your project root.\n`);
237
+ process.exit(1);
238
+ }
239
+
240
+ const rl = readline.createInterface({ input, output });
241
+
242
+ try {
243
+ console.log(`${colors.bright}Pick what you want to try:${colors.reset}\n`);
244
+ LOCAL_PRESETS.forEach((p, i) => {
245
+ console.log(` ${colors.cyan}${i + 1}.${colors.reset} ${p.title}`);
246
+ });
247
+ console.log('');
248
+
249
+ const raw = await rl.question(`${colors.bright}Enter 1–${LOCAL_PRESETS.length} [1]: ${colors.reset}`);
250
+ const n = parseInt(raw || '1', 10);
251
+ if (Number.isNaN(n) || n < 1 || n > LOCAL_PRESETS.length) {
252
+ console.log(`${colors.red}Invalid choice.${colors.reset}\n`);
253
+ process.exit(1);
254
+ }
255
+ const preset = LOCAL_PRESETS[n - 1];
256
+
257
+ let overwrite = true;
258
+ if (fs.existsSync(edgeFile)) {
259
+ const ans = await rl.question(
260
+ `${colors.yellow}Overwrite functions/[proxy].edge.js?${colors.reset} [y/N]: `
261
+ );
262
+ overwrite = /^y(es)?$/i.test(ans.trim());
263
+ if (!overwrite) {
264
+ console.log(`\n${colors.blue}Skipped writing [proxy].edge.js.${colors.reset} Still ensuring dev-worker + wrangler…\n`);
265
+ }
266
+ }
267
+
268
+ if (!fs.existsSync(functionsDir)) {
269
+ fs.mkdirSync(functionsDir, { recursive: true });
270
+ console.log(`${colors.green}✨${colors.reset} Created /functions`);
271
+ }
272
+
273
+ if (overwrite) {
274
+ fs.writeFileSync(edgeFile, preset.template + '\n');
275
+ console.log(`${colors.green}✨${colors.reset} Wrote functions/[proxy].edge.js (${preset.id})`);
276
+ }
277
+
278
+ if (!fs.existsSync(devWorkerFile)) {
279
+ fs.writeFileSync(devWorkerFile, devWorkerTemplate + '\n');
280
+ console.log(`${colors.green}✨${colors.reset} Created /functions/dev-worker.edge.js`);
281
+ } else {
282
+ console.log(`${colors.blue}ℹ${colors.reset} /functions/dev-worker.edge.js already exists`);
283
+ }
284
+
285
+ if (!fs.existsSync(wranglerTomlPath)) {
286
+ fs.writeFileSync(wranglerTomlPath, wranglerTemplate);
287
+ console.log(`${colors.green}✨${colors.reset} Created wrangler.toml`);
288
+ } else {
289
+ console.log(`${colors.blue}ℹ${colors.reset} wrangler.toml already exists (left unchanged)`);
290
+ }
291
+
292
+ printLocalInstructions(preset);
293
+ } catch (err) {
294
+ console.error(`${colors.red}Error:${colors.reset}`, err.message);
295
+ process.exit(1);
296
+ } finally {
297
+ rl.close();
298
+ }
299
+ }
300
+
87
301
  async function init() {
88
302
  console.log(`\n${colors.bright}${colors.cyan}🚀 create-launch-edge: Contentstack Launch Initializer${colors.reset}\n`);
89
303
 
90
304
  let actionsTaken = 0;
91
305
 
92
- // 1. Root level check: Look for package.json
93
306
  const packageJsonPath = path.join(process.cwd(), 'package.json');
94
307
  if (!fs.existsSync(packageJsonPath)) {
95
308
  console.log(`${colors.red}❌ Error: Root directory not detected.${colors.reset}`);
@@ -100,7 +313,6 @@ async function init() {
100
313
  }
101
314
 
102
315
  try {
103
- // 2. Folder existence check
104
316
  if (!fs.existsSync(functionsDir)) {
105
317
  fs.mkdirSync(functionsDir, { recursive: true });
106
318
  console.log(`${colors.green}✨ New:${colors.reset} Created /functions directory`);
@@ -109,7 +321,6 @@ async function init() {
109
321
  console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions directory already found`);
110
322
  }
111
323
 
112
- // 3. File existence check (don't overwrite user's work)
113
324
  if (!fs.existsSync(edgeFile)) {
114
325
  fs.writeFileSync(edgeFile, template + '\n');
115
326
  console.log(`${colors.green}✨ New:${colors.reset} Created /functions/[proxy].edge.js`);
@@ -118,7 +329,22 @@ async function init() {
118
329
  console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions/[proxy].edge.js already found`);
119
330
  }
120
331
 
121
- // Final Summary Messages based on the state
332
+ if (!fs.existsSync(devWorkerFile)) {
333
+ fs.writeFileSync(devWorkerFile, devWorkerTemplate + '\n');
334
+ console.log(`${colors.green}✨ New:${colors.reset} Created /functions/dev-worker.edge.js`);
335
+ actionsTaken++;
336
+ } else {
337
+ console.log(`${colors.blue}ℹ️ Existing:${colors.reset} /functions/dev-worker.edge.js already found`);
338
+ }
339
+
340
+ if (!fs.existsSync(wranglerTomlPath)) {
341
+ fs.writeFileSync(wranglerTomlPath, wranglerTemplate);
342
+ console.log(`${colors.green}✨ New:${colors.reset} Created wrangler.toml (local edge dev)`);
343
+ actionsTaken++;
344
+ } else {
345
+ console.log(`${colors.blue}ℹ️ Existing:${colors.reset} wrangler.toml already found — skipped creating default`);
346
+ }
347
+
122
348
  if (actionsTaken === 0) {
123
349
  console.log(`\n${colors.bright}${colors.blue}🏁 Everything is already set up!${colors.reset}`);
124
350
  console.log(`No changes were made to your existing files.`);
@@ -130,8 +356,10 @@ async function init() {
130
356
  console.log(`\n${colors.bright}Next Steps:${colors.reset}`);
131
357
  console.log(`1. Open ${colors.cyan}functions/[proxy].edge.js${colors.reset}`);
132
358
  console.log(`2. Customize your redirects, auth, and RSC paths`);
133
- console.log(`3. Deploy your project to Contentstack Launch\n`);
134
-
359
+ console.log(`3. Deploy your project to Contentstack Launch`);
360
+ console.log(`4. Test edge locally: ${colors.bright}npx launch-edge-local${colors.reset} (wizard), then ${colors.bright}npx launch-edge-test-local${colors.reset}`);
361
+ console.log(` (${colors.dim}Wrangler is bundled; run app on BACKEND_URL for pass-through routes)${colors.reset}\n`);
362
+
135
363
  console.log(`${colors.blue}Documentation:${colors.reset} https://github.com/AryanBansal-launch/launch-edge-utils#readme\n`);
136
364
  } catch (error) {
137
365
  console.error(`\n${colors.red}❌ Error during setup:${colors.reset}`, error.message);
@@ -139,4 +367,18 @@ async function init() {
139
367
  }
140
368
  }
141
369
 
142
- init();
370
+ const args = process.argv.slice(2);
371
+ const runLocal =
372
+ invokedAsLocal ||
373
+ args[0] === 'local' ||
374
+ args.includes('--local');
375
+
376
+ async function main() {
377
+ if (runLocal) {
378
+ await localWizard();
379
+ } else {
380
+ await init();
381
+ }
382
+ }
383
+
384
+ main();
@@ -1,6 +1,13 @@
1
1
  export function protectWithBasicAuth(request, options) {
2
2
  const url = new URL(request.url);
3
- if (!url.hostname.includes(options.hostnameIncludes)) {
3
+ // Match URL host, Host header, or X-Forwarded-Host (set by rewriteRequestToOrigin when
4
+ // the browser hits localhost:8787 but the request URL is rewritten to 127.0.0.1:3000).
5
+ const hostHeader = request.headers.get("host")?.split(":")[0] ?? "";
6
+ const forwardedHost = request.headers.get("x-forwarded-host")?.split(":")[0] ?? "";
7
+ const hostMatches = url.hostname.includes(options.hostnameIncludes) ||
8
+ hostHeader.includes(options.hostnameIncludes) ||
9
+ forwardedHost.includes(options.hostnameIncludes);
10
+ if (!hostMatches) {
4
11
  return null;
5
12
  }
6
13
  const authHeader = request.headers.get("Authorization");
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Rewrites the request URL so its origin (and optional path prefix from `backendOrigin`)
3
+ * points at your local app. Use with Wrangler / Miniflare so `passThrough(request)` and
4
+ * other `fetch(request)` calls reach the dev server instead of the Worker dev port.
5
+ *
6
+ * When the incoming host differs from the backend host, sets `X-Forwarded-Host` to the
7
+ * original `incoming.host` (unless already present) so helpers like `protectWithBasicAuth`
8
+ * can still match `localhost` while the URL points at `127.0.0.1`.
9
+ */
10
+ export function rewriteRequestToOrigin(request, backendOrigin) {
11
+ const base = new URL(backendOrigin);
12
+ const incoming = new URL(request.url);
13
+ const prefix = base.pathname === "/" ? "" : base.pathname.replace(/\/$/, "");
14
+ const path = `${prefix}${incoming.pathname}${incoming.search}${incoming.hash}`;
15
+ const target = new URL(path, base.origin);
16
+ const headers = new Headers(request.headers);
17
+ if (incoming.hostname !== target.hostname && !headers.has("x-forwarded-host")) {
18
+ headers.set("x-forwarded-host", incoming.host);
19
+ }
20
+ return new Request(target.toString(), {
21
+ method: request.method,
22
+ headers,
23
+ body: request.body,
24
+ redirect: request.redirect,
25
+ ...(request.body != null ? { duplex: "half" } : {}),
26
+ });
27
+ }
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./dev/rewrite-origin.js";
1
2
  export * from "./response/json.js";
2
3
  export * from "./response/passthrough.js";
3
4
  export * from "./redirect/redirect.js";
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "local-dev-example",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "../..": {
7
+ "name": "@aryanbansal-launch/edge-utils",
8
+ "version": "0.1.5",
9
+ "license": "MIT",
10
+ "dependencies": {
11
+ "wrangler": "^3.114.0"
12
+ },
13
+ "bin": {
14
+ "launch-edge-local": "bin/launch-edge-local.js",
15
+ "launch-init": "bin/launch-init.js"
16
+ }
17
+ },
18
+ "node_modules/@aryanbansal-launch/edge-utils": {
19
+ "resolved": "../..",
20
+ "link": true
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "local-dev-example",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "local-dev-example",
8
+ "dependencies": {
9
+ "@aryanbansal-launch/edge-utils": "file:../.."
10
+ }
11
+ },
12
+ "../..": {
13
+ "name": "@aryanbansal-launch/edge-utils",
14
+ "version": "0.1.5",
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "wrangler": "^3.114.0"
18
+ },
19
+ "bin": {
20
+ "launch-edge-local": "bin/launch-edge-local.js",
21
+ "launch-init": "bin/launch-init.js"
22
+ }
23
+ },
24
+ "node_modules/@aryanbansal-launch/edge-utils": {
25
+ "resolved": "../..",
26
+ "link": true
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "local-dev-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "launch-edge-test-local"
7
+ },
8
+ "dependencies": {
9
+ "@aryanbansal-launch/edge-utils": "file:../.."
10
+ }
11
+ }
@@ -0,0 +1,18 @@
1
+ import { jsonResponse, passThrough } from "@aryanbansal-launch/edge-utils";
2
+
3
+ /**
4
+ * Minimal Launch-style handler for local Miniflare testing.
5
+ */
6
+ export default async function handler(request: Request, _context: unknown) {
7
+ const url = new URL(request.url);
8
+
9
+ if (url.pathname === "/api/edge-ping") {
10
+ return jsonResponse({
11
+ ok: true,
12
+ source: "edge-utils-example",
13
+ path: url.pathname,
14
+ });
15
+ }
16
+
17
+ return passThrough(request);
18
+ }
@@ -0,0 +1,11 @@
1
+ import { rewriteRequestToOrigin } from "@aryanbansal-launch/edge-utils";
2
+ import handler from "./sample-handler.js";
3
+
4
+ type Env = { BACKEND_URL: string };
5
+
6
+ export default {
7
+ async fetch(request: Request, env: Env, _ctx: unknown) {
8
+ const req = rewriteRequestToOrigin(request, env.BACKEND_URL);
9
+ return handler(req, {});
10
+ },
11
+ };
@@ -0,0 +1,6 @@
1
+ name = "edge-utils-local-dev-example"
2
+ main = "worker.ts"
3
+ compatibility_date = "2024-01-01"
4
+
5
+ [vars]
6
+ BACKEND_URL = "http://127.0.0.1:3000"
@@ -0,0 +1,12 @@
1
+ source,destination,statusCode
2
+ /old-blog/post-1,/blog/post-1,301
3
+ /old-blog/post-2,/blog/post-2,301
4
+ /old-blog/post-3,/blog/post-3,301
5
+ /products/old-sku-123,/products/new-sku-456,308
6
+ /products/old-sku-789,/products/new-sku-101,308
7
+ /legacy/about,/about,301
8
+ /legacy/contact,/contact,301
9
+ /legacy/pricing,/pricing,301
10
+ /old-shop/*,/shop/*,301
11
+ /archive/*,/blog/archive/*,301
12
+
@@ -0,0 +1,53 @@
1
+ [
2
+ {
3
+ "source": "/old-blog/post-1",
4
+ "destination": "/blog/post-1",
5
+ "statusCode": 301
6
+ },
7
+ {
8
+ "source": "/old-blog/post-2",
9
+ "destination": "/blog/post-2",
10
+ "statusCode": 301
11
+ },
12
+ {
13
+ "source": "/old-blog/post-3",
14
+ "destination": "/blog/post-3",
15
+ "statusCode": 301
16
+ },
17
+ {
18
+ "source": "/products/old-sku-123",
19
+ "destination": "/products/new-sku-456",
20
+ "statusCode": 308
21
+ },
22
+ {
23
+ "source": "/products/old-sku-789",
24
+ "destination": "/products/new-sku-101",
25
+ "statusCode": 308
26
+ },
27
+ {
28
+ "source": "/legacy/about",
29
+ "destination": "/about",
30
+ "statusCode": 301
31
+ },
32
+ {
33
+ "source": "/legacy/contact",
34
+ "destination": "/contact",
35
+ "statusCode": 301
36
+ },
37
+ {
38
+ "source": "/legacy/pricing",
39
+ "destination": "/pricing",
40
+ "statusCode": 301
41
+ },
42
+ {
43
+ "source": "/old-shop/*",
44
+ "destination": "/shop/*",
45
+ "statusCode": 301
46
+ },
47
+ {
48
+ "source": "/archive/*",
49
+ "destination": "/blog/archive/*",
50
+ "statusCode": 301
51
+ }
52
+ ]
53
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryanbansal-launch/edge-utils",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -15,14 +15,20 @@
15
15
  "bin": {
16
16
  "create-launch-edge": "bin/launch-init.js",
17
17
  "launch-config": "bin/launch-config.js",
18
- "launch-help": "bin/launch-help.js"
18
+ "launch-help": "bin/launch-help.js",
19
+ "launch-edge-local": "./bin/launch-edge-local.js",
20
+ "launch-edge-test-local": "./bin/launch-edge-test-local.js"
19
21
  },
20
22
  "exports": {
21
23
  ".": "./dist/index.js"
22
24
  },
25
+ "dependencies": {
26
+ "wrangler": "^3.114.0"
27
+ },
23
28
  "files": [
24
29
  "dist",
25
- "bin"
30
+ "bin",
31
+ "examples"
26
32
  ],
27
33
  "scripts": {
28
34
  "build": "tsc",
package/readme.md CHANGED
@@ -10,12 +10,26 @@ A comprehensive toolkit for [Contentstack Launch](https://www.contentstack.com/d
10
10
  ## 📋 Table of Contents
11
11
 
12
12
  - [Quick Start](#-quick-start)
13
+ - [User journey](#-user-journey)
13
14
  - [Usage Flow](#-usage-flow)
14
15
  - [Complete API Reference](#-complete-api-reference)
15
16
  - [Real-World Examples](#-real-world-examples)
16
17
  - [CLI Commands](#-cli-commands)
17
18
  - [Platform Support](#-platform-support)
18
19
 
20
+ A lightweight, high-performance toolkit specifically designed for **Contentstack Launch Edge Functions**. Speed up your development with production-ready utilities for security, authentication, routing, and Next.js compatibility—all optimized to run at the edge.
21
+
22
+ ---
23
+
24
+ ## ✨ Features
25
+
26
+ - 🛡️ **Security First**: Block AI crawlers and manage IP access with ease.
27
+ - 🔐 **Edge Auth**: Implement Basic Auth directly at the edge for specific hostnames.
28
+ - 📍 **Geo-Aware**: Easily extract location data from request headers.
29
+ - ⚛️ **Next.js Ready**: Built-in fixes for RSC header issues on Launch proxies.
30
+ - 🔀 **Smart Routing**: Declarative redirects based on path and method.
31
+ - ⚡ **Lean edge code**: No runtime deps inside the utilities you import at the edge; the npm package bundles **Wrangler** so local testing works without a separate install.
32
+
19
33
  ---
20
34
 
21
35
  ## ⚡ Quick Start
@@ -51,6 +65,98 @@ npx launch-help
51
65
 
52
66
  ---
53
67
 
68
+ ## 🧭 User journey
69
+
70
+ Step-by-step paths for the three most common goals. Adjust paths and ports to match your app.
71
+
72
+ ### Scenario 1: Use a particular edge utility in production
73
+
74
+ You want **one or more** helpers from this library (for example Basic Auth, bot blocking, or `redirectIfMatch`) in your **Contentstack Launch** edge handler.
75
+
76
+ 1. **Install the package** (from your project root, next to `package.json`):
77
+ ```bash
78
+ npm install @aryanbansal-launch/edge-utils
79
+ ```
80
+ 2. **Scaffold the edge function file** (skip if `functions/[proxy].edge.js` already exists):
81
+ ```bash
82
+ npx create-launch-edge
83
+ ```
84
+ This creates `functions/` and a starter `functions/[proxy].edge.js` if missing.
85
+ 3. **Open** `functions/[proxy].edge.js` in your editor.
86
+ 4. **Import** only what you need from the package, for example:
87
+ ```javascript
88
+ import { blockAICrawlers, redirectIfMatch, passThrough } from "@aryanbansal-launch/edge-utils";
89
+ ```
90
+ 5. **Wire your handler**: call each utility in order; when a function returns a `Response`, return it immediately; otherwise continue until `passThrough(request)` (or your own `fetch`) sends traffic to the origin.
91
+ 6. **Review the API** (optional):
92
+ ```bash
93
+ npx launch-help
94
+ ```
95
+ 7. **Deploy** your site through **Contentstack Launch** using your normal workflow (CLI or UI). Launch runs `functions/[proxy].edge.js` at the edge before traffic hits your app.
96
+
97
+ ---
98
+
99
+ ### Scenario 2: Test edge utilities locally (Wrangler / Miniflare)
100
+
101
+ You want to **try** a preset (redirect, JSON route, basic auth, bots, or Next.js RSC) **on your machine** before deploying.
102
+
103
+ 1. **Install the package**:
104
+ ```bash
105
+ npm install @aryanbansal-launch/edge-utils
106
+ ```
107
+ 2. **One-time scaffold** (creates `functions/dev-worker.edge.js` and `wrangler.toml` if they are missing; safe to run again):
108
+ ```bash
109
+ npx create-launch-edge
110
+ ```
111
+ 3. **Start the local wizard** (pick a preset interactively):
112
+ ```bash
113
+ npx launch-edge-local
114
+ ```
115
+ Or:
116
+ ```bash
117
+ npx create-launch-edge local
118
+ ```
119
+ 4. **Enter a number** `1`–`5` for the preset. If `[proxy].edge.js` already exists, confirm overwrite when prompted (`y`).
120
+ 5. **Align the backend URL** with your app: open `wrangler.toml` and set `[vars] BACKEND_URL` to your dev server (default `http://127.0.0.1:3000`). Change the port if your app uses something else (for example `5173` for Vite).
121
+ 6. **Start your app** in another terminal (for example `npm run dev`) so it listens on that host/port.
122
+ 7. **Start the local Worker** from the **same project root** as `wrangler.toml`:
123
+ ```bash
124
+ npx launch-edge-test-local
125
+ ```
126
+ This runs the bundled `wrangler dev`. Extra args are supported, e.g. `npx launch-edge-test-local --port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`.
127
+ 8. **Open the URL** Wrangler prints (often `http://localhost:8787`) and the path the wizard suggests (for example `/api/edge-ping` for the JSON preset).
128
+ 9. **Optional checks**: see `npx launch-help` under **Local testing** for curl / bot tests.
129
+
130
+ **Note:** After you change **source** in `@aryanbansal-launch/edge-utils`, run `npm run build` in the package before linking or publishing so `dist/` matches `src/`.
131
+
132
+ ---
133
+
134
+ ### Scenario 3: Redirects, rewrites, and cache priming (`launch.json`)
135
+
136
+ You want **config-driven** redirects, rewrites, or cache priming **without** coding them in the edge file—Launch reads **`launch.json`** at the project root.
137
+
138
+ 1. **Install the package** (includes the `launch-config` CLI):
139
+ ```bash
140
+ npm install @aryanbansal-launch/edge-utils
141
+ ```
142
+ 2. **Run the interactive configurator** from your **project root**:
143
+ ```bash
144
+ npx launch-config
145
+ ```
146
+ 3. **Follow the prompts** to add:
147
+ - **Redirects** (one-by-one or bulk),
148
+ - **Rewrites** (source path → destination),
149
+ - **Cache priming URLs** (relative paths only, as required by Launch).
150
+ 4. **Bulk import** (optional): choose CSV or JSON when the CLI asks, and provide a file path to import many redirects at once.
151
+ 5. **Confirm** `launch.json` is created or updated at the **root** of your Launch project (alongside `package.json`).
152
+ 6. **Deploy** through Contentstack Launch so the new configuration is applied.
153
+
154
+ **Alternative (code):** you can build `launch.json` in code with `generateLaunchConfig` from this package and write the file yourself—see the **Configuration** subsection in the [Complete API Reference](#-complete-api-reference) below.
155
+
156
+ **Using both:** keep bulk static rules in `launch.json` and use `functions/[proxy].edge.js` for dynamic logic (geo, cookies, A/B tests). They can coexist on the same project.
157
+
158
+ ---
159
+
54
160
  ## 🔄 Usage Flow
55
161
 
56
162
  ### Understanding Edge Functions
@@ -291,7 +397,7 @@ Add Basic Authentication to protect environments.
291
397
  **Parameters:**
292
398
  - `request` (Request) - The incoming request object
293
399
  - `options` (object)
294
- - `hostnameIncludes` (string) - Protect URLs containing this hostname
400
+ - `hostnameIncludes` (string) - Substring match against the request URL hostname, the `Host` header, or `X-Forwarded-Host` (without port). `rewriteRequestToOrigin` sets `X-Forwarded-Host` when the URL is rewritten so `hostnameIncludes: "localhost"` works with `npx launch-edge-test-local` while `BACKEND_URL` uses `127.0.0.1`.
295
401
  - `username` (string) - Username for authentication
296
402
  - `password` (string) - Password for authentication
297
403
  - `realm` (string, optional) - Auth realm name (default: "Protected Area")
@@ -299,7 +405,7 @@ Add Basic Authentication to protect environments.
299
405
  **Returns:** `Promise<Response> | null`
300
406
  - Returns `401 Unauthorized` if auth fails
301
407
  - Returns authenticated response if credentials valid
302
- - Returns `null` if hostname doesn't match
408
+ - Returns `null` if neither the URL hostname nor the `Host` header matches `hostnameIncludes`
303
409
 
304
410
  **Example:**
305
411
  ```javascript
@@ -387,14 +493,164 @@ if (redirect) return redirect;
387
493
  - A/B testing redirects
388
494
  - Maintenance mode redirects
389
495
 
390
- **When to Use:**
391
- - **Edge Functions (this utility)**: Dynamic redirects requiring logic (cookies, headers, geo)
392
- - **launch.json**: Static path-to-path redirects (better performance)
393
-
394
496
  **Learn More:** [Edge URL Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
395
497
 
396
498
  ---
397
499
 
500
+ ### 📊 Redirects: Config vs Edge Functions
501
+
502
+ Contentstack Launch offers **two ways** to handle redirects. Choose the right approach based on your needs:
503
+
504
+ #### Option 1: Config-Based Redirects (`launch.json`)
505
+
506
+ **Best for:** Static, predictable redirects that don't require logic
507
+
508
+ **Pros:**
509
+ - ⚡ **Faster**: No edge function execution overhead
510
+ - 🎯 **Simpler**: Pure configuration, no code needed
511
+ - 📦 **Bulk-friendly**: Easy to manage hundreds/thousands of redirects
512
+ - 🔧 **Easy updates**: Use `npx launch-config` CLI with CSV/JSON import
513
+
514
+ **Cons:**
515
+ - ❌ No dynamic logic (can't check cookies, headers, geo, etc.)
516
+ - ❌ No conditional redirects based on request data
517
+ - ❌ Limited to exact path or wildcard matching
518
+
519
+ **Setup:**
520
+ ```bash
521
+ # Interactive CLI
522
+ npx launch-config
523
+
524
+ # Or bulk import from CSV/JSON
525
+ npx launch-config
526
+ # Choose option 2 or 3 for bulk import
527
+ ```
528
+
529
+ **Example `launch.json`:**
530
+ ```json
531
+ {
532
+ "redirects": [
533
+ {
534
+ "source": "/old-blog/:slug",
535
+ "destination": "/blog/:slug",
536
+ "statusCode": 301
537
+ },
538
+ {
539
+ "source": "/products/old-*",
540
+ "destination": "/products/new-*",
541
+ "statusCode": 308
542
+ }
543
+ ]
544
+ }
545
+ ```
546
+
547
+ **When to use:**
548
+ - ✅ SEO migrations with hundreds of URL changes
549
+ - ✅ Simple path rewrites (e.g., `/old-path` → `/new-path`)
550
+ - ✅ Bulk product SKU redirects
551
+ - ✅ Static site restructuring
552
+ - ✅ Predictable, rule-based redirects
553
+
554
+ ---
555
+
556
+ #### Option 2: Edge Function Redirects (This Package)
557
+
558
+ **Best for:** Dynamic redirects requiring logic or request inspection
559
+
560
+ **Pros:**
561
+ - 🧠 **Smart**: Access cookies, headers, geo-location, user-agent
562
+ - 🎨 **Flexible**: Complex conditional logic
563
+ - 🔄 **Dynamic**: Redirect based on A/B tests, feature flags, user data
564
+ - 🌍 **Contextual**: Different redirects per country/region
565
+
566
+ **Cons:**
567
+ - 🐌 Slightly slower (edge function execution)
568
+ - 🔧 Requires code changes
569
+ - 📝 More complex for simple redirects
570
+
571
+ **Setup:**
572
+ ```javascript
573
+ import { redirectIfMatch } from '@aryanbansal-launch/edge-utils';
574
+
575
+ export default async function handler(request) {
576
+ // Dynamic redirect based on cookie
577
+ const cookie = request.headers.get('cookie');
578
+ if (cookie?.includes('beta=true')) {
579
+ return Response.redirect(new URL('/beta-features', request.url), 302);
580
+ }
581
+
582
+ // Geo-based redirect
583
+ const country = request.headers.get('x-cs-country');
584
+ if (country === 'FR') {
585
+ return Response.redirect('https://fr.mysite.com', 302);
586
+ }
587
+
588
+ // Conditional redirect
589
+ const redirect = redirectIfMatch(request, {
590
+ path: "/old-page",
591
+ to: "/new-page",
592
+ method: "GET",
593
+ status: 301
594
+ });
595
+ if (redirect) return redirect;
596
+
597
+ return passThrough(request);
598
+ }
599
+ ```
600
+
601
+ **When to use:**
602
+ - ✅ Geo-location based redirects
603
+ - ✅ A/B testing redirects
604
+ - ✅ Cookie/session-based routing
605
+ - ✅ User-agent specific redirects (mobile vs desktop)
606
+ - ✅ Feature flag redirects
607
+ - ✅ Maintenance mode with exceptions
608
+ - ✅ Complex conditional logic
609
+
610
+ ---
611
+
612
+ #### Quick Decision Guide
613
+
614
+ | Scenario | Use Config | Use Edge Function |
615
+ |----------|-----------|-------------------|
616
+ | 500+ simple URL redirects | ✅ | ❌ |
617
+ | SEO migration from old site | ✅ | ❌ |
618
+ | Redirect based on country | ❌ | ✅ |
619
+ | A/B test routing | ❌ | ✅ |
620
+ | Cookie-based redirects | ❌ | ✅ |
621
+ | Mobile vs desktop routing | ❌ | ✅ |
622
+ | Simple path changes | ✅ | ❌ |
623
+ | Maintenance mode (all users) | ✅ | ❌ |
624
+ | Maintenance mode (except admins) | ❌ | ✅ |
625
+ | Bulk product SKU changes | ✅ | ❌ |
626
+
627
+ **💡 Pro Tip:** You can use **both** approaches together! Use `launch.json` for bulk static redirects and edge functions for dynamic logic.
628
+
629
+ **Example Combined Approach:**
630
+ ```javascript
631
+ // launch.json - handles 1000+ static SEO redirects
632
+ {
633
+ "redirects": [
634
+ { "source": "/old-blog/:slug", "destination": "/blog/:slug", "statusCode": 301 }
635
+ // ... 1000 more redirects
636
+ ]
637
+ }
638
+
639
+ // functions/[proxy].edge.js - handles dynamic logic
640
+ export default async function handler(request) {
641
+ // Geo-based redirect (dynamic)
642
+ const country = request.headers.get('x-cs-country');
643
+ if (country === 'FR') {
644
+ return Response.redirect('https://fr.mysite.com', 302);
645
+ }
646
+
647
+ // Static redirects handled by launch.json automatically
648
+ return passThrough(request);
649
+ }
650
+ ```
651
+
652
+ ---
653
+
398
654
  ### ⚛️ Next.js Optimization
399
655
 
400
656
  #### `handleNextJS_RSC(request, options)`
@@ -772,6 +1028,60 @@ export default async function handler(request, context) {
772
1028
 
773
1029
  ---
774
1030
 
1031
+ ## 🧪 Local testing (Wrangler / Miniflare)
1032
+
1033
+ Test your `functions/[proxy].edge.js` chain **locally** without deploying. Wrangler’s dev server uses **Miniflare**, which runs a Workers-compatible runtime on your machine.
1034
+
1035
+ ### Interactive wizard (easiest)
1036
+
1037
+ From your **project root** (where `package.json` lives):
1038
+
1039
+ ```bash
1040
+ npx create-launch-edge local
1041
+ ```
1042
+
1043
+ Or the short alias:
1044
+
1045
+ ```bash
1046
+ npx launch-edge-local
1047
+ ```
1048
+
1049
+ You’ll get a numbered menu (redirect, JSON route, basic auth, bot block, or Next.js RSC). Pick one; the CLI writes `functions/[proxy].edge.js` for that scenario, ensures `dev-worker.edge.js` and `wrangler.toml` exist, then prints a short checklist (start your app, run the dev server, open the test URL). **Wrangler** is installed automatically as a dependency of this package. If `[proxy].edge.js` already exists, you’re asked before it’s overwritten.
1050
+
1051
+ ### Start the local Worker (no `wrangler` typing)
1052
+
1053
+ From the **project root** (next to `wrangler.toml`):
1054
+
1055
+ ```bash
1056
+ npx launch-edge-test-local
1057
+ ```
1058
+
1059
+ This runs the bundled `wrangler dev` for you. Extra arguments are forwarded (for example `--port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`). It is equivalent to `npx wrangler dev`.
1060
+
1061
+ ### Manual setup
1062
+
1063
+ 1. Run `npx create-launch-edge` (or ensure you have `functions/dev-worker.edge.js` and `wrangler.toml`). The init script creates these only if they are missing; it does **not** overwrite an existing `wrangler.toml`.
1064
+ 2. In `wrangler.toml`, set `[vars] BACKEND_URL` to your local app origin (default `http://127.0.0.1:3000`). Wrangler is **already a dependency** of `@aryanbansal-launch/edge-utils`—you do not need `npm install -D wrangler` separately unless you want a different version pinned at the project root.
1065
+ 3. Start your app on that port, then run `npx launch-edge-test-local` (or `npx wrangler dev`) from the **project root**.
1066
+ 4. Open the URL Wrangler prints (for example `http://localhost:8787`). Traffic flows: browser → local Worker → `rewriteRequestToOrigin` → your handler → `fetch` to `BACKEND_URL`.
1067
+
1068
+ Override the backend URL for a single session if needed:
1069
+
1070
+ ```bash
1071
+ npx launch-edge-test-local --var BACKEND_URL=http://127.0.0.1:5173
1072
+ ```
1073
+
1074
+ ### In-repo example
1075
+
1076
+ See [`examples/local-dev/`](examples/local-dev/) for a minimal runnable project (`npm install` in that folder, then `npm run dev` while a server listens on port 3000).
1077
+
1078
+ ### Caveats
1079
+
1080
+ - **Hostname-based rules** (`protectWithBasicAuth`): matching uses both the request URL host and the `Host` header, so `hostnameIncludes: "localhost"` works with `npx launch-edge-test-local` even when the rewritten URL points at `127.0.0.1` (BACKEND_URL).
1081
+ - **Geo and client IP** (`getGeoHeaders`, `getClientIP`): these read request headers. Miniflare does not inject Cloudflare `cf` metadata the way production does; values may be empty unless you set headers yourself or configure Wrangler where supported.
1082
+
1083
+ ---
1084
+
775
1085
  ## 🛠️ CLI Commands
776
1086
 
777
1087
  ### `npx create-launch-edge`
@@ -806,12 +1116,24 @@ Next Steps:
806
1116
 
807
1117
  ---
808
1118
 
1119
+ ### `npx launch-edge-test-local`
1120
+
1121
+ Starts **Wrangler dev** using the Wrangler bundled with this package—no need to type `npx wrangler dev`. Run from the directory that contains `wrangler.toml` (usually your app root).
1122
+
1123
+ ```bash
1124
+ npx launch-edge-test-local
1125
+ ```
1126
+
1127
+ Arguments are passed through to `wrangler dev` (for example `--port 8788` or `--var BACKEND_URL=http://127.0.0.1:5173`).
1128
+
1129
+ ---
1130
+
809
1131
  ### `npx launch-config`
810
1132
 
811
- Interactive CLI to manage `launch.json` configuration.
1133
+ Interactive CLI to manage `launch.json` configuration with support for bulk imports.
812
1134
 
813
1135
  **What it does:**
814
- - Add/manage redirects
1136
+ - Add/manage redirects (one-by-one or bulk import)
815
1137
  - Configure rewrites
816
1138
  - Set up cache priming URLs
817
1139
  - Preserves existing configuration
@@ -825,13 +1147,14 @@ npx launch-config
825
1147
  ```
826
1148
  🚀 Launch Configuration Generator
827
1149
 
828
- Do you want to add a Redirect? (y/n): y
829
- Source path (e.g., /source): /old-page
830
- Destination path (e.g., /destination): /new-page
831
- Status code (default 308): 301
832
- Redirect added.
833
-
834
- Do you want to add a Redirect? another? (y/n): n
1150
+ How would you like to add redirects?
1151
+ 1) One by one (interactive)
1152
+ 2) Bulk import from CSV file
1153
+ 3) Bulk import from JSON file
1154
+ 4) Skip
1155
+ Choose (1-4): 2
1156
+ Enter CSV file path (e.g., ./redirects.csv): ./redirects.csv
1157
+ ✔ Imported 150 redirects from CSV.
835
1158
 
836
1159
  Do you want to add a Rewrite? (y/n): y
837
1160
  Source path (e.g., /api/*): /api/*
@@ -845,6 +1168,47 @@ Enter URLs separated by commas (e.g., /home,/about,/shop): /,/about,/products
845
1168
  ✅ Successfully updated launch.json!
846
1169
  ```
847
1170
 
1171
+ #### Bulk Import Formats
1172
+
1173
+ **CSV Format** (`redirects.csv`):
1174
+ ```csv
1175
+ source,destination,statusCode
1176
+ /old-blog/post-1,/blog/post-1,301
1177
+ /old-blog/post-2,/blog/post-2,301
1178
+ /products/old-sku-123,/products/new-sku-456,308
1179
+ /legacy/*,/new/*,301
1180
+ ```
1181
+
1182
+ **JSON Format** (`redirects.json`):
1183
+ ```json
1184
+ [
1185
+ {
1186
+ "source": "/old-blog/post-1",
1187
+ "destination": "/blog/post-1",
1188
+ "statusCode": 301
1189
+ },
1190
+ {
1191
+ "source": "/old-blog/post-2",
1192
+ "destination": "/blog/post-2",
1193
+ "statusCode": 301
1194
+ },
1195
+ {
1196
+ "source": "/products/old-sku-123",
1197
+ "destination": "/products/new-sku-456",
1198
+ "statusCode": 308
1199
+ }
1200
+ ]
1201
+ ```
1202
+
1203
+ **Use Cases for Bulk Import:**
1204
+ - Migrating from another platform with hundreds of URLs
1205
+ - SEO redirects from spreadsheet/database exports
1206
+ - Bulk product SKU changes
1207
+ - Content restructuring with many path changes
1208
+
1209
+ **Example Files:**
1210
+ See `examples/redirects.csv` and `examples/redirects.json` in this package for ready-to-use templates.
1211
+
848
1212
  **Learn More:**
849
1213
  - [Static Redirects](https://www.contentstack.com/docs/developers/launch/edge-url-redirects)
850
1214
  - [Cache Priming](https://www.contentstack.com/docs/developers/launch/cache-priming)
@@ -865,6 +1229,27 @@ npx launch-help
865
1229
  - Return types and examples
866
1230
  - CLI commands
867
1231
  - Quick links to documentation
1232
+ ## 📖 API Reference
1233
+
1234
+ ### 🧰 Local development
1235
+
1236
+ - **`rewriteRequestToOrigin(request, backendOrigin)`**: Builds a new `Request` whose URL points at `backendOrigin` while preserving path, query, and body. Used by `functions/dev-worker.edge.js` so `passThrough` reaches your local server.
1237
+
1238
+ ### 🛡️ Security
1239
+ - **`blockAICrawlers(request, bots?)`**: Blocks common AI crawlers.
1240
+ - **`ipAccessControl(request, { allow?, deny? })`**: Simple IP-based firewall.
1241
+
1242
+ ### 🔐 Authentication
1243
+ - **`protectWithBasicAuth(request, options)`**: Prompt for credentials based on hostname.
1244
+
1245
+ ### 🔀 Redirection
1246
+ - **`redirectIfMatch(request, options)`**: Perform SEO-friendly redirects at the edge.
1247
+
1248
+ ### 📍 Geo Location
1249
+ - **`getGeoHeaders(request)`**: Returns an object with `country`, `region`, `city`, `latitude`, `longitude`.
1250
+
1251
+ ### ⚛️ Next.js
1252
+ - **`handleNextJS_RSC(request, { affectedPaths })`**: Resolves RSC header issues on Contentstack Launch.
868
1253
 
869
1254
  ---
870
1255