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;
};