Skip to content

Commit

Permalink
Add transformOnly option for build api
Browse files Browse the repository at this point in the history
  • Loading branch information
ije committed Nov 12, 2023
1 parent 464fca3 commit 2f6ee6f
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 63 deletions.
8 changes: 8 additions & 0 deletions build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export async function build(input: string | BuildInput): Promise<BuildOutput> {
return ret;
}

export async function transform(
input: string | BuildInput & { target?: string },
): Promise<{ code: string }> {
const options = typeof input === "string" ? { code: input } : input;
Reflect.set(options, "transformOnly", true);
return await build(options) as unknown as { code: string };
}

export async function esm<T extends object = Record<string, any>>(
strings: TemplateStringsArray,
...values: any[]
Expand Down
87 changes: 49 additions & 38 deletions server/esm_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"path"
"sort"
"strings"
Expand All @@ -19,51 +18,42 @@ 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"`
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"`
}

func apiHandler() rex.Handle {
return func(ctx *rex.Context) interface{} {
if ctx.R.Method == "POST" || ctx.R.Method == "PUT" {
switch ctx.Path.String() {
case "/build":
var input BuildInput
defer ctx.R.Body.Close()
switch ct := ctx.R.Header.Get("Content-Type"); ct {
case "application/json":
err := json.NewDecoder(ctx.R.Body).Decode(&input)
if err != nil {
return rex.Err(400, "failed to parse input config: "+err.Error())
}
case "application/javascript", "text/javascript", "application/typescript", "text/typescript":
code, err := io.ReadAll(ctx.R.Body)
if err != nil {
return rex.Err(400, "failed to read code: "+err.Error())
}
input.Code = string(code)
if strings.Contains(ct, "javascript") {
input.Loader = "jsx"
} else {
input.Loader = "tsx"
}
default:
return rex.Err(400, "invalid content type")
var input BuildInput
err := json.NewDecoder(ctx.R.Body).Decode(&input)
if err != nil {
return rex.Err(400, "invalid body content type")
}
if input.Code == "" {
return rex.Err(400, "code is required")
}
id, err := build(input)
cdnOrigin := getCdnOrign(ctx)
id, err := build(input, cdnOrigin)
if err != nil {
if strings.HasPrefix(err.Error(), "<400> ") {
return rex.Err(400, err.Error()[6:])
}
return rex.Err(500, "failed to save code")
}
cdnOrigin := getCdnOrign(ctx)
ctx.W.Header().Set("Cache-Control", "private, no-store, no-cache, must-revalidate")
if input.TransformOnly {
return map[string]interface{}{
"code": id,
}
}
return map[string]interface{}{
"id": id,
"url": fmt.Sprintf("%s/~%s", cdnOrigin, id),
Expand All @@ -77,17 +67,17 @@ func apiHandler() rex.Handle {
}
}

func build(input BuildInput) (id string, err error) {
func build(input BuildInput, cdnOrigin string) (id string, err error) {
loader := "tsx"
switch input.Loader {
case "js", "jsx", "ts", "tsx":
loader = input.Loader
}
stdin := &api.StdinOptions{
Contents: input.Code,
ResolveDir: "/",
Sourcefile: "index." + loader,
Loader: api.LoaderTSX,
target := api.ESNext
if input.Target != "" {
if t, ok := targets[input.Target]; ok {
target = t
}
}
if input.Deps == nil {
input.Deps = map[string]string{}
Expand All @@ -108,20 +98,33 @@ func build(input BuildInput) (id string, err error) {
} else if _, ok := input.Deps[pkgName]; !ok {
input.Deps[pkgName] = "*"
}
if input.TransformOnly {
path = fmt.Sprintf("%s/%s", cdnOrigin, pkgName)
if version != "" {
path += "@" + version
}
if subPath != "" {
path += "/" + subPath
}
}
}
return api.OnResolveResult{
Path: path,
External: true,
}, nil
}
ret := api.Build(api.BuildOptions{
stdin := &api.StdinOptions{
Contents: input.Code,
ResolveDir: "/",
Sourcefile: "index." + loader,
Loader: api.LoaderTSX,
}
opts := api.BuildOptions{
Outdir: "/esbuild",
Stdin: stdin,
Platform: api.PlatformBrowser,
Format: api.FormatESModule,
TreeShaking: api.TreeShakingTrue,
Target: api.ESNext,
Bundle: true,
Target: target,
MinifyWhitespace: true,
MinifySyntax: true,
Write: false,
Expand All @@ -133,14 +136,22 @@ func build(input BuildInput) (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)
}
if len(ret.OutputFiles) == 0 {
return "", errors.New("<400> failed to validate code: no output files")
}
code := ret.OutputFiles[0].Contents
if input.TransformOnly {
return string(code), nil
}
if len(code) == 0 {
return "", errors.New("<400> code is empty")
}
Expand Down
44 changes: 19 additions & 25 deletions test/build-api/build-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,6 @@ import {
import { build, esm } from "http://localhost:8080/build";

Deno.test("build api", async (t) => {
let url = "";
let bundleUrl = "";
await t.step("build", async () => {
const ret = await fetch("http://localhost:8080/build", {
method: "POST",
headers: { "Content-Type": "application/javascript" },
body: `export default "Hello world!";`,
}).then((r) => r.json());
if (ret.error) {
throw new Error(`<${ret.error.status}> ${ret.error.message}`);
}
url = ret.url;
bundleUrl = ret.bundleUrl;
assertStringIncludes(url, "/~");
assertStringIncludes(bundleUrl, "?bundle");
});

await t.step("import published module", async () => {
const { default: message } = await import(url);
assertEquals(message, "Hello world!");
});
});

Deno.test("build api (json)", async (t) => {
let url = "";
let bundleUrl = "";
await t.step("build", async () => {
Expand All @@ -42,6 +18,7 @@ Deno.test("build api (json)", async (t) => {
import { renderToString } from "npm:[email protected]";
export default () => renderToString(<h1>Hello world!</h1>);
`,
loader: "jsx",
}),
}).then((r) => r.json());
if (ret.error) {
Expand All @@ -61,7 +38,7 @@ Deno.test("build api (json)", async (t) => {
});
});

Deno.test("build api (with types)", async (t) => {
Deno.test("build api (with options)", async () => {
const ret = await fetch("http://localhost:8080/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand All @@ -86,6 +63,23 @@ Deno.test("build api (with types)", async (t) => {
assertEquals(typeof mod.h, "function");
});

Deno.test("build api (transformOnly)", async () => {
const ret = await fetch("http://localhost:8080/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
code: `
const n:number = 42;
`,
transformOnly: true,
}),
}).then((r) => r.json());
if (ret.error) {
throw new Error(`<${ret.error.status}> ${ret.error.message}`);
}
assertEquals(ret.code, "const n=42;\n");
});

Deno.test("build api (use sdk)", async (t) => {
await t.step("use `build` function", async () => {
const ret = await build(`export default "Hello world!";`);
Expand Down

0 comments on commit 2f6ee6f

Please sign in to comment.