mirror of
https://github.com/nvms/esr.git
synced 2025-12-15 22:40:52 +00:00
first
This commit is contained in:
commit
40d031772c
32
Makefile
Normal file
32
Makefile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
all: help
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test .
|
||||||
|
|
||||||
|
build: ## Builds the binary.
|
||||||
|
go build -o esr ./cmd/esr
|
||||||
|
|
||||||
|
install: ## Installs the binary.
|
||||||
|
go install ./cmd/esr
|
||||||
|
|
||||||
|
test-serve: ## Runs the server.
|
||||||
|
go run cmd/esr/main.go --config example/.esr.yml --serve example/src/index.ts
|
||||||
|
|
||||||
|
test-build-watch: ## Runs esr --build
|
||||||
|
go run cmd/esr/main.go --config example/.esr.yml --watch --build example/src/index.ts
|
||||||
|
|
||||||
|
test-build: ## Runs esr --build
|
||||||
|
go run cmd/esr/main.go --config example/.esr.yml --build example/src/index.ts
|
||||||
|
|
||||||
|
test-init: ## Runs esr init
|
||||||
|
go run cmd/esr/main.go init
|
||||||
|
|
||||||
|
test-run: ## Runs esr
|
||||||
|
go run cmd/esr/main.go --config example/.esr.yml --run example/src/index.ts
|
||||||
|
|
||||||
|
test-run-watch: ## Runs esr --watch
|
||||||
|
go run cmd/esr/main.go --config example/.esr.yml --run --watch example/src/index.ts
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Show help messages for make targets
|
||||||
|
@echo esr && grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(firstword $(MAKEFILE_LIST)) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
16
README.md
Normal file
16
README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
```
|
||||||
|
|
||||||
|
░▒▓████████▓▒░░▒▓███████▓▒░▒▓███████▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░
|
||||||
|
░▒▓██████▓▒░ ░▒▓██████▓▒░░▒▓███████▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░ ░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░
|
||||||
|
░▒▓████████▓▒░▒▓███████▓▒░░▒▓█▓▒░░▒▓█▓▒░
|
||||||
|
|
||||||
|
```
|
||||||
|
# esr
|
||||||
|
|
||||||
|
This is a simple esbuild-powered dev server. It has livereloading. It's fast.
|
||||||
|
|
||||||
|
The end.
|
||||||
71
cmd/esr/main.go
Normal file
71
cmd/esr/main.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/config"
|
||||||
|
lib "esr/internal/esr"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initCommand() {
|
||||||
|
lib.InitProject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
serve bool
|
||||||
|
build bool
|
||||||
|
watch bool
|
||||||
|
run bool
|
||||||
|
)
|
||||||
|
|
||||||
|
flag.Usage = func() {
|
||||||
|
lib.ShowHelpMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.StringVar(&configPath, "config", ".esr.yml", "path to the config file")
|
||||||
|
flag.BoolVar(&serve, "serve", false, "serve")
|
||||||
|
flag.BoolVar(&build, "build", false, "build")
|
||||||
|
flag.BoolVar(&watch, "watch", false, "watch")
|
||||||
|
flag.BoolVar(&run, "run", false, "run")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
args := flag.Args()
|
||||||
|
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "init" {
|
||||||
|
initCommand()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) < 1 {
|
||||||
|
fmt.Println("Error: no entrypoint specified")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryPoint := args[0]
|
||||||
|
|
||||||
|
if _, err := os.Stat(entryPoint); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Error: entrypoint '%s' does not exist\n", entryPoint)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.ReadConfig(configPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("esr :: Failed to read config:", err)
|
||||||
|
|
||||||
|
cfg = config.GetDefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
esr := lib.NewEsr(cfg, entryPoint)
|
||||||
|
|
||||||
|
if build || serve || run {
|
||||||
|
lib.ExecuteTask(esr, build, serve, run, watch)
|
||||||
|
} else if watch {
|
||||||
|
fmt.Println("--watch given, but no action specified (e.g. --build, --serve, --run)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
go.mod
Normal file
12
go.mod
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module esr
|
||||||
|
|
||||||
|
go 1.21.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bmatcuk/doublestar/v3 v3.0.0
|
||||||
|
github.com/evanw/esbuild v0.24.0
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.4.0 // indirect
|
||||||
13
go.sum
Normal file
13
go.sum
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
github.com/bmatcuk/doublestar/v3 v3.0.0 h1:TQtVPlDnAYwcrVNB2JiGuMc++H5qzWZd9PhkNo5WyHI=
|
||||||
|
github.com/bmatcuk/doublestar/v3 v3.0.0/go.mod h1:6PcTVMw80pCY1RVuoqu3V++99uQB3vsSYKPTd8AWA0k=
|
||||||
|
github.com/evanw/esbuild v0.24.0 h1:GZ78naTLp7FKr+K7eNuM/SLs5maeiHYRPsTg6kmdsSE=
|
||||||
|
github.com/evanw/esbuild v0.24.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||||
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
124
internal/config/config.go
Normal file
124
internal/config/config.go
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Common struct {
|
||||||
|
Minify bool `yaml:"minify,omitempty"`
|
||||||
|
MinifyWhitespace bool `yaml:"minifyWhitespace"`
|
||||||
|
MinifyIdentifiers bool `yaml:"minifyIdentifiers"`
|
||||||
|
Bundle bool `yaml:"bundle"`
|
||||||
|
Sourcemap bool `yaml:"sourcemap"`
|
||||||
|
Platform string `yaml:"platform"`
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
Outdir string `yaml:"outdir"`
|
||||||
|
JSX string `yaml:"jsx"`
|
||||||
|
JSXFactory string `yaml:"jsxFactory"`
|
||||||
|
JSXImportSource string `yaml:"jsxImportSource"`
|
||||||
|
Loader map[string]string `yaml:"loader,omitempty"`
|
||||||
|
External []string `yaml:"external,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Serve *ServeConfig `yaml:"serve,omitempty"`
|
||||||
|
Build *BuildConfig `yaml:"build,omitempty"`
|
||||||
|
Run *RunConfig `yaml:"run,omitempty"`
|
||||||
|
Watch *WatchConfig `yaml:"watch,omitempty"`
|
||||||
|
Common `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServeConfig struct {
|
||||||
|
Html string `yaml:"html"`
|
||||||
|
Port int `yaml:"port"`
|
||||||
|
Common `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildConfig struct {
|
||||||
|
Common `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RunConfig struct {
|
||||||
|
Runtime string `yaml:"runtime"`
|
||||||
|
Common `yaml:",inline"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WatchConfig struct {
|
||||||
|
Paths []string `yaml:"paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfig() *Config {
|
||||||
|
defaultConfig := Common{
|
||||||
|
Minify: false,
|
||||||
|
MinifyWhitespace: false,
|
||||||
|
MinifyIdentifiers: false,
|
||||||
|
Bundle: true,
|
||||||
|
Sourcemap: true,
|
||||||
|
Platform: "browser",
|
||||||
|
Format: "esm",
|
||||||
|
Outdir: "public",
|
||||||
|
JSX: "automatic",
|
||||||
|
JSXFactory: "React.createElement",
|
||||||
|
JSXImportSource: "",
|
||||||
|
Loader: map[string]string{},
|
||||||
|
External: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
Serve: &ServeConfig{
|
||||||
|
Html: "public/index.html",
|
||||||
|
Port: 1234,
|
||||||
|
Common: defaultConfig,
|
||||||
|
},
|
||||||
|
Build: &BuildConfig{
|
||||||
|
Common: defaultConfig,
|
||||||
|
},
|
||||||
|
Run: &RunConfig{
|
||||||
|
Runtime: "node",
|
||||||
|
Common: defaultConfig,
|
||||||
|
},
|
||||||
|
Watch: &WatchConfig{
|
||||||
|
Paths: []string{"src/**/*.{ts,tsx,js,jsx}"},
|
||||||
|
},
|
||||||
|
Common: defaultConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadConfig(filename string) (*Config, error) {
|
||||||
|
absPath, err := filepath.Abs(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := GetDefaultConfig()
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &config.Build); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &config.Serve); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &config.Run); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &config.Watch); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
132
internal/config/config_test.go
Normal file
132
internal/config/config_test.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestConfigFile(content string) (string, error) {
|
||||||
|
// Create a temporary file
|
||||||
|
dir, err := os.MkdirTemp("", "config_test")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(dir, "test_config.yml")
|
||||||
|
// Write content to the temp file
|
||||||
|
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfigWithDefaultValues(t *testing.T) {
|
||||||
|
ymlContent := `bundle: true
|
||||||
|
platform: browser
|
||||||
|
format: esm
|
||||||
|
sourcemap: true
|
||||||
|
outdir: public
|
||||||
|
html: index.html
|
||||||
|
port: 8080
|
||||||
|
`
|
||||||
|
|
||||||
|
filePath, err := setupTestConfigFile(ymlContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to setup test config file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(filepath.Dir(filePath))
|
||||||
|
|
||||||
|
config, err := ReadConfig(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &Config{
|
||||||
|
Common: Common{
|
||||||
|
Bundle: true,
|
||||||
|
Sourcemap: true,
|
||||||
|
Platform: "browser",
|
||||||
|
Format: "esm",
|
||||||
|
Outdir: "public",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Platform != expected.Platform {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Platform, expected.Platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Format != expected.Format {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Format, expected.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Outdir != expected.Outdir {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Outdir, expected.Outdir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Bundle != expected.Bundle {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Bundle, expected.Bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Sourcemap != expected.Sourcemap {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Sourcemap, expected.Sourcemap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Serve.Html != "index.html" {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Serve.Html, "index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Serve.Port != 8080 {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Serve.Port, 8080)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadConfigWithCustomBuildConfig(t *testing.T) {
|
||||||
|
ymlContent := `build:
|
||||||
|
minify: true
|
||||||
|
minifyWhitespace: true
|
||||||
|
sourcemap: false
|
||||||
|
`
|
||||||
|
|
||||||
|
filePath, err := setupTestConfigFile(ymlContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to setup test config file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(filepath.Dir(filePath))
|
||||||
|
|
||||||
|
config, err := ReadConfig(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &BuildConfig{
|
||||||
|
Common: Common{
|
||||||
|
Minify: true,
|
||||||
|
MinifyWhitespace: true,
|
||||||
|
MinifyIdentifiers: false,
|
||||||
|
Bundle: true,
|
||||||
|
Sourcemap: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Build.Minify != expected.Minify {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Build.Minify, expected.Minify)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Build.MinifyWhitespace != expected.MinifyWhitespace {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Build.MinifyWhitespace, expected.MinifyWhitespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Build.MinifyIdentifiers != expected.MinifyIdentifiers {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Build.MinifyIdentifiers, expected.MinifyIdentifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Build.Bundle != expected.Bundle {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Build.Bundle, expected.Bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Build.Sourcemap != expected.Sourcemap {
|
||||||
|
t.Errorf("ReadConfig().SomeField = %+v, expected %+v", config.Build.Sourcemap, expected.Sourcemap)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
internal/esr/builder.go
Normal file
75
internal/esr/builder.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/config"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
Config *config.Config
|
||||||
|
EntryPoint string
|
||||||
|
BuiltFiles []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuilder(c *config.Config, e string) *Builder {
|
||||||
|
return &Builder{
|
||||||
|
Config: c,
|
||||||
|
EntryPoint: e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Build(buildOptions *api.BuildOptions) error {
|
||||||
|
result := api.Build(*buildOptions)
|
||||||
|
if len(result.Errors) > 0 {
|
||||||
|
return fmt.Errorf("esr :: build failed: %v", result.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.BuiltFiles = []string{}
|
||||||
|
|
||||||
|
for _, file := range result.OutputFiles {
|
||||||
|
if filepath.Ext(file.Path) != ".map" {
|
||||||
|
dir := filepath.Dir(file.Path)
|
||||||
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("esr :: failed to create directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(file.Path, file.Contents, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("esr :: failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("esr :: wrote: %s\n", filepath.Join(b.Config.Outdir, filepath.Base(file.Path)))
|
||||||
|
|
||||||
|
if filepath.Ext(file.Path) == ".js" {
|
||||||
|
b.BuiltFiles = append(b.BuiltFiles, file.Path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.WriteFile(file.Path, file.Contents, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("esr :: failed to write file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) StartWatch(opts *api.BuildOptions, callback func()) error {
|
||||||
|
paths := append([]string{b.EntryPoint}, b.Config.Watch.Paths...)
|
||||||
|
watcher, err := NewWatcher(paths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to start watcher: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
watcher.Start(func() {
|
||||||
|
if err := b.Build(opts); err != nil {
|
||||||
|
fmt.Println("Watch build error:", err)
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
85
internal/esr/esr.go
Normal file
85
internal/esr/esr.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/config"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Esr struct {
|
||||||
|
Config *config.Config
|
||||||
|
Builder *Builder
|
||||||
|
Server *Server
|
||||||
|
Runner *Runner
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEsr(c *config.Config, entryPoint string) *Esr {
|
||||||
|
builder := NewBuilder(c, entryPoint)
|
||||||
|
server := NewDevServer(c, entryPoint, builder)
|
||||||
|
|
||||||
|
return &Esr{
|
||||||
|
Config: c,
|
||||||
|
Builder: builder,
|
||||||
|
Server: server,
|
||||||
|
Runner: NewRunner(c, builder, entryPoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteTask(esr *Esr, build, serve, run, watch bool) {
|
||||||
|
entryPoint := esr.Builder.EntryPoint
|
||||||
|
|
||||||
|
if build {
|
||||||
|
opts := ModeOptions(esr.Config, ModeBuilder, entryPoint, "")
|
||||||
|
if err := esr.Builder.Build(opts); err != nil {
|
||||||
|
fmt.Println("Build error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serve {
|
||||||
|
opts := ModeOptions(esr.Config, ModeServer, entryPoint, "")
|
||||||
|
if err := esr.Builder.Build(opts); err != nil {
|
||||||
|
fmt.Println("Build before serve error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startServerWithWatching(esr, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if run {
|
||||||
|
esr.Runner.Run(entryPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if watch && !(serve || run) {
|
||||||
|
opts := ModeOptions(esr.Config, ModeServer, entryPoint, "")
|
||||||
|
err := esr.Builder.StartWatch(opts, esr.generalCallback(run, entryPoint))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Watch error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startServerWithWatching(esr *Esr, opts *api.BuildOptions) {
|
||||||
|
err := esr.Builder.StartWatch(opts, esr.serveCallback())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Server watch error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := esr.Server.Start(); err != nil {
|
||||||
|
fmt.Println("Server error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (esr *Esr) generalCallback(run bool, entryPoint string) func() {
|
||||||
|
return func() {
|
||||||
|
if run {
|
||||||
|
esr.Runner.Run(entryPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (esr *Esr) serveCallback() func() {
|
||||||
|
return func() {
|
||||||
|
esr.Server.LiveReload.Reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
125
internal/esr/init.go
Normal file
125
internal/esr/init.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
var indexHtml = `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>esr</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="index.js" type="module"></script>
|
||||||
|
{{ livereload }}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
var defaultConfig = `bundle: true
|
||||||
|
platform: browser
|
||||||
|
format: esm
|
||||||
|
sourcemap: true
|
||||||
|
outdir: public
|
||||||
|
|
||||||
|
watch:
|
||||||
|
paths: ['src/**/*.{ts,tsx,js,jsx,css,scss,html}']
|
||||||
|
|
||||||
|
serve:
|
||||||
|
html: public/index.html
|
||||||
|
port: 1234
|
||||||
|
|
||||||
|
build:
|
||||||
|
minify: true
|
||||||
|
minifyWhitespace: true
|
||||||
|
minifyIdentifiers: true
|
||||||
|
sourcemap: false
|
||||||
|
|
||||||
|
run:
|
||||||
|
runtime: bun
|
||||||
|
sourcemap: false
|
||||||
|
|
||||||
|
jsx: transform
|
||||||
|
jsxFactory: h
|
||||||
|
`
|
||||||
|
|
||||||
|
func CreateDefaultPackageJson() string {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
Die("failed to get current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `{
|
||||||
|
"name": "` + filepath.Base(cwd) + `",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "esr --serve src/index.ts",
|
||||||
|
"build": "esr --build src/index.ts",
|
||||||
|
"build:watch": "esr --build --watch src/index.ts",
|
||||||
|
"run": "esr --run src/index.ts",
|
||||||
|
"run:watch": "esr --run --watch src/index.ts"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitProject() {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
Die("failed to get current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwdFiles, err := os.ReadDir(cwd)
|
||||||
|
if err != nil {
|
||||||
|
Die("failed to read current working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cwdFiles) > 0 {
|
||||||
|
Die("current working directory is not empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
publicDir := filepath.Join(cwd, "public")
|
||||||
|
if err := os.MkdirAll(publicDir, 0755); err != nil {
|
||||||
|
Die("failed to create public directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexHtmlPath := filepath.Join(publicDir, "index.html")
|
||||||
|
if err := os.WriteFile(indexHtmlPath, []byte(indexHtml), 0644); err != nil {
|
||||||
|
Die("failed to write index.html: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcDir := filepath.Join(cwd, "src")
|
||||||
|
if err := os.MkdirAll(srcDir, 0755); err != nil {
|
||||||
|
Die("failed to create src directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexTsPath := filepath.Join(srcDir, "index.ts")
|
||||||
|
if err := os.WriteFile(indexTsPath, []byte("console.log('Hello, world!')\n"), 0644); err != nil {
|
||||||
|
Die("failed to write index.ts: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
esrYmlPath := filepath.Join(cwd, ".esr.yml")
|
||||||
|
if err := os.WriteFile(esrYmlPath, []byte(defaultConfig), 0644); err != nil {
|
||||||
|
Die("failed to write .esr.yml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJsonPath := filepath.Join(cwd, "package.json")
|
||||||
|
if err := os.WriteFile(packageJsonPath, []byte(CreateDefaultPackageJson()), 0644); err != nil {
|
||||||
|
Die("failed to write package.json: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tsconfigJsonPath := filepath.Join(cwd, "tsconfig.json")
|
||||||
|
if err := os.WriteFile(tsconfigJsonPath, []byte(`{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "preserve",
|
||||||
|
"jsxFactory": "h"
|
||||||
|
}
|
||||||
|
}`), 0644); err != nil {
|
||||||
|
Die("failed to write tsconfig.json: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
internal/esr/livereload.go
Normal file
84
internal/esr/livereload.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LiveReload struct {
|
||||||
|
clients map[chan string]bool
|
||||||
|
incoming chan chan string
|
||||||
|
outgoing chan chan string
|
||||||
|
messages chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLiveReload() *LiveReload {
|
||||||
|
return &LiveReload{
|
||||||
|
clients: make(map[chan string]bool),
|
||||||
|
incoming: make(chan chan string),
|
||||||
|
outgoing: make(chan chan string),
|
||||||
|
messages: make(chan string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LiveReload) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
flusher, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientChan := make(chan string)
|
||||||
|
l.incoming <- clientChan
|
||||||
|
defer func() { l.outgoing <- clientChan }()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case message, ok := <-clientChan:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, "data: %s\n\n", message)
|
||||||
|
flusher.Flush()
|
||||||
|
case <-r.Context().Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LiveReload) Start() {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case client := <-l.incoming:
|
||||||
|
l.clients[client] = true
|
||||||
|
case client := <-l.outgoing:
|
||||||
|
delete(l.clients, client)
|
||||||
|
close(client)
|
||||||
|
case msg := <-l.messages:
|
||||||
|
for client := range l.clients {
|
||||||
|
client <- msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(time.Minute) {
|
||||||
|
l.messages <- "waiting"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LiveReload) Reload() {
|
||||||
|
l.messages <- "reload"
|
||||||
|
}
|
||||||
|
|
||||||
|
// JsSnippet generates a JS snippet for including in the template.
|
||||||
|
// const JsSnippet = `<script>const source = new EventSource('/livereload'); source.onmessage = (e) => { if (e.data === 'reload') location.reload(); };</script>`
|
||||||
141
internal/esr/mode.go
Normal file
141
internal/esr/mode.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/config"
|
||||||
|
"esr/internal/esr/plugins"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModeType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeBuilder ModeType = iota
|
||||||
|
ModeServer
|
||||||
|
ModeRunner
|
||||||
|
)
|
||||||
|
|
||||||
|
func ModeOptions(config *config.Config, mode ModeType, entryPoint, tempFilePath string) *api.BuildOptions {
|
||||||
|
buildOptions := api.BuildOptions{
|
||||||
|
EntryPoints: []string{entryPoint},
|
||||||
|
Plugins: []api.Plugin{plugins.EnvPlugin},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply common settings based on the mode
|
||||||
|
getSourceMap := func(sourcemap bool) api.SourceMap {
|
||||||
|
if sourcemap {
|
||||||
|
return api.SourceMapLinked
|
||||||
|
}
|
||||||
|
return api.SourceMapNone
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mode {
|
||||||
|
case ModeBuilder:
|
||||||
|
buildOptions.Bundle = config.Build.Bundle
|
||||||
|
buildOptions.Outdir = config.Build.Outdir
|
||||||
|
buildOptions.MinifySyntax = config.Build.Minify
|
||||||
|
buildOptions.MinifyWhitespace = config.Build.MinifyWhitespace
|
||||||
|
buildOptions.MinifyIdentifiers = config.Build.MinifyIdentifiers
|
||||||
|
buildOptions.JSXFactory = config.Build.JSXFactory
|
||||||
|
buildOptions.JSXImportSource = config.Build.JSXImportSource
|
||||||
|
buildOptions.Sourcemap = getSourceMap(config.Build.Sourcemap)
|
||||||
|
case ModeServer:
|
||||||
|
buildOptions.Bundle = config.Serve.Bundle
|
||||||
|
buildOptions.Outdir = config.Serve.Outdir
|
||||||
|
buildOptions.MinifySyntax = config.Serve.Minify
|
||||||
|
buildOptions.MinifyWhitespace = config.Serve.MinifyWhitespace
|
||||||
|
buildOptions.MinifyIdentifiers = config.Serve.MinifyIdentifiers
|
||||||
|
buildOptions.JSXFactory = config.Serve.JSXFactory
|
||||||
|
buildOptions.JSXImportSource = config.Serve.JSXImportSource
|
||||||
|
buildOptions.Sourcemap = getSourceMap(config.Serve.Sourcemap)
|
||||||
|
case ModeRunner:
|
||||||
|
buildOptions.Bundle = true
|
||||||
|
buildOptions.Format = api.FormatESModule
|
||||||
|
buildOptions.Platform = api.PlatformNode
|
||||||
|
buildOptions.Outfile = tempFilePath
|
||||||
|
buildOptions.Outdir = ""
|
||||||
|
buildOptions.Sourcemap = getSourceMap(config.Run.Sourcemap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply format
|
||||||
|
switch config.Format {
|
||||||
|
case "esm":
|
||||||
|
buildOptions.Format = api.FormatESModule
|
||||||
|
case "cjs":
|
||||||
|
buildOptions.Format = api.FormatCommonJS
|
||||||
|
case "iife":
|
||||||
|
buildOptions.Format = api.FormatIIFE
|
||||||
|
default:
|
||||||
|
buildOptions.Format = api.FormatDefault
|
||||||
|
fmt.Printf("Unknown format: %s, using default\n", config.Format)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply platform
|
||||||
|
switch config.Platform {
|
||||||
|
case "browser":
|
||||||
|
buildOptions.Platform = api.PlatformBrowser
|
||||||
|
case "node":
|
||||||
|
buildOptions.Platform = api.PlatformNode
|
||||||
|
case "neutral":
|
||||||
|
buildOptions.Platform = api.PlatformNeutral
|
||||||
|
default:
|
||||||
|
buildOptions.Platform = api.PlatformDefault
|
||||||
|
fmt.Printf("Unknown platform: %s, using default\n", config.Platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.JSX {
|
||||||
|
case "preserve":
|
||||||
|
buildOptions.JSX = api.JSXPreserve
|
||||||
|
case "transform":
|
||||||
|
buildOptions.JSX = api.JSXTransform
|
||||||
|
case "automatic":
|
||||||
|
buildOptions.JSX = api.JSXAutomatic
|
||||||
|
default:
|
||||||
|
buildOptions.JSX = api.JSXPreserve
|
||||||
|
fmt.Printf("Unknown JSX: %s, using default\n", config.JSX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply loader
|
||||||
|
buildOptions.Loader = make(map[string]api.Loader)
|
||||||
|
for k, v := range config.Loader {
|
||||||
|
switch v {
|
||||||
|
case "file":
|
||||||
|
buildOptions.Loader[k] = api.LoaderFile
|
||||||
|
case "dataurl":
|
||||||
|
buildOptions.Loader[k] = api.LoaderDataURL
|
||||||
|
case "binary":
|
||||||
|
buildOptions.Loader[k] = api.LoaderBinary
|
||||||
|
case "base64":
|
||||||
|
buildOptions.Loader[k] = api.LoaderBase64
|
||||||
|
case "copy":
|
||||||
|
buildOptions.Loader[k] = api.LoaderCopy
|
||||||
|
case "text":
|
||||||
|
buildOptions.Loader[k] = api.LoaderText
|
||||||
|
case "js":
|
||||||
|
buildOptions.Loader[k] = api.LoaderJS
|
||||||
|
case "jsx":
|
||||||
|
buildOptions.Loader[k] = api.LoaderJSX
|
||||||
|
case "tsx":
|
||||||
|
buildOptions.Loader[k] = api.LoaderTSX
|
||||||
|
case "ts":
|
||||||
|
buildOptions.Loader[k] = api.LoaderTS
|
||||||
|
case "json":
|
||||||
|
buildOptions.Loader[k] = api.LoaderJSON
|
||||||
|
case "css":
|
||||||
|
buildOptions.Loader[k] = api.LoaderCSS
|
||||||
|
case "globalcss":
|
||||||
|
buildOptions.Loader[k] = api.LoaderGlobalCSS
|
||||||
|
case "localcss":
|
||||||
|
buildOptions.Loader[k] = api.LoaderLocalCSS
|
||||||
|
case "empty":
|
||||||
|
buildOptions.Loader[k] = api.LoaderEmpty
|
||||||
|
case "default":
|
||||||
|
buildOptions.Loader[k] = api.LoaderDefault
|
||||||
|
default:
|
||||||
|
buildOptions.Loader[k] = api.LoaderNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buildOptions
|
||||||
|
}
|
||||||
40
internal/esr/plugins/env.go
Normal file
40
internal/esr/plugins/env.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evanw/esbuild/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var EnvPlugin = api.Plugin{
|
||||||
|
Name: "env",
|
||||||
|
Setup: func(build api.PluginBuild) {
|
||||||
|
build.OnResolve(api.OnResolveOptions{Filter: `^env$`},
|
||||||
|
func(args api.OnResolveArgs) (api.OnResolveResult, error) {
|
||||||
|
return api.OnResolveResult{
|
||||||
|
Path: args.Path,
|
||||||
|
Namespace: "env-ns",
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "env-ns"},
|
||||||
|
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
|
||||||
|
mappings := make(map[string]string)
|
||||||
|
for _, item := range os.Environ() {
|
||||||
|
if equals := strings.IndexByte(item, '='); equals != -1 {
|
||||||
|
mappings[item[:equals]] = item[equals+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes, err := json.Marshal(mappings)
|
||||||
|
if err != nil {
|
||||||
|
return api.OnLoadResult{}, err
|
||||||
|
}
|
||||||
|
contents := string(bytes)
|
||||||
|
return api.OnLoadResult{
|
||||||
|
Contents: &contents,
|
||||||
|
Loader: api.LoaderJSON,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
66
internal/esr/runner.go
Normal file
66
internal/esr/runner.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/config"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
Config *config.Config
|
||||||
|
EntryPoint string
|
||||||
|
Builder *Builder
|
||||||
|
TempFilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunner(c *config.Config, b *Builder, e string) *Runner {
|
||||||
|
return &Runner{
|
||||||
|
Config: c,
|
||||||
|
EntryPoint: e,
|
||||||
|
Builder: b,
|
||||||
|
TempFilePath: newTempFilePath(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTempFilePath() string {
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
return filepath.Join(tempDir, fmt.Sprintf("esr-%d.mjs", timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run(entryPoint string) {
|
||||||
|
opts := ModeOptions(r.Config, ModeRunner, entryPoint, r.TempFilePath)
|
||||||
|
opts.Outfile = r.TempFilePath
|
||||||
|
|
||||||
|
if err := r.Builder.Build(opts); err != nil {
|
||||||
|
fmt.Println("Build error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(r.Config.Run.Runtime, r.TempFilePath)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Println("Error starting process:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
fmt.Printf("Process finished with error: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("Process finished successfully")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Stop(cmd *exec.Cmd) error {
|
||||||
|
if cmd.Process != nil {
|
||||||
|
return cmd.Process.Signal(syscall.SIGINT)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
110
internal/esr/server.go
Normal file
110
internal/esr/server.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"esr/internal/config"
|
||||||
|
"esr/internal/livereload"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Config *config.Config
|
||||||
|
EntryPoint string
|
||||||
|
LiveReload *LiveReload
|
||||||
|
Builder *Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDevServer(cfg *config.Config, entryPoint string, builder *Builder) *Server {
|
||||||
|
return &Server{
|
||||||
|
Config: cfg,
|
||||||
|
EntryPoint: entryPoint,
|
||||||
|
LiveReload: NewLiveReload(),
|
||||||
|
Builder: builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
s.LiveReload.Start()
|
||||||
|
|
||||||
|
http.Handle("/livereload", s.LiveReload)
|
||||||
|
http.Handle("/", s)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("esr :: Serving on http://localhost:%d\n", s.Config.Serve.Port)
|
||||||
|
if err := http.ListenAndServe(fmt.Sprintf(":%d", s.Config.Serve.Port), nil); err != nil {
|
||||||
|
log.Printf("esr :: Server error: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
<-sigCh
|
||||||
|
log.Println("esr :: Signal received, gracefully shutting down.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
|
dir := filepath.Dir(s.Config.Serve.Html)
|
||||||
|
root := filepath.Join(dir, filepath.Clean(r.URL.Path))
|
||||||
|
|
||||||
|
if stat, err := os.Stat(root); err != nil || stat.IsDir() {
|
||||||
|
if content, err := s.BuildIndex(); err == nil {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(content))
|
||||||
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) BuildIndex() (string, error) {
|
||||||
|
target, err := filepath.Abs(s.Config.Serve.Html)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("index.html").Funcs(template.FuncMap{
|
||||||
|
"livereload": func() template.HTML { return template.HTML(livereload.JsSnippet) },
|
||||||
|
"js": func() template.HTML {
|
||||||
|
var scripts strings.Builder
|
||||||
|
if s.Builder != nil {
|
||||||
|
for _, file := range s.Builder.BuiltFiles {
|
||||||
|
fmt.Fprintf(&scripts, "<script src=\"/%s\"></script>", filepath.Base(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template.HTML(scripts.String())
|
||||||
|
},
|
||||||
|
}).ParseFiles(target)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := tmpl.Execute(&buf, nil); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) TriggerRebuild() error {
|
||||||
|
opts := ModeOptions(s.Config, ModeServer, s.Builder.EntryPoint, "")
|
||||||
|
return s.Builder.Build(opts)
|
||||||
|
}
|
||||||
17
internal/esr/utils.go
Normal file
17
internal/esr/utils.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Die(format string, v ...interface{}) {
|
||||||
|
fmt.Fprintf(os.Stderr, format, v...)
|
||||||
|
os.Stderr.Write([]byte("\n"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowHelpMessage() {
|
||||||
|
fmt.Println(`Usage:
|
||||||
|
esr [--config <path>] [--serve] [--build] [--watch] <entrypoint>`)
|
||||||
|
}
|
||||||
76
internal/esr/watcher.go
Normal file
76
internal/esr/watcher.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package esr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"esr/internal/glob"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Watcher struct {
|
||||||
|
watcher *fsnotify.Watcher
|
||||||
|
paths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWatcher(paths []string) (*Watcher, error) {
|
||||||
|
fsWatcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create fsnotify watcher: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newPaths []string
|
||||||
|
for _, path := range paths {
|
||||||
|
matchedPaths, err := glob.GlobFiles(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to glob files: %v", err)
|
||||||
|
}
|
||||||
|
newPaths = append(newPaths, matchedPaths...)
|
||||||
|
}
|
||||||
|
|
||||||
|
uniquePaths := make(map[string]struct{})
|
||||||
|
for _, path := range newPaths {
|
||||||
|
uniquePaths[path] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPaths := make([]string, 0, len(uniquePaths))
|
||||||
|
for path := range uniquePaths {
|
||||||
|
finalPaths = append(finalPaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Watcher{
|
||||||
|
watcher: fsWatcher,
|
||||||
|
paths: finalPaths,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) Start(callback func()) {
|
||||||
|
for _, path := range w.paths {
|
||||||
|
if err := w.watcher.Add(path); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer w.watcher.Close()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-w.watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
||||||
|
go callback()
|
||||||
|
}
|
||||||
|
case err, ok := <-w.watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) Close() error {
|
||||||
|
return w.watcher.Close()
|
||||||
|
}
|
||||||
32
internal/glob/matcher.go
Normal file
32
internal/glob/matcher.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package glob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GlobFiles(pattern string) ([]string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get current working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := doublestar.Glob(filepath.Join(cwd, pattern))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to match pattern: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, match := range matches {
|
||||||
|
matches[i] = strings.TrimPrefix(match, cwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, match := range matches {
|
||||||
|
matches[i] = strings.TrimPrefix(match, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
52
internal/glob/matcher_test.go
Normal file
52
internal/glob/matcher_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package glob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGlobFiles(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pattern string
|
||||||
|
want []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Test with valid pattern",
|
||||||
|
pattern: "testfiles/**/*.{txt,md}",
|
||||||
|
want: []string{
|
||||||
|
"testfiles/subdir1/test1.txt",
|
||||||
|
"testfiles/subdir2/test2.md",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test with partial pattern",
|
||||||
|
pattern: "testfiles/**/test1.txt",
|
||||||
|
want: []string{
|
||||||
|
"testfiles/subdir1/test1.txt",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test with invalid pattern",
|
||||||
|
pattern: "testfiles/**/test3.txt",
|
||||||
|
want: nil,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GlobFiles(tt.pattern)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GlobFiles() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("GlobFiles() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
0
internal/glob/testfiles/subdir1/test1.txt
Normal file
0
internal/glob/testfiles/subdir1/test1.txt
Normal file
0
internal/glob/testfiles/subdir2/test2.md
Normal file
0
internal/glob/testfiles/subdir2/test2.md
Normal file
18
internal/livereload/livereload.go
Normal file
18
internal/livereload/livereload.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package livereload
|
||||||
|
|
||||||
|
// const JsSnippet = `<script>
|
||||||
|
// const source = new EventSource('/livereload');
|
||||||
|
// source.onmessage = (e) => {
|
||||||
|
// if (e.data === 'reload') {
|
||||||
|
// location.reload(true);
|
||||||
|
// } else {
|
||||||
|
// console.error(e.data);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// source.onerror = () => {
|
||||||
|
// source.onopen = () => location.reload(true);
|
||||||
|
// };
|
||||||
|
// console.info('esr: livereload enabled');
|
||||||
|
// </script>
|
||||||
|
// `
|
||||||
|
const JsSnippet = `<script>const source = new EventSource('/livereload'); source.onmessage = (e) => { if (e.data === 'reload') location.reload(); }; console.info('esr :: livereload listening');</script>`
|
||||||
Loading…
Reference in New Issue
Block a user