lopata 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/dev.ts +3 -32
- package/src/s3/proxy.ts +50 -0
- package/src/vite-plugin/dev-server-plugin.ts +27 -0
package/package.json
CHANGED
package/src/cli/dev.ts
CHANGED
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from '../api'
|
|
20
20
|
import { QueuePullConsumer } from '../bindings/queue'
|
|
21
21
|
import type { AckRequest, PullRequest } from '../bindings/queue'
|
|
22
|
-
import type { FileR2Bucket } from '../bindings/r2'
|
|
23
22
|
import { CFWebSocket } from '../bindings/websocket-pair'
|
|
24
23
|
import { autoLoadConfig, loadConfig } from '../config'
|
|
25
24
|
import { handleDashboardRequest } from '../dashboard-serve'
|
|
@@ -29,7 +28,7 @@ import { GenerationManager } from '../generation-manager'
|
|
|
29
28
|
import { loadLopataConfig } from '../lopata-config'
|
|
30
29
|
import { addCfProperty } from '../request-cf'
|
|
31
30
|
import { extractHostname, RouteDispatcher } from '../route-matcher'
|
|
32
|
-
import {
|
|
31
|
+
import { handleS3ProxyRequest, matchS3Path } from '../s3/proxy'
|
|
33
32
|
import { getTraceStore } from '../tracing/store'
|
|
34
33
|
import type { TraceEvent } from '../tracing/types'
|
|
35
34
|
import { WorkerRegistry } from '../worker-registry'
|
|
@@ -265,36 +264,8 @@ export async function run(ctx: CliContext, args: string[]) {
|
|
|
265
264
|
// S3-compatible proxy: /__s3/{bucket}/{key...} → R2 binding on active worker
|
|
266
265
|
const s3Match = matchS3Path(url.pathname)
|
|
267
266
|
if (s3Match) {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
const binding = gen?.env[s3Match.bucket] as FileR2Bucket | undefined
|
|
271
|
-
const resolveBucket = (name: string) => gen?.env[name] as FileR2Bucket | undefined
|
|
272
|
-
const listAllBuckets = () => {
|
|
273
|
-
if (!gen) return []
|
|
274
|
-
const out: Array<{ name: string; creationDate: Date }> = []
|
|
275
|
-
for (const [name, value] of Object.entries(gen.env)) {
|
|
276
|
-
// Duck-typed R2Bucket check — instrumentBinding wraps in a Proxy, so instanceof
|
|
277
|
-
// isn't reliable. R2 bindings are distinguished by having these methods.
|
|
278
|
-
if (
|
|
279
|
-
value
|
|
280
|
-
&& typeof (value as { put?: unknown }).put === 'function'
|
|
281
|
-
&& typeof (value as { head?: unknown }).head === 'function'
|
|
282
|
-
&& typeof (value as { createMultipartUpload?: unknown }).createMultipartUpload === 'function'
|
|
283
|
-
) {
|
|
284
|
-
out.push({ name, creationDate: new Date(0) })
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return out
|
|
288
|
-
}
|
|
289
|
-
const rewritten = new URL(request.url)
|
|
290
|
-
rewritten.pathname = '/' + s3Match.keyPath
|
|
291
|
-
const virtualReq = new Request(rewritten.toString(), {
|
|
292
|
-
method: request.method,
|
|
293
|
-
headers: request.headers,
|
|
294
|
-
body: request.body,
|
|
295
|
-
duplex: 'half',
|
|
296
|
-
})
|
|
297
|
-
return handleS3Request(virtualReq, s3Match.bucket, binding, resolveBucket, listAllBuckets)
|
|
267
|
+
const gen = resolveWorkerParam(url, registry, manager).active
|
|
268
|
+
return handleS3ProxyRequest(request, s3Match, gen?.env)
|
|
298
269
|
}
|
|
299
270
|
|
|
300
271
|
// Queue pull consumer endpoints: POST /cdn-cgi/handler/queues/<name>/messages/pull and /ack
|
package/src/s3/proxy.ts
CHANGED
|
@@ -29,6 +29,19 @@ import {
|
|
|
29
29
|
export type ResolveBucket = (name: string) => FileR2Bucket | undefined
|
|
30
30
|
export type ListAllBuckets = () => Array<{ name: string; creationDate: Date }>
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Duck-typed R2 binding check. `instrumentBinding` wraps R2 buckets in a
|
|
34
|
+
* Proxy, so `instanceof FileR2Bucket` isn't reliable — identify them by the
|
|
35
|
+
* presence of the methods we need from `handleS3Request`.
|
|
36
|
+
*/
|
|
37
|
+
export function isR2Binding(v: unknown): v is FileR2Bucket {
|
|
38
|
+
return !!v
|
|
39
|
+
&& typeof v === 'object'
|
|
40
|
+
&& typeof (v as { put?: unknown }).put === 'function'
|
|
41
|
+
&& typeof (v as { head?: unknown }).head === 'function'
|
|
42
|
+
&& typeof (v as { createMultipartUpload?: unknown }).createMultipartUpload === 'function'
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* Per-bucket CORS configuration XML, set via PutBucketCors and returned by
|
|
34
47
|
* GetBucketCors. Purely observational — lopata's dev server sends a fixed
|
|
@@ -697,3 +710,40 @@ export function matchS3Path(pathname: string): { bucket: string; keyPath: string
|
|
|
697
710
|
if (slash === -1) return { bucket: rest, keyPath: '' }
|
|
698
711
|
return { bucket: rest.slice(0, slash), keyPath: rest.slice(slash + 1) }
|
|
699
712
|
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Given a matched `/__s3/{bucket}/{key...}` request and a worker `env`,
|
|
716
|
+
* rewrite the URL to `/{key}`, duck-type R2 bindings from `env`, and dispatch
|
|
717
|
+
* to `handleS3Request`. Shared by the CLI dev server and the Vite plugin.
|
|
718
|
+
*
|
|
719
|
+
* `env` may be undefined when no worker generation is active yet — the S3
|
|
720
|
+
* handler then sees an undefined bucket binding and responds NoSuchBucket.
|
|
721
|
+
*/
|
|
722
|
+
export function handleS3ProxyRequest(
|
|
723
|
+
req: Request,
|
|
724
|
+
match: { bucket: string; keyPath: string },
|
|
725
|
+
env: Record<string, unknown> | undefined,
|
|
726
|
+
): Promise<Response> {
|
|
727
|
+
const resolveBucket: ResolveBucket = (name) => {
|
|
728
|
+
const v = env?.[name]
|
|
729
|
+
return isR2Binding(v) ? v : undefined
|
|
730
|
+
}
|
|
731
|
+
const binding = resolveBucket(match.bucket)
|
|
732
|
+
const listAllBuckets: ListAllBuckets = () => {
|
|
733
|
+
if (!env) return []
|
|
734
|
+
const out: Array<{ name: string; creationDate: Date }> = []
|
|
735
|
+
for (const [name, value] of Object.entries(env)) {
|
|
736
|
+
if (isR2Binding(value)) out.push({ name, creationDate: new Date(0) })
|
|
737
|
+
}
|
|
738
|
+
return out
|
|
739
|
+
}
|
|
740
|
+
const rewritten = new URL(req.url)
|
|
741
|
+
rewritten.pathname = '/' + match.keyPath
|
|
742
|
+
const virtualReq = new Request(rewritten.toString(), {
|
|
743
|
+
method: req.method,
|
|
744
|
+
headers: req.headers,
|
|
745
|
+
body: req.body,
|
|
746
|
+
duplex: 'half',
|
|
747
|
+
})
|
|
748
|
+
return handleS3Request(virtualReq, match.bucket, binding, resolveBucket, listAllBuckets)
|
|
749
|
+
}
|
|
@@ -51,6 +51,8 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
51
51
|
let handleDashboardRequest: Function
|
|
52
52
|
let handleApiRequest: Function
|
|
53
53
|
let getTraceStore: Function
|
|
54
|
+
let handleS3ProxyRequest: typeof import('../s3/proxy.ts').handleS3ProxyRequest
|
|
55
|
+
let matchS3Path: typeof import('../s3/proxy.ts').matchS3Path
|
|
54
56
|
|
|
55
57
|
// Route dispatcher for multi-worker route-based dispatching
|
|
56
58
|
let routeDispatcher: RouteDispatcher | undefined
|
|
@@ -253,6 +255,7 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
253
255
|
const dashboardMod = await import('../dashboard-serve.ts')
|
|
254
256
|
const apiMod = await import('../api/index.ts')
|
|
255
257
|
const traceMod = await import('../tracing/store.ts')
|
|
258
|
+
const s3Mod = await import('../s3/proxy.ts')
|
|
256
259
|
|
|
257
260
|
wireClassRefs = envMod.wireClassRefs
|
|
258
261
|
setGlobalEnv = envMod.setGlobalEnv
|
|
@@ -265,6 +268,8 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
265
268
|
handleDashboardRequest = dashboardMod.handleDashboardRequest
|
|
266
269
|
handleApiRequest = apiMod.handleApiRequest
|
|
267
270
|
getTraceStore = traceMod.getTraceStore
|
|
271
|
+
handleS3ProxyRequest = s3Mod.handleS3ProxyRequest
|
|
272
|
+
matchS3Path = s3Mod.matchS3Path
|
|
268
273
|
|
|
269
274
|
// 1. Load wrangler config
|
|
270
275
|
if (options.configPath) {
|
|
@@ -524,6 +529,28 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
524
529
|
return
|
|
525
530
|
}
|
|
526
531
|
|
|
532
|
+
// S3-compatible proxy: /__s3/{bucket}/{key...} → R2 binding on the main worker env.
|
|
533
|
+
// Routes straight through to lopata's S3 handler without going through the
|
|
534
|
+
// worker's fetch(), so sandbox uploaders etc. can hit R2 bindings directly
|
|
535
|
+
// with the same URL shape that `bunx lopata dev` exposes.
|
|
536
|
+
{
|
|
537
|
+
const pathOnly = url.split('?')[0] ?? url
|
|
538
|
+
const s3Match = matchS3Path(pathOnly)
|
|
539
|
+
if (s3Match) {
|
|
540
|
+
try {
|
|
541
|
+
const response = await handleS3ProxyRequest(nodeReqToRequest(req), s3Match, env)
|
|
542
|
+
await writeResponse(response, res)
|
|
543
|
+
} catch (err) {
|
|
544
|
+
console.error('[lopata:vite] S3 proxy error:', err)
|
|
545
|
+
if (!res.headersSent) {
|
|
546
|
+
res.writeHead(500, { 'content-type': 'text/plain' })
|
|
547
|
+
res.end(String(err))
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
527
554
|
// Aux worker dispatch: host-based first, then route-based
|
|
528
555
|
{
|
|
529
556
|
const resolved = resolveAuxWorker(req, url)
|