mirror of
https://github.com/nvms/esr.git
synced 2025-12-13 06:10: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