Skip to content

Commit

Permalink
Improve run script
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Nov 22, 2023
1 parent c5d17f0 commit cd52fde
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 74 deletions.
32 changes: 18 additions & 14 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type TransformOptions = {
| "esnext"
| `es201${5 | 6 | 7 | 8 | 9}`
| `es202${0 | 1 | 2}`;
jsxImportSource?: string;
imports?: Record<string, string>;
};

export type BuildOutput = {
Expand All @@ -27,7 +27,7 @@ export async function build(input: string | BuildInput): Promise<BuildOutput> {
if (!options?.code) {
throw new Error("esm.sh [build] <400> missing code");
}
const ret: any = await fetch(new URL("/build", import.meta.url), {
const ret = await fetch(new URL("/build", import.meta.url), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options),
Expand Down Expand Up @@ -61,13 +61,12 @@ export async function esm<T extends object = Record<string, any>>(
};
}

export async function withCache(
async function withCache(
input: string | BuildInput,
): Promise<BuildOutput> {
let key = typeof input === "string" ? input : JSON.stringify(input);
if (globalThis.crypto) {
key = await hashText(key);
}
const key = await computeHash(
typeof input === "string" ? input : JSON.stringify(input),
);
if (globalThis.localStorage) {
const cached = localStorage.getItem(key);
if (cached) {
Expand All @@ -81,14 +80,19 @@ export async function withCache(
return ret;
}

export async function hashText(s: string): Promise<string> {
const buffer = await crypto.subtle.digest(
"SHA-1",
new TextEncoder().encode(s),
async function computeHash(input: string): Promise<string> {
if (!globalThis.crypto) {
const { h64ToString } = await (await import(`./[email protected]`))
.default();
return h64ToString(input);
}
const buffer = new Uint8Array(
await crypto.subtle.digest(
"SHA-1",
new TextEncoder().encode(input),
),
);
return Array.from(new Uint8Array(buffer)).map((b) =>
b.toString(16).padStart(2, "0")
).join("");
return [...buffer].map((b) => b.toString(16).padStart(2, "0")).join("");
}

export default build;
75 changes: 42 additions & 33 deletions run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,32 @@
*
*/

/// <reference lib="dom" />

const d = document;
const l = localStorage;
const kImportmap = "importmap";
const kJsxImportSource = "@jsxImportSource";
const kScript = "script";
const loaders: Record<string, string> = {
"text/jsx": "jsx",
"text/babel": "jsx",
"text/tsx": "tsx",
"text/ts": "ts",
};

const runScripts: { loader: string; code: string }[] = [];
let jsxImportSource: string | undefined = undefined;
let imports: Record<string, string> | undefined = undefined;

d.querySelectorAll("script").forEach((el) => {
d.querySelectorAll(kScript).forEach((el) => {
let loader: string | null = null;
switch (el.type) {
case "importmap": {
const im = JSON.parse(el.innerHTML);
jsxImportSource = im.imports?.["@jsxImportSource"];
break;
if (el.type === kImportmap) {
imports = JSON.parse(el.innerHTML).imports;
if (imports && HTMLScriptElement.supports?.(kImportmap)) {
imports = { [kJsxImportSource]: imports[kJsxImportSource] };
}
case "text/babel":
case "text/tsx":
loader = "tsx";
break;
case "text/jsx":
loader = "jsx";
break;
case "text/typescript":
case "application/typescript":
loader = "ts";
break;
} else {
loader = loaders[el.type];
}
if (loader) {
runScripts.push({ loader, code: el.innerHTML });
Expand All @@ -35,32 +38,38 @@ d.querySelectorAll("script").forEach((el) => {

runScripts.forEach(async (input) => {
const murl = new URL(import.meta.url);
const buffer = new Uint8Array(
await crypto.subtle.digest(
"SHA-1",
new TextEncoder().encode(
murl.pathname + input.loader + (jsxImportSource ?? "") +
input.code,
),
),
);
const hash = [...buffer].map((b) => b.toString(16).padStart(2, "0"))
.join("");
const hash = await computeHash(JSON.stringify([murl, input, imports]));
const cacheKey = "esm.sh/run/" + hash;
let js = localStorage.getItem(cacheKey);
let js = l.getItem(cacheKey);
if (!js) {
const res = await fetch(murl.origin + `/+${hash}.mjs`);
if (res.ok) {
js = await res.text();
} else {
const { transform } = await import(`./build`);
const ret = await transform({ ...input, jsxImportSource, hash });
const ret = await transform({ ...input, imports, hash });
js = ret.code;
}
localStorage.setItem(cacheKey, js!);
l.setItem(cacheKey, js!);
}
const script = d.createElement("script");
const script = d.createElement(kScript);
script.type = "module";
script.innerHTML = js!;
d.body.appendChild(script);
});

async function computeHash(input: string): Promise<string> {
const c = window.crypto;
if (!c) {
const { h64ToString } = await (await import(`./[email protected]`))
.default();
return h64ToString(input);
}
const buffer = new Uint8Array(
await c.subtle.digest(
"SHA-1",
new TextEncoder().encode(input),
),
);
return [...buffer].map((b) => b.toString(16).padStart(2, "0")).join("");
}
80 changes: 54 additions & 26 deletions server/esm_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import (
)

type BuildInput struct {
Code string `json:"code"`
Loader string `json:"loader,omitempty"`
Deps map[string]string `json:"dependencies,omitempty"`
Types string `json:"types,omitempty"`
TransformOnly bool `json:"transformOnly,omitempty"`
Target string `json:"target,omitempty"`
JsxImportSource string `json:"jsxImportSource,omitempty"`
Hash string `json:"hash,omitempty"`
Code string `json:"code"`
Loader string `json:"loader,omitempty"`
Deps map[string]string `json:"dependencies,omitempty"`
Types string `json:"types,omitempty"`
TransformOnly bool `json:"transformOnly,omitempty"`
Target string `json:"target,omitempty"`
Imports map[string]string `json:"imports,omitempty"`
Hash string `json:"hash,omitempty"`
}

func apiHandler() rex.Handle {
Expand Down Expand Up @@ -111,21 +111,51 @@ func build(input BuildInput, cdnOrigin string) (id string, err error) {
if input.Deps == nil {
input.Deps = map[string]string{}
}

imports := map[string]string{}
trailingSlashImports := map[string]string{}
jsxImportSource := ""
if input.Imports != nil {
for key, value := range input.Imports {
if strings.HasSuffix(key, "/") {
trailingSlashImports[key] = value
} else {
if key == "@jsxImportSource" {
jsxImportSource = value
}
imports[key] = value
}
}
}

onResolver := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
path := args.Path
if isLocalSpecifier(path) {
return api.OnResolveResult{}, errors.New("local specifier is not allowed")
}
if !isHttpSepcifier(path) {
pkgName, version, subPath := splitPkgPath(strings.TrimPrefix(path, "npm:"))
path = pkgName
if subPath != "" {
path += "/" + subPath
if input.TransformOnly {
if value, ok := imports[path]; ok {
path = value
} else {
for key, value := range trailingSlashImports {
if strings.HasPrefix(path, key) {
path = value + path[len(key):]
break
}
}
}
} else {
if isLocalSpecifier(path) {
return api.OnResolveResult{}, errors.New("local specifier is not allowed")
}
if version != "" {
input.Deps[pkgName] = version
} else if _, ok := input.Deps[pkgName]; !ok {
input.Deps[pkgName] = "*"
if !isHttpSepcifier(path) {
pkgName, version, subPath := splitPkgPath(strings.TrimPrefix(path, "npm:"))
path = pkgName
if subPath != "" {
path += "/" + subPath
}
if version != "" {
input.Deps[pkgName] = version
} else if _, ok := input.Deps[pkgName]; !ok {
input.Deps[pkgName] = "*"
}
}
}
return api.OnResolveResult{
Expand All @@ -140,7 +170,7 @@ func build(input BuildInput, cdnOrigin string) (id string, err error) {
Loader: api.LoaderTSX,
}
jsx := api.JSXTransform
if input.JsxImportSource != "" {
if jsxImportSource != "" {
jsx = api.JSXAutomatic
}
opts := api.BuildOptions{
Expand All @@ -150,7 +180,9 @@ func build(input BuildInput, cdnOrigin string) (id string, err error) {
Format: api.FormatESModule,
Target: target,
JSX: jsx,
JSXImportSource: input.JsxImportSource,
JSXImportSource: jsxImportSource,
Bundle: true,
TreeShaking: api.TreeShakingFalse,
MinifyWhitespace: true,
MinifySyntax: true,
Write: false,
Expand All @@ -163,10 +195,6 @@ func build(input BuildInput, cdnOrigin string) (id string, err error) {
},
},
}
if !input.TransformOnly {
opts.Bundle = true
opts.TreeShaking = api.TreeShakingTrue
}
ret := api.Build(opts)
if len(ret.Errors) > 0 {
return "", errors.New("<400> failed to validate code: " + ret.Errors[0].Text)
Expand Down
2 changes: 1 addition & 1 deletion test/build-api/build-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Deno.test("build api (transformOnly)", async () => {
if (ret.error) {
throw new Error(`<${ret.error.status}> ${ret.error.message}`);
}
assertEquals(ret.code, "const n=42;\n");
assertEquals(ret.code, "var n=42;\n");
});

Deno.test("build api (use sdk)", async (t) => {
Expand Down

0 comments on commit cd52fde

Please sign in to comment.