-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathworker.js
More file actions
290 lines (256 loc) · 8.74 KB
/
Copy pathworker.js
File metadata and controls
290 lines (256 loc) · 8.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// Helper to escape regex special characters except brackets
function escapeRegex(str) {
return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
}
// Sort routes: static segments first, then params, then catchalls
function getSegmentScore(segment) {
if (segment.startsWith('[[') && segment.endsWith(']]')) {
return 2 // Catch-all: lowest priority
}
if (segment.startsWith('[') && segment.endsWith(']')) {
return 1 // Parameter: medium priority
}
return 0 // Static: highest priority
}
function parsePathPattern(pathStr, isMiddleware = false) {
const segments = pathStr.split('/').filter(Boolean)
let regexStr = ''
const paramNames = []
for (const segment of segments) {
if (segment.startsWith('[[') && segment.endsWith(']]')) {
const name = segment.slice(2, -2)
paramNames.push({ name, isCatchAll: true })
regexStr += '(?:/(.*))?'
} else if (segment.startsWith('[') && segment.endsWith(']')) {
const name = segment.slice(1, -1)
paramNames.push({ name, isCatchAll: false })
regexStr += '/([^/]+)'
} else {
regexStr += '/' + escapeRegex(segment)
}
}
if (regexStr === '') {
regexStr = '/'
}
const regex = isMiddleware
? (regexStr === '/' ? new RegExp('^(?:/.*)?$') : new RegExp(`^${regexStr}(?:/.*)?$`))
: new RegExp(`^${regexStr}/?$`)
return { regex, paramNames, segments }
}
async function resolveHandlers(module, method) {
const handlerName = 'onRequest' + method.charAt(0).toUpperCase() + method.slice(1).toLowerCase()
let handler = module[handlerName] || module.onRequest || module.default
if (!handler) return []
if (Array.isArray(handler)) return handler
return [handler]
}
export function createWorker(options = {}) {
const { modules, queue: staticQueue, scheduled: staticScheduled } = options
// Override Response.redirect to return mutable Response objects (fixes workerd immutable header errors)
Response.redirect = function (url, status = 302) {
return new Response(null, {
status,
headers: { Location: url },
})
}
// Parse modules into middlewares and route endpoints
const middlewares = []
const routes = []
for (const key of Object.keys(modules || {})) {
const isMiddleware = key.endsWith('_middleware.js')
// Normalize path relative to 'functions'
let relPath = key.replace(/^(\.\.?\/)?functions/, '')
if (isMiddleware) {
let prefix = relPath.slice(0, -'/_middleware.js'.length)
if (prefix === '') prefix = '/'
const { regex, paramNames, segments } = parsePathPattern(prefix, true)
middlewares.push({
key,
prefix,
regex,
paramNames,
segments,
importModule: modules[key],
})
} else {
// Skip queue.js and scheduled.js as they are worker event handlers rather than request endpoints
if (relPath === '/queue.js' || relPath === '/scheduled.js') {
continue
}
let routePath = relPath.replace(/\.js$/, '')
if (routePath.endsWith('/index')) {
routePath = routePath.slice(0, -6)
}
if (routePath === '/index' || routePath === '') {
routePath = '/'
}
const { regex, paramNames, segments } = parsePathPattern(routePath, false)
routes.push({
key,
routePath,
regex,
paramNames,
segments,
importModule: modules[key],
})
}
}
// Sort routes: static segments first, then params, then catchalls
routes.sort((a, b) => {
const minLen = Math.min(a.segments.length, b.segments.length)
for (let i = 0; i < minLen; i++) {
const scoreA = getSegmentScore(a.segments[i])
const scoreB = getSegmentScore(b.segments[i])
if (scoreA !== scoreB) {
return scoreA - scoreB
}
}
return b.segments.length - a.segments.length
})
// Sort middlewares by segment count ascending (root first), then by segment score
middlewares.sort((a, b) => {
const minLen = Math.min(a.segments.length, b.segments.length)
for (let i = 0; i < minLen; i++) {
const scoreA = getSegmentScore(a.segments[i])
const scoreB = getSegmentScore(b.segments[i])
if (scoreA !== scoreB) {
return scoreA - scoreB
}
}
return a.segments.length - b.segments.length
})
return {
async fetch(request, env, ctx) {
if (request.headers.get('Upgrade')?.toLowerCase() === 'websocket') {
return await env.ASSETS.fetch(request)
}
const url = new URL(request.url)
const pathname = url.pathname
const method = request.method
const safeDecode = (str) => {
try {
return decodeURIComponent(str)
} catch {
return str
}
}
// Find matching middlewares and endpoint
const matchedMiddlewares = []
let params = {}
for (const m of middlewares) {
const match = pathname.match(m.regex)
if (match) {
matchedMiddlewares.push(m)
let matchIndex = 1
for (const paramInfo of m.paramNames) {
const val = match[matchIndex++]
if (paramInfo.isCatchAll) {
params[paramInfo.name] = val ? val.split('/').map(safeDecode) : []
} else {
params[paramInfo.name] = val ? safeDecode(val) : undefined
}
}
}
}
let matchedRoute = null
for (const route of routes) {
const match = pathname.match(route.regex)
if (match) {
matchedRoute = route
let matchIndex = 1
for (const paramInfo of route.paramNames) {
const val = match[matchIndex++]
if (paramInfo.isCatchAll) {
params[paramInfo.name] = val ? val.split('/').map(safeDecode) : []
} else {
params[paramInfo.name] = val ? safeDecode(val) : undefined
}
}
break
}
}
const tasks = matchedMiddlewares.map((m) => ({ type: 'middleware', info: m }))
if (matchedRoute) {
tasks.push({ type: 'endpoint', info: matchedRoute })
}
const data = {}
async function runTask(taskIdx, handlerIdx, currentHandlers, req, environment) {
if (handlerIdx < currentHandlers.length) {
const handler = currentHandlers[handlerIdx]
const context = {
request: req,
env: environment,
params,
data,
waitUntil: ctx.waitUntil.bind(ctx),
next: (nextReq, nextEnv) =>
runTask(taskIdx, handlerIdx + 1, currentHandlers, nextReq || req, nextEnv || environment),
}
return await handler(context)
}
if (taskIdx < tasks.length) {
const task = tasks[taskIdx]
try {
const module = await task.info.importModule()
const handlers = await resolveHandlers(module, method)
return await runTask(taskIdx + 1, 0, handlers, req, environment)
} catch (err) {
console.error(`Error executing handler for ${task.info.key}:`, err)
throw err
}
}
// Reached the end of the chain, fallback to ASSETS
try {
const fallbackResponse = await environment.ASSETS.fetch(req)
if (req.headers.get('Upgrade')?.toLowerCase() === 'websocket' || fallbackResponse.status === 101) {
return fallbackResponse
}
return new Response(fallbackResponse.body, fallbackResponse)
} catch (e) {
return new Response(`Not Found: ${e.message}`, { status: 404 })
}
}
return await runTask(0, 0, [], request, env)
},
async queue(batch, env, ctx) {
let queueFn = staticQueue
if (!queueFn && modules) {
const key = Object.keys(modules).find((k) => k.endsWith('/queue.js'))
if (key) {
const mod = await modules[key]()
queueFn = mod.queue || mod.default
}
}
if (queueFn) {
const c = {
batch,
env,
data: {},
waitUntil: (promise) => ctx.waitUntil(promise),
passThroughOnException: () => ctx.passThroughOnException(),
}
return await queueFn(c)
}
},
async scheduled(controller, env, ctx) {
let scheduledFn = staticScheduled
if (!scheduledFn && modules) {
const key = Object.keys(modules).find((k) => k.endsWith('/scheduled.js'))
if (key) {
const mod = await modules[key]()
scheduledFn = mod.scheduled || mod.default
}
}
if (scheduledFn) {
const c = {
controller,
env,
data: {},
waitUntil: (promise) => ctx.waitUntil(promise),
passThroughOnException: () => ctx.passThroughOnException(),
}
return await scheduledFn(c)
}
},
}
}