Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: introduce im command #1057

Closed
wants to merge 18 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor
ije committed Jan 29, 2025
commit 35197b90ea7c8fd773758e9ffbdba050eed5affa
21 changes: 11 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -33,29 +33,30 @@ More server options please check [config.exmaple.jsonc](./config.example.jsonc).
## Running the Server from Source Code

```bash
go run -tags debug main.go
# or
make dev
make dev/server
```

Then you can import `React` from "http://localhost:8080/react"

## Running Integration Tests
## Running Server Integration Tests

We use [Deno](https://deno.land) to run all the integration testing cases. Make sure you have Deno installed on your computer.

```bash
# Run all tests
make test
make test/server

# Run a specific test
make test dir=react-18
make test/server dir=react-18
```

To add a new integration test case, copy the [test/_template](./test/_template) directory and rename it to your case name.
To add a new integration test case, copy the [test/.template](./test/.template) directory and rename it to your case name.

```bash
cp -r test/_template test/new_test
nvim test/new_test/test.ts
make dir=new_test
# copy the testing template
cp -r test/.template test/fix-xxx
# edit the test code
nvim test/fix-xxx/test.ts
# run the test
make test/server dir=fix-xxx
```
4 changes: 2 additions & 2 deletions HOSTING.md
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ You can find all the server options in [config.example.jsonc](./config.example.j
You will need [Go](https://golang.org/dl) 1.22+ to compile and run the server.

```bash
go run main.go --config=config.json
go run server/cmd/main.go --config=config.json
```

Then you can import `React` from <http://localhost:8080/react>.
@@ -48,7 +48,7 @@ You can deploy the server to a single machine with the [deploy.sh](./scripts/dep

Recommended hosting requirements:

- Linux system (Debian/Ubuntu)
- Linux system (with systemd)
- 4x CPU cores or more
- 8GB RAM or more
- 100GB disk space or more
10 changes: 5 additions & 5 deletions cli/command_im.go
Original file line number Diff line number Diff line change
@@ -5,13 +5,13 @@ import (
"os"
)

const helpMessage = "\033[30mImport Maps Management CLI with esm.sh CDN.\033[0m" + `
const helpMessage = "\033[30mImport Map Management CLI with esm.sh CDN.\033[0m" + `

Usage: esm.sh im [sub-command] [options]
Usage: esm.sh im [sub-command] <options>

Sub Commands:
add [...packages] Add packages to "importmap" script
update <...packages> Update packages in "importmap" script
add [...packages] Add packages to "importmap" script
update [...packages] Update packages in "importmap" script
`

// Manage `importmap` script
@@ -36,6 +36,6 @@ func ManageImportMap(subCommand string) {
case "update":
fmt.Println(packages)
default:
fmt.Printf("Unkown sub command \"%s\"\n", subCommand)
fmt.Printf("Unknown sub command \"%s\"\n", subCommand)
}
}
6 changes: 3 additions & 3 deletions cli/command_init.go
Original file line number Diff line number Diff line change
@@ -45,20 +45,20 @@ func Init(fs *embed.FS) {

if *framework == "" {
*framework = term.Select(raw, "Select a framework:", frameworks)
} else if !stringInSlice(frameworks, *framework) {
} else if !includes(frameworks, *framework) {
fmt.Println("Invalid framework: ", *framework)
os.Exit(1)
}

if *cssFramework == "" {
*cssFramework = term.Select(raw, "Select a CSS framework:", cssFrameworks)
} else if !stringInSlice(cssFrameworks, *cssFramework) {
} else if !includes(cssFrameworks, *cssFramework) {
*cssFramework = cssFrameworks[0]
}

if *lang == "" {
*lang = term.Select(raw, "Select a variant:", langVariants)
} else if !stringInSlice(langVariants, *lang) {
} else if !includes(langVariants, *lang) {
*lang = langVariants[0]
}

17 changes: 9 additions & 8 deletions cli/dev_server.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package cli
import (
"bytes"
"embed"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
@@ -242,7 +243,7 @@ func (d *DevServer) ServeHtml(w http.ResponseWriter, r *http.Request, pathname s
hrefAttr, _ = utils.SplitByFirstByte(hrefAttr, '?')
if hrefAttr == "uno.css" || strings.HasSuffix(hrefAttr, "/uno.css") {
w.Write([]byte("<link rel=\"stylesheet\" href=\""))
w.Write([]byte(hrefAttr + "?ctx=" + btoaUrl(pathname)))
w.Write([]byte(hrefAttr + "?ctx=" + base64.RawURLEncoding.EncodeToString([]byte(pathname))))
w.Write([]byte{'"', '>'})
overriding = "script"
} else {
@@ -254,7 +255,7 @@ func (d *DevServer) ServeHtml(w http.ResponseWriter, r *http.Request, pathname s
case "src":
hrefAttr, _ = utils.SplitByFirstByte(hrefAttr, '?')
w.Write([]byte(" src=\""))
w.Write([]byte(hrefAttr + "?im=" + btoaUrl(pathname)))
w.Write([]byte(hrefAttr + "?im=" + base64.RawURLEncoding.EncodeToString([]byte(pathname))))
w.Write([]byte{'"'})
default:
w.Write([]byte{' '})
@@ -314,12 +315,12 @@ func (d *DevServer) ServeHtml(w http.ResponseWriter, r *http.Request, pathname s

func (d *DevServer) ServeModule(w http.ResponseWriter, r *http.Request, pathname string, sourceCode []byte) {
query := r.URL.Query()
im, err := atobUrl(query.Get("im"))
im, err := base64.RawURLEncoding.DecodeString(query.Get("im"))
if err != nil {
http.Error(w, "Bad Request", 400)
return
}
imHtmlFilename := filepath.Join(d.rootDir, im)
imHtmlFilename := filepath.Join(d.rootDir, string(im))
imHtmlFile, err := os.Open(imHtmlFilename)
if err != nil {
if os.IsNotExist(err) {
@@ -358,7 +359,7 @@ func (d *DevServer) ServeModule(w http.ResponseWriter, r *http.Request, pathname
w.Write([]byte(`throw new Error("Failed to parse import map: invalid JSON")`))
return
}
importMap.Src = "file://" + im
importMap.Src = "file://" + string(im)
break
}
} else if string(tagName) == "body" {
@@ -483,12 +484,12 @@ func (d *DevServer) ServeCSSModule(w http.ResponseWriter, r *http.Request, pathn

func (d *DevServer) ServeUnoCSS(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
ctx, err := atobUrl(query.Get("ctx"))
ctx, err := base64.RawURLEncoding.DecodeString(query.Get("ctx"))
if err != nil {
http.Error(w, "Bad Request", 400)
return
}
imHtmlFilename := filepath.Join(d.rootDir, ctx)
imHtmlFilename := filepath.Join(d.rootDir, string(ctx))
imHtmlFile, err := os.Open(imHtmlFilename)
if err != nil {
if os.IsNotExist(err) {
@@ -535,7 +536,7 @@ func (d *DevServer) ServeUnoCSS(w http.ResponseWriter, r *http.Request) {
if len(innerText) > 0 {
err := json.Unmarshal(innerText, &importMap)
if err == nil {
importMap.Src = ctx
importMap.Src = string(ctx)
}
}
} else if srcAttr == "" {
19 changes: 2 additions & 17 deletions cli/utils.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cli

import (
"encoding/base64"
"flag"
"os"
"strings"
@@ -63,20 +62,6 @@ func endsWith(s string, suffixs ...string) bool {
return false
}

// btoaUrl converts a string to a base64 string.
func btoaUrl(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}

// atobUrl converts a base64 string to a string.
func atobUrl(s string) (string, error) {
data, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(data), nil
}

// parseCommandFlag parses the command flag.
func parseCommandFlag() (string, []string) {
flag.CommandLine.Parse(os.Args[2:])
@@ -100,8 +85,8 @@ func parseCommandFlag() (string, []string) {
return args[0], args[1:]
}

// stringInSlice returns true if the given value is included in the array.
func stringInSlice(arr []string, value string) bool {
// includes returns true if the given value is included in the array.
func includes(arr []string, value string) bool {
for _, v := range arr {
if v == value {
return true
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ require (
github.com/evanw/esbuild v0.24.2
github.com/gorilla/websocket v1.5.3
github.com/ije/esbuild-internal v0.24.2
github.com/ije/gox v0.9.7
github.com/ije/gox v0.9.8
github.com/ije/rex v1.14.7
github.com/mssola/useragent v1.0.0
github.com/yuin/goldmark v1.7.8
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -10,8 +10,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/ije/esbuild-internal v0.24.2 h1:i1sjIu6suFZ1Arc4w0gWNE8OLLiAhEQ3nhU987uB+5w=
github.com/ije/esbuild-internal v0.24.2/go.mod h1:s7HvKZ4ZGifyzvgWpSwnJOQTr6b+bsgfNBZ8HAEwwSM=
github.com/ije/gox v0.9.7 h1:KfEiWOvk/ZqhJnTxJCWMB8mkBLi3HYhGJrrxQyua204=
github.com/ije/gox v0.9.7/go.mod h1:3GTaK8WXf6oxRbrViLqKNLTNcMR871Dz0zoujFNmG48=
github.com/ije/gox v0.9.8 h1:vlkCIy8NxmZAVfZ6eah0/H/RNqMKHXC9FS5kiYdAwbE=
github.com/ije/gox v0.9.8/go.mod h1:3GTaK8WXf6oxRbrViLqKNLTNcMR871Dz0zoujFNmG48=
github.com/ije/rex v1.14.7 h1:j/aS56uE1U6KJVIBk44qAWfWBwxe26aOK2yHUXLgL0k=
github.com/ije/rex v1.14.7/go.mod h1:Gvl2st1enT+SilH1q2enM8o37evm0IN/cx9F+B0z7jU=
github.com/mssola/useragent v1.0.0 h1:WRlDpXyxHDNfvZaPEut5Biveq86Ze4o4EMffyMxmH5o=
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -13,8 +13,8 @@ const helpMessage = "\033[30mesm.sh - A no-build CDN for modern web development.
Usage: esm.sh [command] <options>

Commands:
i, add [...pakcage] Alias as "esm.sh importmap add"
im, importmap Manage "importmap" script
i, add [...pakcage] Alias for 'esm.sh im add'.
im, importmap Manage "importmap" script.
init Create a new no-build web app with esm.sh CDN.
serve Serve a no-build web app with esm.sh CDN, HMR, transforming TS/Vue/Svelte on the fly.
`
60 changes: 26 additions & 34 deletions server/loader.go
Original file line number Diff line number Diff line change
@@ -11,17 +11,9 @@ import (
"path"
"runtime"
"strings"
"sync"

esbuild "github.com/evanw/esbuild/pkg/api"
"github.com/ije/gox/term"
"github.com/ije/gox/utils"
)

var (
loaderRuntime = "deno"
loaderRuntimeVersion = "2.1.4"
compileSyncMap = sync.Map{}
)

type LoaderOutput struct {
@@ -36,7 +28,7 @@ func runLoader(loaderJsPath string, filename string, code string) (output *Loade
stderr, recycle := NewBuffer()
defer recycle()
cmd := exec.Command(
path.Join(config.WorkDir, "bin", loaderRuntime), "run",
path.Join(config.WorkDir, "bin", "deno"), "run",
"--no-config",
"--no-lock",
"--cached-only",
@@ -107,40 +99,40 @@ func buildLoader(wd, loaderJs, outfile string) (err error) {
return
}

func installLoaderRuntime() (err error) {
func installDeno(version string) (installedVersion string, err error) {
binDir := path.Join(config.WorkDir, "bin")
err = ensureDir(binDir)
if err != nil {
return err
return
}

// check local installed deno
installedRuntime, err := exec.LookPath(loaderRuntime)
installedDeno, err := exec.LookPath("deno")
if err == nil {
output, err := run(installedRuntime, "eval", "console.log(Deno.version.deno)")
output, err := run(installedDeno, "eval", "console.log(Deno.version.deno)")
if err == nil {
version := strings.TrimSpace(string(output))
if !semverLessThan(version, "1.45") {
_, err = utils.CopyFile(installedRuntime, path.Join(binDir, loaderRuntime))
if err == nil {
loaderRuntimeVersion = version
v := strings.TrimSpace(string(output))
if !semverLessThan(v, "1.45") {
err = os.Symlink(installedDeno, path.Join(binDir, "deno"))
if err != nil && !os.IsExist(err) {
return "", err
}
return err
return v, nil
}
}
}

if existsFile(path.Join(binDir, loaderRuntime)) {
output, err := run(path.Join(binDir, loaderRuntime), "eval", "console.log(Deno.version.deno)")
if existsFile(path.Join(binDir, "deno")) {
output, err := run(path.Join(binDir, "deno"), "eval", "console.log(Deno.version.deno)")
if err == nil {
version := strings.TrimSpace(string(output))
if !semverLessThan(version, loaderRuntimeVersion) {
return nil
if !semverLessThan(version, version) {
return version, nil
}
}
}

url, err := getLoaderRuntimeInstallURL()
url, err := getDenoInstallURL(version)
if err != nil {
return
}
@@ -156,10 +148,10 @@ func installLoaderRuntime() (err error) {
defer res.Body.Close()

if res.StatusCode != 200 {
return fmt.Errorf("failed to download %s: %s", loaderRuntime, res.Status)
return "", fmt.Errorf("failed to download Deno install package: %s", res.Status)
}

tmpFile := path.Join(binDir, loaderRuntime+".zip")
tmpFile := path.Join(binDir, "deno.zip")
defer os.Remove(tmpFile)

f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY, 0644)
@@ -180,31 +172,31 @@ func installLoaderRuntime() (err error) {
defer zr.Close()

for _, zf := range zr.File {
if zf.Name == loaderRuntime {
if zf.Name == "deno" {
r, err := zf.Open()
if err != nil {
return err
return "", err
}
defer r.Close()

f, err := os.OpenFile(path.Join(binDir, loaderRuntime), os.O_CREATE|os.O_WRONLY, 0755)
f, err := os.OpenFile(path.Join(binDir, "deno"), os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
return err
return "", err
}
defer f.Close()

_, err = io.Copy(f, r)
if err != nil {
return err
return "", err
}
break
}
}

return
return version, nil
}

func getLoaderRuntimeInstallURL() (string, error) {
func getDenoInstallURL(version string) (string, error) {
var arch string
var os string

@@ -228,5 +220,5 @@ func getLoaderRuntimeInstallURL() (string, error) {
return "", errors.New("unsupported os: " + runtime.GOOS)
}

return fmt.Sprintf("https://github.com/denoland/deno/releases/download/v%s/%s-%s-%s.zip", loaderRuntimeVersion, loaderRuntime, arch, os), nil
return fmt.Sprintf("https://github.com/denoland/deno/releases/download/v%s/deno-%s-%s.zip", version, arch, os), nil
}
Loading