@aryanbansal-launch/edge-utils 0.1.12 → 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.
- package/bin/launch-edge-local.js +9 -0
- package/bin/launch-edge-test-local.js +53 -0
- package/bin/launch-help.js +38 -0
- package/bin/launch-init.js +251 -9
- package/dist/auth/basic-auth.js +8 -1
- package/dist/dev/rewrite-origin.js +27 -0
- package/dist/index.js +1 -0
- package/examples/local-dev/node_modules/.package-lock.json +23 -0
- package/examples/local-dev/package-lock.json +29 -0
- package/examples/local-dev/package.json +11 -0
- package/examples/local-dev/sample-handler.ts +18 -0
- package/examples/local-dev/worker.ts +11 -0
- package/examples/local-dev/wrangler.toml +6 -0
- package/package.json +7 -2
- package/readme.md +195 -2
|
@@ -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
|
+
});
|
package/bin/launch-help.js
CHANGED
|
@@ -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}
|
|
@@ -177,6 +187,34 @@ ${colors.dim}──────────────────────
|
|
|
177
187
|
Configure: redirects, rewrites, cache priming
|
|
178
188
|
Supports bulk import from CSV/JSON files
|
|
179
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/
|
|
217
|
+
|
|
180
218
|
${colors.cyan}npx launch-help${colors.reset}
|
|
181
219
|
Display this help guide
|
|
182
220
|
|
package/bin/launch-init.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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();
|
package/dist/auth/basic-auth.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export function protectWithBasicAuth(request, options) {
|
|
2
2
|
const url = new URL(request.url);
|
|
3
|
-
|
|
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
|
@@ -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,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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryanbansal-launch/edge-utils",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -15,11 +15,16 @@
|
|
|
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
30
|
"bin",
|
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) -
|
|
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
|
|
408
|
+
- Returns `null` if neither the URL hostname nor the `Host` header matches `hostnameIncludes`
|
|
303
409
|
|
|
304
410
|
**Example:**
|
|
305
411
|
```javascript
|
|
@@ -922,6 +1028,60 @@ export default async function handler(request, context) {
|
|
|
922
1028
|
|
|
923
1029
|
---
|
|
924
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
|
+
|
|
925
1085
|
## 🛠️ CLI Commands
|
|
926
1086
|
|
|
927
1087
|
### `npx create-launch-edge`
|
|
@@ -956,6 +1116,18 @@ Next Steps:
|
|
|
956
1116
|
|
|
957
1117
|
---
|
|
958
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
|
+
|
|
959
1131
|
### `npx launch-config`
|
|
960
1132
|
|
|
961
1133
|
Interactive CLI to manage `launch.json` configuration with support for bulk imports.
|
|
@@ -1057,6 +1229,27 @@ npx launch-help
|
|
|
1057
1229
|
- Return types and examples
|
|
1058
1230
|
- CLI commands
|
|
1059
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.
|
|
1060
1253
|
|
|
1061
1254
|
---
|
|
1062
1255
|
|