Skip to content

Commit

Permalink
run: Use esm-compiler to transform jsx/ts for localhost referer
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Aug 26, 2024
1 parent df9b8f4 commit 3626ae2
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 180 deletions.
File renamed without changes.
15 changes: 15 additions & 0 deletions server/embed/run-tsx.dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// @ts-expect-error $TARGET is defined at build time
import { init, transform } from "/[email protected]/$TARGET/esm_compiler.mjs";
const initPromise = init("/[email protected]/pkg/esm_compiler_bg.wasm");

export async function tsx(
url: URL,
code: string,
importMap: { imports?: Record<string, string> },
target: string,
cachePromise: Promise<Cache>,
): Promise<Response> {
await initPromise;
const ret = transform(url.pathname, code, { importMap, target });
return new Response(ret.code, { headers: { "Content-Type": "application/javascript; charset=utf-8" } });
}
51 changes: 51 additions & 0 deletions server/embed/run-tsx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const stringify = JSON.stringify;

export async function tsx(
url: URL,
code: string,
importMap: { imports?: Record<string, string> },
target: string,
cachePromise: Promise<Cache>,
): Promise<Response> {
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);
}
89 changes: 22 additions & 67 deletions server/embed/run.ts
Original file line number Diff line number Diff line change
@@ -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<ServiceWorker> {
function run(options: RunOptions): Promise<ServiceWorker> {
const serviceWorker = navigator.serviceWorker;
if (!serviceWorker) {
throw new Error("Service Worker is restricted to running across HTTPS for security reasons.");
Expand All @@ -19,14 +18,14 @@ function run(options: RunOptions = {}): Promise<ServiceWorker> {
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<HTMLScriptElement>('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);
Expand All @@ -36,13 +35,13 @@ function run(options: RunOptions = {}): Promise<ServiceWorker> {
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 {
Expand All @@ -54,7 +53,7 @@ function run(options: RunOptions = {}): Promise<ServiceWorker> {
installing.onstatechange = () => {
const waiting = reg.waiting;
if (waiting) {
waiting.onstatechange = run;
waiting.onstatechange = active;
}
};
}
Expand All @@ -66,50 +65,10 @@ function run(options: RunOptions = {}): Promise<ServiceWorker> {
function setupServiceWorker() {
// @ts-expect-error `$TARGET` is injected by esbuild
const target: string = $TARGET;
const on = global.addEventListener;
const importMap: { imports: Record<string, string> } = { imports: {} };
const importMap: { imports?: Record<string, string> } = {};
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
Expand All @@ -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));
}),
);
}
}
});
Expand Down Expand Up @@ -164,11 +124,6 @@ function queryElement<T extends Element>(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. <script type="module" src="https://esm.sh/run" main="/main.mjs" sw="/sw.mjs"></script>
Expand All @@ -195,7 +150,7 @@ if (document) {
});
// compatibility with esm.sh/run(v1) which has been renamed to 'esm.sh/tsx'
queryElement<HTMLScriptElement>("script[type^='text/']", () => {
import(urlFromCurrentModule("/tsx").href);
import(new URL("/tsx", import.meta.url).href);
});
} else if (clients) {
setupServiceWorker();
Expand Down
96 changes: 65 additions & 31 deletions server/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
Expand All @@ -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))
}
Expand Down
Binary file removed test/esm-worker/hello-1.0.0.tgz
Binary file not shown.
Binary file added test/esm-worker/pkg-1.0.0.tgz
Binary file not shown.
Loading

0 comments on commit 3626ae2

Please sign in to comment.