diff --git a/server/embed/run.html b/server/embed/run-demo.html similarity index 100% rename from server/embed/run.html rename to server/embed/run-demo.html diff --git a/server/embed/run-tsx.dev.ts b/server/embed/run-tsx.dev.ts new file mode 100644 index 00000000..88abf951 --- /dev/null +++ b/server/embed/run-tsx.dev.ts @@ -0,0 +1,15 @@ +// @ts-expect-error $TARGET is defined at build time +import { init, transform } from "/esm-compiler@0.6.2/$TARGET/esm_compiler.mjs"; +const initPromise = init("/esm-compiler@0.6.2/pkg/esm_compiler_bg.wasm"); + +export async function tsx( + url: URL, + code: string, + importMap: { imports?: Record }, + target: string, + cachePromise: Promise, +): Promise { + await initPromise; + const ret = transform(url.pathname, code, { importMap, target }); + return new Response(ret.code, { headers: { "Content-Type": "application/javascript; charset=utf-8" } }); +} diff --git a/server/embed/run-tsx.ts b/server/embed/run-tsx.ts new file mode 100644 index 00000000..8ed8dff9 --- /dev/null +++ b/server/embed/run-tsx.ts @@ -0,0 +1,51 @@ +const stringify = JSON.stringify; + +export async function tsx( + url: URL, + code: string, + importMap: { imports?: Record }, + target: string, + cachePromise: Promise, +): Promise { + const filename = url.pathname.split("/").pop()!; + const extname = filename.split(".").pop()!; + const buffer = new Uint8Array( + await crypto.subtle.digest( + "SHA-1", + new TextEncoder().encode(extname + code + stringify(importMap) + target + "false"), + ), + ); + const id = [...buffer].map((b) => b.toString(16).padStart(2, "0")).join(""); + const cache = await cachePromise; + const cacheKey = new URL(url); + cacheKey.searchParams.set("_tsxid", id); + + let res = await cache.match(cacheKey); + if (res) { + return res; + } + + res = await fetch(urlFromCurrentModule(`/+${id}.mjs`)); + if (res.status === 404) { + res = await fetch(urlFromCurrentModule("/transform"), { + method: "POST", + body: stringify({ filename, code, importMap, target }), + }); + const ret = await res.json(); + if (ret.error) { + throw new Error(ret.error.message); + } + res = new Response(ret.code, { headers: { "Content-Type": "application/javascript; charset=utf-8" } }); + } + if (!res.ok) { + return res; + } + + cache.put(cacheKey, res.clone()); + return res; +} + +/** create a URL object from the given path in the current module. */ +function urlFromCurrentModule(path: string) { + return new URL(path, import.meta.url); +} diff --git a/server/embed/run.ts b/server/embed/run.ts index 7b202dc6..01d6ae53 100644 --- a/server/embed/run.ts +++ b/server/embed/run.ts @@ -1,14 +1,13 @@ -/*! 🔥 esm.sh/run - ts/jsx just works™️ in browser. - *! 📚 https://docs.esm.sh/run - */ +/*! 🔥 esm.sh/run - ts/jsx just works™️ in browser. (📚 https://docs.esm.sh/run) */ import type { RunOptions } from "./types/run.d.ts"; +import { tsx } from "./run-tsx"; const global = globalThis; const document: Document | undefined = global.document; const clients: Clients | undefined = global.clients; -function run(options: RunOptions = {}): Promise { +function run(options: RunOptions): Promise { const serviceWorker = navigator.serviceWorker; if (!serviceWorker) { throw new Error("Service Worker is restricted to running across HTTPS for security reasons."); @@ -19,14 +18,14 @@ function run(options: RunOptions = {}): Promise { type: "module", scope: options.swScope, }); - const run = async () => { - const { active } = reg; - if (active?.state === "activated") { + const active = async () => { + const { active: sw } = reg; + if (sw?.state === "activated") { queryElement('script[type="importmap"]', (el) => { try { const { imports } = JSON.parse(el.textContent!); if (imports) { - active.postMessage(["importmap", { imports }]); + sw.postMessage(["importmap", { imports }]); } } catch (e) { throw new Error("Invalid importmap: " + e.message); @@ -36,13 +35,13 @@ function run(options: RunOptions = {}): Promise { if (options.main) { queueMicrotask(() => import(options.main!)); } - resolve(active); + resolve(sw); } }; if (hasController) { - // run the app immediately if the Service Worker is already installed - run(); + // active the app immediately if the Service Worker is already installed + active(); // listen for the new service worker to take over serviceWorker.oncontrollerchange = options.onUpdateFound ?? (() => location.reload()); } else { @@ -54,7 +53,7 @@ function run(options: RunOptions = {}): Promise { installing.onstatechange = () => { const waiting = reg.waiting; if (waiting) { - waiting.onstatechange = run; + waiting.onstatechange = active; } }; } @@ -66,50 +65,10 @@ function run(options: RunOptions = {}): Promise { function setupServiceWorker() { // @ts-expect-error `$TARGET` is injected by esbuild const target: string = $TARGET; - const on = global.addEventListener; - const importMap: { imports: Record } = { imports: {} }; + const importMap: { imports?: Record } = {}; const regexpTsx = /\.(jsx|ts|mts|tsx)$/; const cachePromise = caches.open("esm.sh/run"); - const stringify = JSON.stringify; - - async function tsx(url: URL, code: string) { - const cache = await cachePromise; - const filename = url.pathname.split("/").pop()!; - const extname = filename.split(".").pop()!; - const buffer = new Uint8Array( - await crypto.subtle.digest( - "SHA-1", - new TextEncoder().encode(extname + code + stringify(importMap) + target + "false"), - ), - ); - const id = [...buffer].map((b) => b.toString(16).padStart(2, "0")).join(""); - const cacheKey = new URL(url); - cacheKey.searchParams.set("_tsxid", id); - - let res = await cache.match(cacheKey); - if (res) { - return res; - } - - res = await fetch(urlFromCurrentModule(`/+${id}.mjs`)); - if (res.status === 404) { - res = await fetch(urlFromCurrentModule("/transform"), { - method: "POST", - body: stringify({ filename, code, importMap, target }), - }); - const ret = await res.json(); - if (ret.error) { - throw new Error(ret.error.message); - } - res = new Response(ret.code, { headers: { "Content-Type": "application/javascript; charset=utf-8" } }); - } - if (!res.ok) { - return res; - } - - cache.put(cacheKey, res.clone()); - return res; - } + const on = global.addEventListener; on("install", (evt) => { // @ts-ignore The `skipWaiting` method forces the waiting service worker to become @@ -129,13 +88,14 @@ function setupServiceWorker() { const url = new URL(request.url); const pathname = url.pathname; if (regexpTsx.test(pathname)) { - evt.respondWith((async () => { - const res = await fetch(request); - if (!res.ok || (/^(text|application)\/javascript/.test(res.headers.get("Content-Type") ?? ""))) { - return res; - } - return tsx(url, await res.text()); - })()); + evt.respondWith( + fetch(request).then((res) => { + if (!res.ok || (/^(text|application)\/javascript/.test(res.headers.get("Content-Type") ?? ""))) { + return res; + } + return res.text().then((code) => tsx(url, code, importMap, target, cachePromise)); + }), + ); } } }); @@ -164,11 +124,6 @@ function queryElement(selector: string, callback: (el: T) => } } -/** create a URL object from the given path in the current module. */ -function urlFromCurrentModule(path: string) { - return new URL(path, import.meta.url); -} - if (document) { // run the `main` module if it's provided in the script tag with `src` attribute equals to current script url // e.g. @@ -195,7 +150,7 @@ if (document) { }); // compatibility with esm.sh/run(v1) which has been renamed to 'esm.sh/tsx' queryElement("script[type^='text/']", () => { - import(urlFromCurrentModule("/tsx").href); + import(new URL("/tsx", import.meta.url).href); }); } else if (clients) { setupServiceWorker(); diff --git a/server/router.go b/server/router.go index 61fa94c1..f5ddf236 100644 --- a/server/router.go +++ b/server/router.go @@ -277,9 +277,7 @@ func router() rex.Handle { html := bytes.ReplaceAll(indexHTML, []byte("'# README'"), readmeStrLit) html = bytes.ReplaceAll(html, []byte("{VERSION}"), []byte(fmt.Sprintf("%d", VERSION))) header.Set("Cache-Control", ccMustRevalidate) - if globalETag != "" { - header.Set("ETag", globalETag) - } + header.Set("Etag", globalETag) return rex.Content("index.html", startTime, bytes.NewReader(html)) case "/status.json": @@ -386,22 +384,70 @@ func router() rex.Handle { // replace `$TARGET` with the target data = bytes.ReplaceAll(data, []byte("$TARGET"), []byte(fmt.Sprintf(`"%s"`, target))) - code, err := minify(string(data), targets[target], api.LoaderTS) - if err != nil { - return throwErrorJS(ctx, fmt.Sprintf("Transform error: %v", err), false) + var code []byte + if pathname == "/run" { + referer := ctx.R.Header.Get("Referer") + isLocalhost := strings.HasPrefix(referer, "http://localhost:") || strings.HasPrefix(referer, "http://localhost/") + ret := api.Build(api.BuildOptions{ + Stdin: &api.StdinOptions{ + Sourcefile: "run.ts", + Loader: api.LoaderTS, + Contents: string(data), + }, + Target: targets[target], + Format: api.FormatESModule, + Platform: api.PlatformBrowser, + MinifyWhitespace: true, + MinifyIdentifiers: true, + MinifySyntax: true, + Bundle: true, + Write: false, + Outfile: "-", + LegalComments: api.LegalCommentsExternal, + Plugins: []api.Plugin{{ + Name: "loader", + Setup: func(build api.PluginBuild) { + build.OnResolve(api.OnResolveOptions{Filter: ".*"}, func(args api.OnResolveArgs) (api.OnResolveResult, error) { + if strings.HasPrefix(args.Path, "/") { + return api.OnResolveResult{Path: args.Path, External: true}, nil + } + if args.Path == "./run-tsx" { + return api.OnResolveResult{Path: args.Path, Namespace: "tsx"}, nil + } + return api.OnResolveResult{}, nil + }) + build.OnLoad(api.OnLoadOptions{Filter: ".*", Namespace: "tsx"}, func(args api.OnLoadArgs) (api.OnLoadResult, error) { + sourceFile := "server/embed/run-tsx.ts" + if isLocalhost { + sourceFile = "server/embed/run-tsx.dev.ts" + } + data, err := embedFS.ReadFile(sourceFile) + if err != nil { + return api.OnLoadResult{}, err + } + sourceCode := string(bytes.ReplaceAll(data, []byte("$TARGET"), []byte(target))) + return api.OnLoadResult{Contents: &sourceCode, Loader: api.LoaderTS}, nil + }) + }, + }}, + }) + if ret.Errors != nil { + return throwErrorJS(ctx, fmt.Sprintf("Transform error: %v", ret.Errors), false) + } + code = concatBytes(ret.OutputFiles[0].Contents, ret.OutputFiles[1].Contents) + appendVaryHeader(header, "Referer") + } else { + code, err = minify(string(data), targets[target], api.LoaderTS) + if err != nil { + return throwErrorJS(ctx, fmt.Sprintf("Transform error: %v", err), false) + } } - header.Set("Content-Type", ctJavaScript) if targetByUA { appendVaryHeader(header, "User-Agent") } - if query.Get("v") != "" { - header.Set("Cache-Control", ccImmutable) - } else { - header.Set("Cache-Control", cc1day) - if globalETag != "" { - header.Set("ETag", globalETag) - } - } + header.Set("Content-Type", ctJavaScript) + header.Set("Cache-Control", cc1day) + header.Set("Etag", globalETag) if pathname == "/run" { header.Set("X-Typescript-Types", fmt.Sprintf("%s/run.d.ts", cdnOrigin)) } @@ -468,14 +514,8 @@ func router() rex.Handle { if ifNoneMatch != "" && ifNoneMatch == globalETag { return rex.Status(http.StatusNotModified, "") } - if query := ctx.R.URL.Query(); query.Get("v") != "" { - header.Set("Cache-Control", ccImmutable) - } else { - header.Set("Cache-Control", cc1day) - if globalETag != "" { - header.Set("ETag", globalETag) - } - } + header.Set("Cache-Control", cc1day) + header.Set("Etag", globalETag) } target := getBuildTargetByUA(userAgent) code, err := minify(lib, targets[target], api.LoaderJS) @@ -495,14 +535,8 @@ func router() rex.Handle { if ifNoneMatch != "" && ifNoneMatch == globalETag { return rex.Status(http.StatusNotModified, "") } - if query := ctx.R.URL.Query(); query.Get("v") != "" { - header.Set("Cache-Control", ccImmutable) - } else { - header.Set("Cache-Control", cc1day) - if globalETag != "" { - header.Set("ETag", globalETag) - } - } + header.Set("Cache-Control", cc1day) + header.Set("Etag", globalETag) header.Set("Content-Type", ctTypeScript) return rex.Content(pathname, startTime, bytes.NewReader(data)) } diff --git a/test/esm-worker/hello-1.0.0.tgz b/test/esm-worker/hello-1.0.0.tgz deleted file mode 100644 index a4043fc1..00000000 Binary files a/test/esm-worker/hello-1.0.0.tgz and /dev/null differ diff --git a/test/esm-worker/pkg-1.0.0.tgz b/test/esm-worker/pkg-1.0.0.tgz new file mode 100644 index 00000000..a1991a8b Binary files /dev/null and b/test/esm-worker/pkg-1.0.0.tgz differ diff --git a/test/esm-worker/test.ts b/test/esm-worker/test.ts index 97237243..32c6e028 100644 --- a/test/esm-worker/test.ts +++ b/test/esm-worker/test.ts @@ -91,9 +91,9 @@ Deno.serve( const url = new URL(req.url); const pathname = decodeURIComponent(url.pathname); - if (pathname === "/@private/hello/1.0.0.tgz") { + if (pathname === "/@private/pkg/1.0.0.tgz") { try { - const buf = Deno.readFileSync(join(dirname(new URL(import.meta.url).pathname), "hello-1.0.0.tgz")); + const buf = Deno.readFileSync(join(dirname(new URL(import.meta.url).pathname), "pkg-1.0.0.tgz")); return new Response(buf, { headers: { "content-type": "application/octet-stream", @@ -106,17 +106,17 @@ Deno.serve( } } - if (pathname === "/@private/hello") { + if (pathname === "/@private/pkg") { return Response.json({ - "name": "@private/hello", - "description": "Hello world!", + "name": "@private/pkg", + "description": "My private package", "dist-tags": { "latest": "1.0.0", }, "versions": { "1.0.0": { - "name": "@private/hello", - "description": "Hello world!", + "name": "@private/pkg", + "description": "My private package", "version": "1.0.0", "type": "module", "module": "dist/index.js", @@ -125,11 +125,11 @@ Deno.serve( "dist/", ], "dist": { - "tarball": "http://localhost:8082/@private/hello/1.0.0.tgz", - // shasum -a 1 hello-1.0.0.tgz - "shasum": "E308F75E8F8D4E67853C8BC11E66E217805FC7D7", - // openssl dgst -binary -sha512 hello-1.0.0.tgz | openssl base64 - "integrity": "sha512-lgXANkhDdsvlhWaqrMN3L+d5S0X621h8NFrDA/V4eITPRUhH6YW3OWYG6NSa+n+peubBh7UHAXhtcsxdXUiYMA==", + "tarball": "http://localhost:8082/@private/pkg/1.0.0.tgz", + // shasum -a 1 pkg-1.0.0.tgz + "shasum": "71080422342aac4549dca324bf4361596288ba17", + // openssl dgst -binary -sha512 pkg-1.0.0.tgz | openssl base64 + "integrity": "sha512-sYRCpe+Q0gh6RfBhHsUveq3ihSADt64X8Ag7DCpAlcKrwI/wUF4yrEYlzb9eEJO0t/89Lb+ZSmG7qU4DMsBkrg==", }, }, }, @@ -393,29 +393,33 @@ Deno.test("esm-worker", { sanitizeOps: false, sanitizeResources: false }, async }); await t.step("builtin scripts", async () => { - const res = await fetch(`${workerOrigin}/run`); - res.body?.cancel(); - assertEquals(new URL(res.url).pathname, "/run"); + const res = await fetch(`${workerOrigin}/run`, { redirect: "manual" }); + assert(res.ok); + assert(!res.redirected); assertEquals(res.headers.get("Etag"), `W/"${version}"`); assertEquals(res.headers.get("Cache-Control"), "public, max-age=86400"); assertEquals(res.headers.get("Content-Type"), "application/javascript; charset=utf-8"); assertStringIncludes(res.headers.get("Vary") ?? "", "User-Agent"); + assertStringIncludes(res.headers.get("Vary") ?? "", "Referer"); + assertStringIncludes(await res.text(), '("/transform")'); + const dtsUrl = res.headers.get("X-Typescript-Types")!; assert(dtsUrl.startsWith(workerOrigin)); assert(dtsUrl.endsWith(".d.ts")); - const res2 = await fetch(`${workerOrigin}/run?target=es2022`); - assertEquals(res2.headers.get("Etag"), `W/"${version}"`); - assertEquals(res2.headers.get("Cache-Control"), "public, max-age=86400"); - assertEquals(res2.headers.get("Content-Type"), "application/javascript; charset=utf-8"); - assertStringIncludes(await res2.text(), "esm.sh/run"); - - const res3 = await fetch(`${workerOrigin}/tsx`); - assertEquals(res3.headers.get("Etag"), `W/"${version}"`); - assertEquals(res3.headers.get("Cache-Control"), "public, max-age=86400"); - assertEquals(res3.headers.get("Content-Type"), "application/javascript; charset=utf-8"); - assertStringIncludes(res3.headers.get("Vary") ?? "", "User-Agent"); - assertStringIncludes(await res3.text(), "esm.sh/tsx"); + const res2 = await fetch(`${workerOrigin}/run?target=es2022`, { headers: { "referer": "http://localhost:8080/sw.js" } }); + const code = await res2.text(); + assert(!res2.headers.get("Vary")?.includes("User-Agent")); + assertStringIncludes(res.headers.get("Vary") ?? "", "Referer"); + assertStringIncludes(code, 'from"/esm-compiler@'); + assertStringIncludes(code, '/es2022/esm_compiler.mjs"'); + + const res4 = await fetch(`${workerOrigin}/tsx`); + assertEquals(res4.headers.get("Etag"), `W/"${version}"`); + assertEquals(res4.headers.get("Cache-Control"), "public, max-age=86400"); + assertEquals(res4.headers.get("Content-Type"), "application/javascript; charset=utf-8"); + assertStringIncludes(res4.headers.get("Vary") ?? "", "User-Agent"); + assertStringIncludes(await res4.text(), "esm.sh/tsx"); }); await t.step("transform api", async () => { @@ -486,7 +490,6 @@ Deno.test("esm-worker", { sanitizeOps: false, sanitizeResources: false }, async headers: { "User-Agent": ua }, }); res.body?.cancel(); - console.log(res.headers.get("Vary")); assertStringIncludes(res.headers.get("Vary") ?? "", "User-Agent"); return res.headers.get("x-esm-path")!; }; @@ -541,29 +544,29 @@ Deno.test("esm-worker", { sanitizeOps: false, sanitizeResources: false }, async }); await t.step("private registry", async () => { - const res0 = await fetch(`http://localhost:8082/@private/hello`); + const res0 = await fetch(`http://localhost:8082/@private/pkg`); res0.body?.cancel(); assertEquals(res0.status, 401); - const res1 = await fetch(`http://localhost:8082/@private/hello`, { + const res1 = await fetch(`http://localhost:8082/@private/pkg`, { headers: { authorization: "Bearer " + testRegisterToken }, }); assertEquals(res1.status, 200); const pkg = await res1.json(); - assertEquals(pkg.name, "@private/hello"); + assertEquals(pkg.name, "@private/pkg"); - const res2 = await fetch(`http://localhost:8082/@private/hello/1.0.0.tgz`); + const res2 = await fetch(`http://localhost:8082/@private/pkg/1.0.0.tgz`); res2.body?.cancel(); assertEquals(res2.status, 401); - const res3 = await fetch(`http://localhost:8082/@private/hello/1.0.0.tgz`, { + const res3 = await fetch(`http://localhost:8082/@private/pkg/1.0.0.tgz`, { headers: { authorization: "Bearer " + testRegisterToken }, }); res3.body?.cancel(); assertEquals(res3.status, 200); - const { messsage } = await import(`${workerOrigin}/@private/hello`); - assertEquals(messsage, "Hello world!"); + const { key } = await import(`${workerOrigin}/@private/pkg`); + assertEquals(key, "secret"); }); await t.step("fallback to legacy worker", async () => { @@ -584,6 +587,7 @@ Deno.test("esm-worker", { sanitizeOps: false, sanitizeResources: false }, async }); console.log("storage summary:"); + console.log("Cache", [...cache._store.keys()].map((url) => `${url} (${cache._store.get(url)!.headers.get("Cache-Control")})`)); console.log("R2", [...R2._store.keys()]); closeServer(); diff --git a/worker/src/index.ts b/worker/src/index.ts index 230adb83..f3f143d1 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -32,7 +32,7 @@ const regexpLegacyBuild = /^\/~[a-f0-9]{40}$/; const regexpLocSuffix = /:\d+:\d+$/; /** fetch data from the origin server */ -async function fetchOrigin(req: Request, env: Env, ctx: Context, uri: string): Promise { +async function fetchOrigin(req: Request, env: Env, ctx: Context, pathname: string, query?: string): Promise { const headers = new Headers(); copyHeaders( headers, @@ -57,7 +57,7 @@ async function fetchOrigin(req: Request, env: Env, ctx: Context, uri: string): P headers.set("X-Npmrc", env.NPMRC); } const res = await fetch( - new URL(uri, env.ESM_SERVER_ORIGIN ?? defaultEsmServerOrigin), + new URL(pathname + (query ?? ""), env.ESM_SERVER_ORIGIN ?? defaultEsmServerOrigin), { method: req.method === "HEAD" ? "GET" : req.method, body: req.body, @@ -268,7 +268,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d switch (pathname) { case "/error.js": return ctx.withCache(async () => { - const res = await fetchOrigin(req, env, ctx, pathname + url.search); + const res = await fetchOrigin(req, env, ctx, pathname, url.search); copyHeaders(res.headers, ctx.corsHeaders()); return res; }); @@ -285,30 +285,26 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d pathname === "/tsx" || (pathname.startsWith("/node/") && pathname.endsWith(".js")) ) { - const varyUA = !pathname.endsWith(".ts"); const isChunkjs = pathname.startsWith("/node/chunk-"); if (!isChunkjs) { const ifNoneMatch = h.get("If-None-Match"); if (ifNoneMatch === globalEtag) { const headers = ctx.corsHeaders(); headers.set("Cache-Control", "public, max-age=86400"); + headers.set("Content-Type", getContentType(pathname)); return new Response(null, { status: 304, headers }); } } return ctx.withCache((target) => { - const query: string[] = []; - const v = url.searchParams.get("v"); - if (target) { - query.push(`target=${target}`); + let query = target ? "?target=" + target : undefined; + if (isChunkjs) { + return fetchBuild(req, env, ctx, pathname, query); } - if (v) { - const n = parseInt(v, 10); - if (n >= 136 && n <= VERSION) { - query.push(`v=${v}`); - } - } - return fetchBuild(req, env, ctx, pathname, query.length > 0 ? "?" + query.join("&") : undefined); - }, { varyUA }); + return fetchOrigin(req, env, ctx, pathname, query); + }, { + varyUA: !pathname.endsWith(".d.ts"), + varyReferer: pathname === "/run", + }); } if (middleware) { @@ -359,7 +355,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d // use the default landing page/embedded files if (pathname === "/" || pathname === "/favicon.ico" || pathname.startsWith("/embed/")) { - return fetchOrigin(req, env, ctx, `${pathname}${url.search}`); + return fetchOrigin(req, env, ctx, pathname); } // if it's a singleton build module which is created by https://esm.sh/tsx @@ -490,7 +486,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d )) ) { return ctx.withCache(async () => { - const res = await fetchOrigin(req, env, ctx, url.pathname + url.search); + const res = await fetchOrigin(req, env, ctx, url.pathname, url.search); copyHeaders(res.headers, ctx.corsHeaders()); return res; }); @@ -504,13 +500,12 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d ( !isTargetUrl && !(subPath !== "" && assetsExts.has(splitBy(subPath, ".", true)[1])) && - !subPath.endsWith(".d.ts") && - !subPath.endsWith(".d.mts") && + !isDtsFile(subPath) && !url.searchParams.has("raw") ) ) { return ctx.withCache(async () => { - const res = await fetchOrigin(req, env, ctx, url.pathname + url.search); + const res = await fetchOrigin(req, env, ctx, url.pathname, url.search); copyHeaders(res.headers, ctx.corsHeaders()); return res; }); @@ -643,7 +638,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d // use origin server response for `*.wasm?module` if (ext === "wasm" && url.searchParams.has("module")) { return ctx.withCache(async () => { - const res = await fetchOrigin(req, env, ctx, url.pathname + "?module"); + const res = await fetchOrigin(req, env, ctx, url.pathname, "?module"); copyHeaders(res.headers, ctx.corsHeaders()); return res; }); @@ -665,7 +660,7 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d if (isTargetUrl || isDtsFile(subPath)) { return ctx.withCache(() => { const pathname = `${prefix}/${pkgFullname}@${packageVersion}${subPath}`; - return fetchBuild(req, env, ctx, pathname, undefined); + return fetchBuild(req, env, ctx, pathname); }); } @@ -780,19 +775,28 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d withCache: async (fetcher, options) => { const { pathname, searchParams } = url; const isHeadMethod = req.method === "HEAD"; - const hasPinedTarget = targets.has(searchParams.get("target") ?? ""); - const realOrigin = req.headers.get("X-REAL-ORIGIN"); + const targetArg = searchParams.get("target"); + const hasPinedTarget = !!targetArg && targets.has(targetArg); + const realOrigin = req.headers.get("X-Real-Origin"); const cacheKey = new URL(url); // clone let targetFromUA: string | undefined; + let referer: string | null | undefined; if (options?.varyUA && !hasPinedTarget && !isDtsFile(pathname) && !searchParams.has("raw")) { targetFromUA = getBuildTargetFromUA(req.headers.get("User-Agent")); cacheKey.searchParams.set("target", targetFromUA); } + if (options?.varyReferer) { + referer = req.headers.get("referer"); + cacheKey.searchParams.set( + "referer", + referer?.startsWith("http://localhost:") || referer?.startsWith("http://localhost/") ? "localhost" : "*", + ); + } if (realOrigin) { - cacheKey.searchParams.set("x-origin", realOrigin); + cacheKey.searchParams.set("X-Origin", realOrigin); } if (env.ZONE_ID) { - cacheKey.searchParams.set("x-zone-id", env.ZONE_ID); + cacheKey.searchParams.set("X-Zone-Id", env.ZONE_ID); } let res = await cache.match(cacheKey); if (res) { @@ -804,16 +808,18 @@ function withESMWorker(middleware?: Middleware, cache: Cache = (caches as any).d } return res; } - res = await fetcher(targetFromUA); + res = await fetcher(targetFromUA ?? (hasPinedTarget ? targetArg : null)); if (targetFromUA) { res.headers.append("Vary", "User-Agent"); } + if (options?.varyReferer) { + res.headers.append("Vary", "Referer"); + } if (res.ok && res.headers.get("Cache-Control")?.startsWith("public, max-age=")) { workerCtx.waitUntil(cache.put(cacheKey, res.clone())); } if (isHeadMethod) { - const { status, headers } = res; - return new Response(null, { status, headers }); + return new Response(null, { status: res.status, headers: res.headers }); } return res; }, diff --git a/worker/types/index.d.ts b/worker/types/index.d.ts index 410a6cfa..9a3c0cef 100644 --- a/worker/types/index.d.ts +++ b/worker/types/index.d.ts @@ -26,25 +26,12 @@ declare global { } } -// compatibility with Cloudflare KV -export interface WorkerStorageKV { - getWithMetadata( - key: string, - options: { type: "stream"; cacheTtl?: number }, - ): Promise<{ value: ReadableStream | null; metadata: HttpMetadata | null }>; - put( - key: string, - value: string | ArrayBufferLike | ArrayBuffer | ReadableStream, - options?: { expirationTtl?: number; metadata?: HttpMetadata | null }, - ): Promise; -} - // compatibility with Cloudflare R2 export interface WorkerStorage { get(key: string): Promise< { body: ReadableStream; - httpMetadata?: HttpMetadata; + httpMetadata?: R2HTTPMetadata; customMetadata?: Record; } | null >; @@ -52,7 +39,7 @@ export interface WorkerStorage { key: string, value: ArrayBufferLike | ArrayBuffer | ReadableStream, options?: { - httpMetadata?: HttpMetadata; + httpMetadata?: R2HTTPMetadata; customMetadata?: Record; }, ): Promise; @@ -78,8 +65,8 @@ export type Context = { url: URL; waitUntil(promise: Promise): void; withCache( - fetcher: (targetFromUA?: string) => Promise | Response, - options?: { varyUA: boolean }, + fetcher: (targetFromUA: string | null) => Promise | Response, + options?: { varyUA?: boolean; varyReferer?: boolean }, ): Promise; corsHeaders(headers?: Headers): Headers; };