diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c023415 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2da7c90 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +on: + push: + branches: + - main + pull_request: + +name: run tests +jobs: + test: + + runs-on: ubuntu-latest + env: + GOOS: js + GOARCH: wasm + GO_VERSION: "1.21" + GOLANGCI_LINT_VERSION: v1.55.0 + + steps: + - name: Install Go + if: success() + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Go modules + uses: actions/cache@v3 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run linter + uses: golangci/golangci-lint-action@v3 + with: + version: ${{ env.GOLANGCI_LINT_VERSION }} + skip-pkg-cache: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..36ca989 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,48 @@ +run: + tests: false + deadline: 5m + +linters-settings: + gofumpt: + extra-rules: true + +linters: + enable-all: true + disable: + - interfacebloat + - sqlclosecheck # not relevant (SQL) + - rowserrcheck # not relevant (SQL) + - execinquery # not relevant (SQL) + - interfacer # deprecated + - scopelint # deprecated + - maligned # deprecated + - golint # deprecated + - deadcode # deprecated + - exhaustivestruct # deprecated + - ifshort # deprecated + - nosnakecase # deprecated + - structcheck # deprecated + - varcheck # deprecated + - cyclop # duplicate of gocyclo + - depguard + - exhaustive + - exhaustruct + - forcetypeassert + - funlen + - gochecknoglobals + - gochecknoinits + - gocognit + - gocyclo + - goerr113 + - gomnd + - ireturn + - nestif + - nlreturn + - nonamedreturns + - tagliatelle + - varnamelen + - wrapcheck + - wsl + +issues: + exclude-use-default: false diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..fe222a6 --- /dev/null +++ b/LICENCE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nicholas Wiersma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b5fd35 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +GOOS=js +GOARCH=wasm + +export GOOS +export GOARCH + +# Format all files +fmt: + @echo "==> Formatting source" + @gofmt -s -w $(shell find . -type f -name '*.go' -not -path "./vendor/*") + @echo "==> Done" +.PHONY: fmt + +# Tidy the go.mod file +tidy: + @echo "==> Cleaning go.mod" + @go mod tidy + @echo "==> Done" +.PHONY: tidy + +# Run all tests +test: + @go test -cover ./... +.PHONY: test + +# Lint the project +lint: + @golangci-lint run ./... +.PHONY: lint diff --git a/README.md b/README.md new file mode 100644 index 0000000..d88ca7a --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +![Logo](http://svg.wiersma.co.za/glasslabs/module?title=CLIENT-GO&tag=a%20WAS<%20client%20library) + +[![GitHub release](https://img.shields.io/github/release/glasslabs/looking-glass.svg)](https://github.com/glasslabs/looking-glass/releases) +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/glasslabs/looking-glass/main/LICENSE) + +`client-go` bridges the gap between WASM modules and looking glass. + +**Note:** This is in active development, the API may change. diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..29e23b4 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package client provides bridging functions between WASM modules and looking glass. +package client diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..806376c --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/glasslabs/client-go + +go 1.21.3 + +require ( + gopkg.in/yaml.v3 v3.0.1 + honnef.co/go/js/dom/v2 v2.0.0-20230808055721-96db8f4d5e3b +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b745986 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/js/dom/v2 v2.0.0-20230808055721-96db8f4d5e3b h1:XOEHdukvK2DAtBpN8kQbuj6UIK5dz9DLvqc51o6w4L0= +honnef.co/go/js/dom/v2 v2.0.0-20230808055721-96db8f4d5e3b/go.mod h1:+JtEcbinwR4znM12aluJ3WjKgvhDPKPQ8hnP4YM+4jI= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..271b12e --- /dev/null +++ b/logger.go @@ -0,0 +1,45 @@ +//go:build js && wasm + +package client + +import ( + "os" //nolint:gci + "syscall/js" +) + +// Logger binds to the looking glass logger in the browser. +type Logger struct { + name string +} + +// NewLogger returns a new logger. +func NewLogger() *Logger { + name := os.Args[0] + + return &Logger{ + name: name, + } +} + +// Debug writes a debug log message. +func (l *Logger) Debug(msg string, kvs ...string) { + js.Global().Call("log.debug", msg, toSliceAny(kvs)) +} + +// Info writes a info log message. +func (l *Logger) Info(msg string, kvs ...string) { + js.Global().Call("log.info", msg, toSliceAny(kvs)) +} + +// Error writes a error log message. +func (l *Logger) Error(msg string, kvs ...string) { + js.Global().Call("log.error", msg, toSliceAny(kvs)) +} + +func toSliceAny[T any](a []T) []any { + arr := make([]any, len(a)) + for i, s := range a { + arr[i] = s + } + return arr +} diff --git a/module.go b/module.go new file mode 100644 index 0000000..5b5aaf9 --- /dev/null +++ b/module.go @@ -0,0 +1,97 @@ +//go:build js && wasm + +package client + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + + "gopkg.in/yaml.v3" + "honnef.co/go/js/dom/v2" +) + +// Module bridges the gap between WASM modules and looking glass. +type Module struct { + name string + root dom.Element +} + +// NewModule returns a module. +func NewModule() (*Module, error) { + name := os.Args[0] + + root := dom.GetWindow().Document().QuerySelector("#" + name + ".module") + if root == nil { + return nil, fmt.Errorf("module %q not found", name) + } + + return &Module{ + name: name, + root: root, + }, nil +} + +// Name returns the module name. +func (m *Module) Name() string { + return m.name +} + +// ParseConfig parse the config for the module into v. +func (m *Module) ParseConfig(v any) error { + if len(os.Args) <= 1 { + return nil + } + + return yaml.Unmarshal([]byte(os.Args[1]), v) +} + +// Asset returns the path in the configured asset directory. +func (m *Module) Asset(path string) ([]byte, error) { + assetPath := os.Getenv("ASSETS_URL") + u, err := url.Parse(os.Getenv("ASSETS_URL")) + if err != nil { + return nil, fmt.Errorf("parsing assest path %q: %w", assetPath, err) + } + u = u.JoinPath(path) + + //nolint:noctx // There is not context here anyway. + resp, err := http.DefaultClient.Get(u.String()) + if err != nil { + return nil, fmt.Errorf("getting asset: %w", err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// LoadCSS loads the given styles into the module. +func (m *Module) LoadCSS(styles ...string) error { + for _, style := range styles { + styleElem := dom.GetWindow().Document().CreateElement("style") + styleElem.SetID(m.name) + styleElem.SetTextContent(style) + + headElem := dom.GetWindow().Document().QuerySelector("head") + if headElem == nil { + return errors.New("head element not found") + } + headElem.AppendChild(styleElem) + } + return nil +} + +// Element returns the modules root DOM element. +func (m *Module) Element() dom.Element { + return m.root +}