├── .envrc
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .tool-versions
├── Changelog.md
├── License.md
├── Makefile
├── Readme.md
├── contributing
├── Dockerfile
└── Readme.md
├── docs
├── commands.svx
├── controllers.svx
├── generators.svx
└── plugins.svx
├── example
├── basic
│ ├── .gitignore
│ ├── Readme.md
│ ├── controller
│ │ └── controller.go
│ ├── go.mod
│ ├── go.sum
│ ├── package-lock.json
│ ├── package.json
│ └── view
│ │ ├── edit.svelte
│ │ ├── index.svelte
│ │ ├── new.svelte
│ │ └── show.svelte
└── hn
│ ├── .gitignore
│ ├── Readme.md
│ ├── controller
│ ├── controller.go
│ ├── sessions
│ │ └── sessions.go
│ └── users
│ │ └── users.go
│ ├── go.mod
│ ├── go.sum
│ ├── package-lock.json
│ ├── package.json
│ ├── public
│ └── favicon.ico
│ └── view
│ ├── Comment.svelte
│ ├── Header.svelte
│ ├── Story.svelte
│ ├── index.svelte
│ ├── sessions
│ └── new.svelte
│ └── show.svelte
├── framework
├── afs
│ ├── afs.go
│ ├── afs.gotext
│ └── afsrt
│ │ └── appfsrt.go
├── app
│ ├── app.go
│ ├── app.gotext
│ ├── app_test.go
│ ├── loader.go
│ └── state.go
├── controller
│ ├── controller.go
│ ├── controller.gotext
│ ├── controller_test.go
│ ├── controllerrt
│ │ ├── request
│ │ │ ├── request.go
│ │ │ ├── unmarshal.go
│ │ │ └── unmarshal_test.go
│ │ └── response
│ │ │ └── response.go
│ ├── loader.go
│ └── state.go
├── framework.go
├── framework_test.go
├── generator
│ ├── generator.go
│ ├── generator.gotext
│ ├── generator_test.go
│ ├── loader.go
│ └── state.go
├── public
│ ├── loader.go
│ ├── public.go
│ ├── public.gotext
│ ├── public_test.go
│ ├── publicrt
│ │ └── publicrt.go
│ └── state.go
├── transform
│ └── transformrt
│ │ ├── dom.go
│ │ ├── ssr.go
│ │ ├── transform.go
│ │ └── transform_test.go
├── transpiler
│ ├── transpiler.go
│ ├── transpiler.gotext
│ └── transpiler_test.go
├── view
│ ├── dom
│ │ ├── dom.go
│ │ ├── dom.gotext
│ │ └── dom_test.go
│ ├── loader.go
│ ├── nodemodules
│ │ └── nodemodules.go
│ ├── ssr
│ │ ├── jsx.gotext
│ │ ├── jsx.ts
│ │ ├── ssr.go
│ │ ├── ssr.gotext
│ │ ├── ssr.ts
│ │ ├── ssr_test.go
│ │ ├── svelte.gotext
│ │ ├── svelte.js
│ │ └── svelte.ts
│ ├── state.go
│ ├── view.go
│ ├── view.gotext
│ ├── view_test.go
│ └── viewrt
│ │ └── viewrt.go
└── web
│ ├── loader.go
│ ├── state.go
│ ├── web.go
│ ├── web.gotext
│ ├── web_test.go
│ ├── webrt
│ ├── serve.go
│ └── serve_test.go
│ └── welcome
│ ├── build
│ ├── bud
│ │ └── view
│ │ │ └── _index.svelte.js
│ └── index.html
│ └── welcome.go
├── go.mod
├── go.sum
├── install.sh
├── internal
├── ansi
│ └── color.go
├── bail
│ └── bail.go
├── cli
│ ├── build.go
│ ├── build_test.go
│ ├── cli.go
│ ├── cli_test.go
│ ├── create.go
│ ├── create
│ │ └── gomod.gotext
│ ├── create_test.go
│ ├── custom.go
│ ├── generate.go
│ ├── generators.go
│ ├── new_controller.go
│ ├── new_controller
│ │ ├── controller.gotext
│ │ ├── view_edit.gotext
│ │ ├── view_index.gotext
│ │ ├── view_new.gotext
│ │ └── view_show.gotext
│ ├── new_controller_test.go
│ ├── run.go
│ ├── tool_cache.go
│ ├── tool_di.go
│ ├── tool_ds.go
│ ├── tool_fs_cat.go
│ ├── tool_fs_ls.go
│ ├── tool_fs_tree.go
│ ├── tool_fs_txtar.go
│ ├── tool_v8.go
│ ├── tool_v8_test.go
│ └── version.go
├── current
│ ├── current.go
│ └── current_test.go
├── dag
│ ├── dag.go
│ ├── dag_test.go
│ ├── discard.go
│ └── sqlite.go
├── dag2
│ ├── dag.go
│ └── dag_test.go
├── dsync
│ ├── dsync.go
│ └── dsync_test.go
├── embed
│ └── file.go
├── embedded
│ ├── embedded.go
│ ├── favicon.ico
│ └── gitignore.txt
├── entrypoint
│ ├── find.go
│ ├── list.go
│ ├── list_test.go
│ └── state.go
├── envs
│ ├── envs.go
│ └── envs_test.go
├── errs
│ ├── errs.go
│ └── errs_test.go
├── esmeta
│ └── esmeta.go
├── extrafile
│ ├── extrafile.go
│ └── extrafile_test.go
├── gitignore
│ ├── gitignore.go
│ └── gitignore_test.go
├── glob
│ ├── base.go
│ ├── base_test.go
│ ├── bases.go
│ ├── bases_test.go
│ ├── compile.go
│ ├── compile_test.go
│ ├── expand.go
│ └── expand_test.go
├── gois
│ ├── builtin.go
│ └── stdlib.go
├── is
│ ├── is.go
│ ├── is_internal.go
│ └── is_test.go
├── mergefs
│ ├── mergefs.go
│ └── mergefs_test.go
├── npm
│ ├── npm.go
│ └── npm_test.go
├── once
│ ├── bytes.go
│ ├── bytes_test.go
│ ├── closer.go
│ ├── closer_test.go
│ ├── direntries.go
│ ├── error.go
│ ├── error_test.go
│ ├── fileinfo.go
│ ├── string.go
│ └── string_test.go
├── orderedset
│ ├── strings.go
│ └── strings_test.go
├── printfs
│ ├── printfs.go
│ └── printfs_test.go
├── prompter
│ └── prompter.go
├── pubsub
│ ├── discard.go
│ ├── pubsub.go
│ └── pubsub_test.go
├── scaffold
│ └── scaffold.go
├── shell
│ ├── command.go
│ └── command_test.go
├── sig
│ ├── sig.go
│ └── sig_test.go
├── stacktrace
│ └── stacktrace.go
├── targz
│ ├── all_test.go
│ ├── unzip.go
│ └── zip.go
├── terminal
│ ├── html.go
│ └── terminal.css
├── testcli
│ ├── process.go
│ └── testcli.go
├── testsub
│ ├── testsub.go
│ └── testsub_test.go
├── txtar
│ ├── testdata
│ │ └── one.txt
│ ├── txtar.go
│ └── txtar_test.go
├── urlx
│ ├── parse.go
│ ├── parse.peg
│ ├── parse.peg.go
│ └── parse_test.go
└── versions
│ ├── align.go
│ ├── align_test.go
│ ├── check.go
│ ├── check_test.go
│ └── versions.go
├── livebud
├── package-lock.json
├── package.json
├── qs
│ ├── index.ts
│ └── index_test.ts
├── runtime
│ ├── hot
│ │ └── index.ts
│ ├── index.ts
│ ├── jsx
│ │ └── index.ts
│ └── svelte
│ │ └── index.ts
├── tsconfig.json
└── url
│ └── index.ts
├── main.go
├── package-lock.json
├── package.json
├── package
├── budhttp
│ ├── budsvr
│ │ ├── handler.go
│ │ ├── server.go
│ │ └── server_test.go
│ ├── client.go
│ ├── client_test.go
│ └── discard.go
├── commander
│ ├── arg.go
│ ├── args.go
│ ├── bool.go
│ ├── cli.go
│ ├── cli_test.go
│ ├── color.go
│ ├── command.go
│ ├── custom.go
│ ├── flag.go
│ ├── int.go
│ ├── string.go
│ ├── stringmap.go
│ ├── strings.go
│ ├── usage.go
│ └── usage.gotext
├── di
│ ├── alias.go
│ ├── di.go
│ ├── di_test.go
│ ├── error.go
│ ├── finder.go
│ ├── function.go
│ ├── generator.go
│ ├── hoist.go
│ ├── injector.go
│ ├── node.go
│ ├── provider.go
│ ├── struct.go
│ └── type.go
├── es
│ ├── bundle_test.go
│ ├── es.go
│ ├── http.go
│ ├── http_test.go
│ ├── importmap.go
│ ├── importmap_test.go
│ ├── nodemodules.go
│ └── serve_test.go
├── finder
│ └── finder.go
├── genfs
│ ├── dir.go
│ ├── direntry.go
│ ├── embed.go
│ ├── external.go
│ ├── file.go
│ ├── fileserver.go
│ ├── genfs.go
│ ├── genfs_test.go
│ ├── mode.go
│ ├── scoped.go
│ ├── tree.go
│ ├── tree_test.go
│ └── wrapfile.go
├── gomod
│ ├── file.go
│ ├── file_test.go
│ ├── goroot.go
│ ├── mod.go
│ ├── mod_test.go
│ └── module.go
├── gotemplate
│ ├── gotemplate.go
│ └── gotemplate_test.go
├── hot
│ ├── client.go
│ ├── hot_test.go
│ └── server.go
├── imports
│ ├── imports.go
│ └── imports_test.go
├── js
│ ├── js.go
│ └── v8
│ │ ├── v8.go
│ │ └── v8_test.go
├── log
│ ├── console
│ │ ├── console.go
│ │ └── console_test.go
│ ├── discard.go
│ ├── level.go
│ ├── levelfilter
│ │ └── levelfilter.go
│ ├── log.go
│ ├── logfmt
│ │ └── logfmt.go
│ ├── memory
│ │ ├── memory.go
│ │ └── memory_test.go
│ ├── multi
│ │ └── multi.go
│ ├── ndjson
│ │ └── ndjson.go
│ └── testlog
│ │ └── testlog.go
├── middleware
│ ├── httpbuffer
│ │ ├── middleware.go
│ │ └── middleware_test.go
│ ├── methodoverride
│ │ ├── methodoverride.go
│ │ └── methodoverride_test.go
│ ├── middleware.go
│ └── middleware_test.go
├── modcache
│ ├── modcache.go
│ └── modcache_test.go
├── parser
│ ├── alias.go
│ ├── builtin.go
│ ├── definition.go
│ ├── field.go
│ ├── file.go
│ ├── function.go
│ ├── interface.go
│ ├── package.go
│ ├── parser.go
│ ├── parser_test.go
│ ├── struct.go
│ ├── testdata
│ │ ├── alias-lookup.txt
│ │ ├── interface-lookup.txt
│ │ └── struct-lookup.txt
│ ├── type.go
│ └── typespec.go
├── pluginmod
│ ├── pluginmod.go
│ └── pluginmod_test.go
├── remotefs
│ ├── client.go
│ ├── command.go
│ ├── remotefs_test.go
│ ├── server.go
│ └── service.go
├── router
│ ├── lex
│ │ ├── lex.go
│ │ ├── lex_test.go
│ │ ├── tokens.go
│ │ └── tokens_test.go
│ ├── parse.go
│ ├── radix
│ │ ├── tree.go
│ │ └── tree_test.go
│ ├── router.go
│ └── router_test.go
├── scaffold
│ └── template.go
├── socket
│ ├── socket.go
│ └── socket_test.go
├── svelte
│ ├── compiler.go
│ ├── compiler.js
│ ├── compiler.ts
│ ├── compiler_test.go
│ └── shimssr.ts
├── testdir
│ ├── testdir.go
│ └── testdir_test.go
├── testgen
│ ├── main.gotext
│ ├── testgen.go
│ └── testgen_test.go
├── transpiler
│ ├── transpiler.go
│ └── transpiler_test.go
├── valid
│ ├── valid.go
│ └── valid_test.go
├── vfs
│ ├── exist.go
│ ├── exist_test.go
│ ├── map.go
│ ├── map_test.go
│ ├── memory.go
│ ├── memory_test.go
│ ├── singleflight.go
│ └── vfs.go
├── viewer
│ ├── find.go
│ ├── find_test.go
│ ├── gohtml
│ │ ├── gohtml.go
│ │ └── gohtml_test.go
│ ├── svelte
│ │ ├── compiler.go
│ │ ├── compiler.js
│ │ ├── compiler.ts
│ │ ├── compiler_shim.ts
│ │ ├── compiler_test.go
│ │ ├── dom_entry.gotext
│ │ ├── dom_runtime.ts
│ │ ├── ssr_entry.gotext
│ │ ├── ssr_runtime.ts
│ │ ├── static.go
│ │ ├── static_test.go
│ │ ├── svelte.go
│ │ └── svelte_test.go
│ └── viewer.go
├── virtual
│ ├── copy.go
│ ├── copy_test.go
│ ├── dir.go
│ ├── direntry.go
│ ├── exclude.go
│ ├── exclude_test.go
│ ├── file.go
│ ├── fileinfo.go
│ ├── from.go
│ ├── json.go
│ ├── json_test.go
│ ├── list.go
│ ├── list_test.go
│ ├── map.go
│ ├── map_test.go
│ ├── os.go
│ ├── os_test.go
│ ├── print.go
│ ├── print_test.go
│ ├── sync.go
│ ├── sync_test.go
│ ├── tree.go
│ ├── tree_test.go
│ └── virtual.go
└── watcher
│ ├── watcher.go
│ └── watcher_test.go
├── runtime
└── transpiler
│ ├── transpiler.go
│ └── transpiler_test.go
├── scripts
├── _test-v8-pipe
│ ├── child
│ │ └── main.go
│ ├── grandchild
│ │ └── main.go
│ └── main.go
├── generate-changelog
│ ├── changelog.gotext
│ └── main.go
├── generate-checksums
│ └── main.go
├── set-package-json
│ └── main.go
├── svelte
│ ├── error.svelte
│ ├── index.svelte
│ ├── layout.svelte
│ ├── main.go
│ └── show.svelte
├── test-cache-diff
│ └── main.go
├── test-socket-passthrough
│ ├── child
│ │ └── main.go
│ └── main.go
├── test-subprocess-interrupt
│ ├── child
│ │ └── main.go
│ └── main.go
└── test-watcher
│ └── main.go
├── tools.go
└── version.txt
/.envrc:
--------------------------------------------------------------------------------
1 | if has asdf && has use_asdf; then
2 | use asdf
3 | fi
4 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [matthewmueller]
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Don't commit mac files
2 | .DS_Store
3 |
4 | # Don't commit node_modules
5 | node_modules
6 |
7 | # Sometimes tests write to _tmp/ don't commit these files
8 | _tmp
9 |
10 | # Don't commit the build file
11 | main
12 |
13 | # Don't commit generated files
14 | example/*/bud
15 |
16 | # The scratch example is always built from scratch
17 | example/scratch
18 |
19 | # Avoid accidental builds getting committed
20 | /bud
21 |
22 | # Release is where we build binaries for release
23 | /release
24 |
25 | # Ignore the VSCode directory
26 | .vscode/
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | # This file is used by the `asdf` CLI (https://asdf-vm.com) to easily switch
2 | # between binary versions. You can think of it like `nvm` or `gvm` but across
3 | # languages.
4 | #
5 | # We'll use `asdf` during development to easily test between
6 | # different versions.
7 | #
8 | # We recommend to use `direnv` (https://direnv.net/) alongside with `asdf`
9 | # to load/unload project-specific environments when changing directories.
10 | #
11 | # `asdf-direnv`(https://github.com/asdf-community/asdf-direnv) lets play
12 | # them together nicely. Please refer to
13 | # https://github.com/asdf-community/asdf-direnv#setup for instructions on how
14 | # to set up the integration of both tools to make them work together seamlessly.
15 | #
16 | #
17 | # The versions below are not the only valid versions that Bud supports. To see
18 | # a more complete list, review the GitHub workflow to see the versions tested
19 | # in CI.
20 |
21 | golang 1.20.2
22 | nodejs 16.4.2
23 |
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Matt Mueller
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/contributing/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG NODE_VERSION=16.15.1
2 | FROM node:${NODE_VERSION}-slim
3 |
4 | ARG GO_VERSION=1.18.3
5 | ARG BUD_VERSION=main
6 |
7 | RUN node -v
8 |
9 | # Install basic dependencies
10 | RUN apt-get -qq update \
11 | && apt-get -qq -y install curl git make gcc g++ \
12 | && rm -rf /var/lib/apt/lists/*
13 |
14 | # Install Go
15 | RUN curl -L --output - https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz | tar -xz -C /usr/local --strip-components 1
16 | RUN go version
17 | ENV PATH "/root/go/bin:${PATH}"
18 |
19 | # Install Bud
20 | RUN git clone https://github.com/livebud/bud /bud
21 | WORKDIR /bud
22 | RUN git checkout $BUD_VERSION
23 | RUN make install
24 | RUN go install .
25 | RUN bud version
26 |
--------------------------------------------------------------------------------
/example/basic/.gitignore:
--------------------------------------------------------------------------------
1 | bud/
2 | node_modules/
--------------------------------------------------------------------------------
/example/basic/Readme.md:
--------------------------------------------------------------------------------
1 | # Basic
2 |
3 | Basic example with a view and controller.
4 |
5 | ## Running locally
6 |
7 | 1. Run `npm install` to install the required `node_modules`.
8 | 2. From this directory, run `npm link livebud ../../livebud` to link `node_modules/livebud` to the runtime.
9 |
--------------------------------------------------------------------------------
/example/basic/controller/controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | context "context"
5 | )
6 |
7 | // Controller for posts
8 | type Controller struct {
9 | }
10 |
11 | // Post struct
12 | type Post struct {
13 | ID int `json:"id"`
14 | }
15 |
16 | // Index of posts
17 | // GET
18 | func (c *Controller) Index(ctx context.Context) (posts []*Post, err error) {
19 | return []*Post{}, nil
20 | }
21 |
22 | // New returns a view for creating a new post
23 | // GET new
24 | func (c *Controller) New(ctx context.Context) {
25 | }
26 |
27 | // Create post
28 | // POST
29 | func (c *Controller) Create(ctx context.Context) (post *Post, err error) {
30 | return &Post{
31 | ID: 0,
32 | }, nil
33 | }
34 |
35 | // Show post
36 | // GET :id
37 | func (c *Controller) Show(ctx context.Context, id int) (post *Post, err error) {
38 | return &Post{
39 | ID: id,
40 | }, nil
41 | }
42 |
43 | // Edit returns a view for editing a post
44 | // GET :id/edit
45 | func (c *Controller) Edit(ctx context.Context, id int) (post *Post, err error) {
46 | return &Post{
47 | ID: id,
48 | }, nil
49 | }
50 |
51 | // Update post
52 | // PATCH :id
53 | func (c *Controller) Update(ctx context.Context, id int) (post *Post, err error) {
54 | return &Post{
55 | ID: id,
56 | }, nil
57 | }
58 |
59 | // Delete post
60 | // DELETE :id
61 | func (c *Controller) Delete(ctx context.Context, id int) error {
62 | return nil
63 | }
64 |
--------------------------------------------------------------------------------
/example/basic/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-duo/bud/example/basic
2 |
3 | go 1.18
4 |
5 | replace github.com/livebud/bud => ../..
6 |
7 | require github.com/livebud/bud v0.2.1
8 |
9 | require (
10 | github.com/ajg/form v1.5.2-0.20200323032839-9aeb3cf462e1 // indirect
11 | github.com/armon/go-radix v1.0.0 // indirect
12 | github.com/cespare/xxhash v1.1.0 // indirect
13 | github.com/evanw/esbuild v0.14.11 // indirect
14 | github.com/fatih/structtag v1.2.0 // indirect
15 | github.com/fsnotify/fsnotify v1.5.1 // indirect
16 | github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 // indirect
17 | github.com/matthewmueller/gotext v0.0.0-20210424201144-265ed61725ac // indirect
18 | github.com/matthewmueller/text v0.0.0-20210424201111-ec1e4af8dfe8 // indirect
19 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
20 | github.com/otiai10/copy v1.7.0 // indirect
21 | github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 // indirect
22 | github.com/xlab/treeprint v1.1.0 // indirect
23 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
24 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
25 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
26 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
27 | )
28 |
--------------------------------------------------------------------------------
/example/basic/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "basic",
9 | "version": "1.0.0",
10 | "license": "ISC"
11 | },
12 | "../../livebud": {
13 | "version": "0.0.1",
14 | "extraneous": true,
15 | "license": "MIT",
16 | "dependencies": {
17 | "internal": "2.0.27",
18 | "react": "17.0.2",
19 | "react-dom": "17.0.2",
20 | "svelte": "3.46.4"
21 | },
22 | "devDependencies": {
23 | "@types/mocha": "8.2.2",
24 | "@types/node": "15.12.4",
25 | "@types/react": "17.0.11",
26 | "@types/react-dom": "17.0.8",
27 | "microbundle": "0.14.1",
28 | "typescript": "4.3.4"
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "basic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC"
12 | }
13 |
--------------------------------------------------------------------------------
/example/basic/view/edit.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
Edit Post
6 |
7 |
12 |
13 |
14 |
15 | Back
16 | |
17 | Show Post
18 |
--------------------------------------------------------------------------------
/example/basic/view/index.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | Post Index
6 |
7 |
8 | {#if posts.length > 0}
9 |
10 | {#each Object.keys(posts[0]) as key}
11 | {key} |
12 | {/each}
13 |
14 | {/if}
15 | {#each posts as post}
16 |
17 | {#each Object.keys(post) as key}
18 | {#if key.toLowerCase() === "id"}
19 | {post[key]} |
20 | {:else}
21 | {post[key]} |
22 | {/if}
23 | {/each}
24 |
25 | {/each}
26 |
27 |
28 |
29 |
30 | New Post
31 |
32 |
37 |
--------------------------------------------------------------------------------
/example/basic/view/new.svelte:
--------------------------------------------------------------------------------
1 | New Post
2 |
3 |
7 |
8 |
9 |
10 | Back
11 |
--------------------------------------------------------------------------------
/example/basic/view/show.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | Show Post
6 |
7 |
8 |
9 | {#each Object.keys(post) as key}
10 | {key} |
11 | {/each}
12 |
13 |
14 | {#each Object.keys(post) as key}
15 | {post[key]} |
16 | {/each}
17 |
18 |
19 |
20 |
21 |
22 | Back
23 | |
24 | Edit
25 |
26 |
31 |
--------------------------------------------------------------------------------
/example/hn/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bud
--------------------------------------------------------------------------------
/example/hn/Readme.md:
--------------------------------------------------------------------------------
1 | # HN
2 |
3 | Hacker News example.
4 |
5 | ## Running locally
6 |
7 | 1. Run `npm install` to install the required `node_modules`.
8 | 2. From this directory, run `npm link livebud ../../livebud` to link `node_modules/livebud` to the runtime.
9 |
--------------------------------------------------------------------------------
/example/hn/controller/controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/matthewmueller/hackernews"
7 | )
8 |
9 | func New(hn *hackernews.Client) *Controller {
10 | return &Controller{hn}
11 | }
12 |
13 | type Controller struct {
14 | hn *hackernews.Client
15 | }
16 |
17 | func (c *Controller) Index(ctx context.Context) (stories []*hackernews.Story, err error) {
18 | return c.hn.FrontPage(ctx)
19 | }
20 |
21 | // Show a comment
22 | func (c *Controller) Show(ctx context.Context, id int) (story *hackernews.Story, err error) {
23 | return c.hn.Find(ctx, id)
24 | }
25 |
--------------------------------------------------------------------------------
/example/hn/controller/sessions/sessions.go:
--------------------------------------------------------------------------------
1 | package sessions
2 |
3 | import "fmt"
4 |
5 | type Controller struct {
6 | }
7 |
8 | func (c *Controller) New() {
9 | }
10 |
11 | func (c *Controller) Create(email, password string) error {
12 | return fmt.Errorf("not done yet")
13 | }
14 |
--------------------------------------------------------------------------------
/example/hn/controller/users/users.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | type Controller struct {
4 | }
5 |
6 | func (c *Controller) Index() (string, error) {
7 | return "hello user", nil
8 | }
9 |
--------------------------------------------------------------------------------
/example/hn/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/livebud/bud/example/hn
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/livebud/bud v0.1.11
7 | github.com/matthewmueller/hackernews v0.3.0
8 | )
9 |
10 | require (
11 | github.com/RyanCarrier/dijkstra v1.1.0 // indirect
12 | github.com/ajg/form v1.5.2-0.20200323032839-9aeb3cf462e1 // indirect
13 | github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
14 | github.com/cespare/xxhash v1.1.0 // indirect
15 | github.com/evanw/esbuild v0.14.11 // indirect
16 | github.com/fatih/structtag v1.2.0 // indirect
17 | github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 // indirect
18 | github.com/go-logfmt/logfmt v0.5.1 // indirect
19 | github.com/gobwas/glob v0.2.3 // indirect
20 | github.com/keegancsmith/rpc v1.3.0 // indirect
21 | github.com/livebud/transpiler v0.0.3 // indirect
22 | github.com/matthewmueller/gotext v0.0.0-20210424201144-265ed61725ac // indirect
23 | github.com/matthewmueller/text v0.0.0-20210424201111-ec1e4af8dfe8 // indirect
24 | github.com/mattn/go-sqlite3 v1.14.16 // indirect
25 | github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 // indirect
26 | github.com/xlab/treeprint v1.1.0 // indirect
27 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
29 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
30 | )
31 |
32 | replace github.com/livebud/bud => ../..
33 |
--------------------------------------------------------------------------------
/example/hn/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hn",
3 | "version": "0.0.0",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "hn",
9 | "version": "0.0.0",
10 | "dependencies": {
11 | "svelte": "3.44.1",
12 | "timeago.js": "4.0.2"
13 | }
14 | },
15 | "node_modules/svelte": {
16 | "version": "3.44.1",
17 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.1.tgz",
18 | "integrity": "sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==",
19 | "engines": {
20 | "node": ">= 8"
21 | }
22 | },
23 | "node_modules/timeago.js": {
24 | "version": "4.0.2",
25 | "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
26 | "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
27 | }
28 | },
29 | "dependencies": {
30 | "svelte": {
31 | "version": "3.44.1",
32 | "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.1.tgz",
33 | "integrity": "sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ=="
34 | },
35 | "timeago.js": {
36 | "version": "4.0.2",
37 | "resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
38 | "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/hn/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hn",
3 | "version": "0.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "svelte": "3.44.1",
7 | "timeago.js": "4.0.2"
8 | }
9 | }
--------------------------------------------------------------------------------
/example/hn/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livebud/bud/721420fadaf11a0539aa783e833f2e8aaf81b719/example/hn/public/favicon.ico
--------------------------------------------------------------------------------
/example/hn/view/Comment.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
27 |
28 |
52 |
--------------------------------------------------------------------------------
/example/hn/view/Header.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
54 |
--------------------------------------------------------------------------------
/example/hn/view/Story.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
32 |
33 |
53 |
--------------------------------------------------------------------------------
/example/hn/view/index.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 | {#each stories as story}
12 |
13 | {/each}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/example/hn/view/sessions/new.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
25 |
--------------------------------------------------------------------------------
/example/hn/view/show.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 | {#each story.children as comment}
14 |
15 | {/each}
16 |
17 |
--------------------------------------------------------------------------------
/framework/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | _ "embed"
5 |
6 | "github.com/livebud/bud/framework"
7 | "github.com/livebud/bud/package/di"
8 | "github.com/livebud/bud/package/genfs"
9 |
10 | "github.com/livebud/bud/package/gomod"
11 | "github.com/livebud/bud/package/gotemplate"
12 | )
13 |
14 | //go:embed app.gotext
15 | var template string
16 |
17 | var generator = gotemplate.MustParse("framework/app/app.gotext", template)
18 |
19 | func Generate(state *State) ([]byte, error) {
20 | return generator.Generate(state)
21 | }
22 |
23 | func New(injector *di.Injector, module *gomod.Module, flag *framework.Flag) *Generator {
24 | return &Generator{flag, injector, module}
25 | }
26 |
27 | type Generator struct {
28 | flag *framework.Flag
29 | injector *di.Injector
30 | module *gomod.Module
31 | }
32 |
33 | func (g *Generator) GenerateFile(fsys genfs.FS, file *genfs.File) error {
34 | state, err := Load(fsys, g.injector, g.module, g.flag)
35 | if err != nil {
36 | return err
37 | }
38 | code, err := Generate(state)
39 | if err != nil {
40 | return err
41 | }
42 | file.Data = code
43 | return nil
44 | }
45 |
--------------------------------------------------------------------------------
/framework/app/app_test.go:
--------------------------------------------------------------------------------
1 | package app_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/testcli"
9 | "github.com/livebud/bud/package/testdir"
10 | )
11 |
12 | func TestWelcome(t *testing.T) {
13 | is := is.New(t)
14 | ctx, cancel := context.WithCancel(context.Background())
15 | defer cancel()
16 | td, err := testdir.Load()
17 | is.NoErr(err)
18 | is.NoErr(td.Write(ctx))
19 | cli := testcli.New(td.Directory())
20 | is.NoErr(td.NotExists("bud/app"))
21 | app, err := cli.Start(ctx, "run")
22 | is.NoErr(err)
23 | // Test the index page
24 | res, err := app.Get("/")
25 | is.NoErr(err)
26 | is.Equal(res.Status(), 200)
27 | is.In(res.Body().String(), "Hey Bud")
28 | is.In(res.Body().String(), "Hey Bud") // should work multiple times
29 | // Test the client-side JS
30 | res, err = app.Get("/bud/view/_index.svelte.js")
31 | is.NoErr(err)
32 | is.Equal(res.Status(), 200)
33 | is.In(res.Body().String(), "Hey Bud")
34 | is.Equal(app.Stdout(), "")
35 | // is.Equal(app.Stderr(), "")
36 | is.NoErr(td.Exists("bud/app"))
37 | is.NoErr(app.Close())
38 | }
39 |
--------------------------------------------------------------------------------
/framework/app/state.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/livebud/bud/framework"
5 | "github.com/livebud/bud/package/di"
6 | "github.com/livebud/bud/package/imports"
7 | )
8 |
9 | type State struct {
10 | Imports []*imports.Import
11 | Provider *di.Provider
12 | Flag *framework.Flag
13 | }
14 |
--------------------------------------------------------------------------------
/framework/controller/controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 |
5 | // Embed templates
6 |
7 | _ "embed"
8 | "fmt"
9 |
10 | "github.com/livebud/bud/package/di"
11 | "github.com/livebud/bud/package/genfs"
12 | "github.com/livebud/bud/package/gomod"
13 | "github.com/livebud/bud/package/gotemplate"
14 | "github.com/livebud/bud/package/parser"
15 | )
16 |
17 | //go:embed controller.gotext
18 | var template string
19 |
20 | var generator = gotemplate.MustParse("framework/controller/controller.gotext", template)
21 |
22 | // Generate the controller template from state
23 | func Generate(state *State) ([]byte, error) {
24 | return generator.Generate(state)
25 | }
26 |
27 | // New controller generator
28 | func New(injector *di.Injector, module *gomod.Module, parser *parser.Parser) *Generator {
29 | return &Generator{injector, module, parser}
30 | }
31 |
32 | // Generator for controllers
33 | type Generator struct {
34 | injector *di.Injector
35 | module *gomod.Module
36 | parser *parser.Parser
37 | }
38 |
39 | func (g *Generator) GenerateFile(fsys genfs.FS, file *genfs.File) error {
40 | state, err := Load(fsys, g.injector, g.module, g.parser)
41 | if err != nil {
42 | return fmt.Errorf("controller: unable to load. %w", err)
43 | }
44 | code, err := Generate(state)
45 | if err != nil {
46 | return err
47 | }
48 | file.Data = code
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/framework/controller/controllerrt/request/request.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/timewasted/go-accept-headers"
7 | )
8 |
9 | // New request context
10 | func New(r *http.Request) *Context {
11 | return &Context{r}
12 | }
13 |
14 | // Context struct
15 | type Context struct {
16 | r *http.Request
17 | }
18 |
19 | // Unmarshal the request body or parameters
20 | func (c *Context) Unmarshal(r *http.Request, in interface{}) error {
21 | return Unmarshal(r, in)
22 | }
23 |
24 | // Accepts a type
25 | func Accepts(r *http.Request) Acceptable {
26 | return Acceptable(accept.Parse(r.Header.Get("Accept")))
27 | }
28 |
29 | // Acceptable types
30 | type Acceptable accept.AcceptSlice
31 |
32 | // Accepts checks if the content type is acceptable
33 | func (as Acceptable) Accepts(ctype string) bool {
34 | return accept.AcceptSlice(as).Accepts(ctype)
35 | }
36 |
--------------------------------------------------------------------------------
/framework/controller/controllerrt/request/unmarshal.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "mime"
7 | "net/http"
8 | "net/url"
9 |
10 | "github.com/ajg/form"
11 | )
12 |
13 | // Unmarshal the request data into v
14 | func Unmarshal(r *http.Request, v interface{}) error {
15 | err := unmarshalBody(r, v)
16 | if err != nil {
17 | return err
18 | }
19 | err = unmarshalURL(r.URL, v)
20 | if err != nil {
21 | return err
22 | }
23 | return nil
24 | }
25 |
26 | func unmarshalBody(r *http.Request, v interface{}) error {
27 | contentType := r.Header.Get("Content-Type")
28 | if contentType == "" {
29 | return nil
30 | }
31 | mediaType, _, err := mime.ParseMediaType(contentType)
32 | if err != nil {
33 | return err
34 | }
35 | switch mediaType {
36 | case "application/json":
37 | return unmarshalJSON(r.Body, v)
38 | case "application/x-www-form-urlencoded":
39 | return unmarshalForm(r, v)
40 | }
41 | return nil
42 | }
43 |
44 | func unmarshalURL(u *url.URL, v interface{}) error {
45 | dec := form.NewDecoder(nil)
46 | dec.IgnoreCase(true)
47 | dec.IgnoreUnknownKeys(true)
48 | return dec.DecodeValues(v, u.Query())
49 | }
50 |
51 | func unmarshalForm(r *http.Request, v interface{}) error {
52 | if r.PostForm == nil {
53 | r.ParseForm()
54 | }
55 | dec := form.NewDecoder(nil)
56 | dec.IgnoreCase(true)
57 | dec.IgnoreUnknownKeys(true)
58 | return dec.DecodeValues(v, r.PostForm)
59 | }
60 |
61 | func unmarshalJSON(r io.Reader, v interface{}) error {
62 | data, err := io.ReadAll(r)
63 | if err != nil {
64 | return err
65 | }
66 | if len(data) == 0 {
67 | return nil
68 | }
69 | return json.Unmarshal(data, v)
70 | }
71 |
--------------------------------------------------------------------------------
/framework/framework.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | // Flag is used by many of the framework generators
8 | type Flag struct {
9 | Embed bool
10 | Minify bool
11 | Hot bool
12 | }
13 |
14 | func (f *Flag) Flags() []string {
15 | return []string{
16 | "--embed=" + strconv.FormatBool(f.Embed),
17 | "--minify=" + strconv.FormatBool(f.Minify),
18 | "--hot=" + strconv.FormatBool(f.Hot),
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/framework/framework_test.go:
--------------------------------------------------------------------------------
1 | package framework_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/framework"
7 | "github.com/livebud/bud/internal/is"
8 | )
9 |
10 | func TestString(t *testing.T) {
11 | is := is.New(t)
12 | f := framework.Flag{
13 | Embed: true,
14 | Minify: true,
15 | Hot: false,
16 | }
17 | flags := f.Flags()
18 | is.Equal(flags[0], "--embed=true")
19 | is.Equal(flags[1], "--minify=true")
20 | is.Equal(flags[2], "--hot=false")
21 | }
22 |
--------------------------------------------------------------------------------
/framework/generator/generator.gotext:
--------------------------------------------------------------------------------
1 | package generator
2 |
3 | // GENERATED BY BUD. DO NOT EDIT.
4 |
5 | {{- if $.Imports }}
6 |
7 | import (
8 | {{- range $import := $.Imports }}
9 | {{$import.Name}} "{{$import.Path}}"
10 | {{- end }}
11 | )
12 | {{- end }}
13 |
14 | func New(
15 | fsys genfs.FileSystem,
16 | {{- range $generator := $.FileGenerators }}
17 | {{ $generator.Camel }} *{{ $generator.Import.Name }}.Generator,
18 | {{- end }}
19 | {{- range $generator := $.FileServers }}
20 | {{ $generator.Camel }} *{{ $generator.Import.Name }}.Generator,
21 | {{- end }}
22 | {{- range $generator := $.GenerateDirs }}
23 | {{ $generator.Camel }} *{{ $generator.Import.Name }}.Generator,
24 | {{- end }}
25 | {{- range $generator := $.ServeFiles }}
26 | {{ $generator.Camel }} *{{ $generator.Import.Name }}.Generator,
27 | {{- end }}
28 | ) FS {
29 | {{- range $generator := $.FileGenerators }}
30 | fsys.FileGenerator(`{{ $generator.Path }}`, {{ $generator.Camel }})
31 | {{- end }}
32 | {{- range $generator := $.FileServers }}
33 | fsys.FileServer(`{{ $generator.Path }}`, {{ $generator.Camel }})
34 | {{- end }}
35 | {{- range $generator := $.GenerateDirs }}
36 | fsys.GenerateDir(`{{ $generator.Path }}`, {{ $generator.Camel }}.{{ $generator.Method }})
37 | {{- end }}
38 | {{- range $generator := $.ServeFiles }}
39 | fsys.ServeFile(`{{ $generator.Path }}`, {{ $generator.Camel }}.Serve)
40 | {{- end }}
41 | return fsys
42 | }
43 |
44 | type FS = fs.FS
45 |
--------------------------------------------------------------------------------
/framework/generator/state.go:
--------------------------------------------------------------------------------
1 | package generator
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/livebud/bud/package/imports"
8 | )
9 |
10 | type State struct {
11 | Imports []*imports.Import
12 | FileGenerators []*CodeGenerator
13 | FileServers []*CodeGenerator
14 | GenerateDirs []*CodeGenerator
15 | ServeFiles []*CodeGenerator
16 | }
17 |
18 | type Type string
19 |
20 | type CodeGenerator struct {
21 | Import *imports.Import
22 | Path string // Path that triggers the generator (e.g. "bud/cmd/app/main.go")
23 | Camel string
24 | }
25 |
26 | func (c *CodeGenerator) Method() (string, error) {
27 | switch {
28 | case strings.HasPrefix(c.Path, "bud/internal"):
29 | return "Generate", nil
30 | case strings.HasPrefix(c.Path, "bud/cmd"):
31 | return "GenerateCmd", nil
32 | case strings.HasPrefix(c.Path, "bud/pkg"):
33 | return "GeneratePkg", nil
34 | default:
35 | return "", fmt.Errorf("generator: unexpected generator path %q", c.Path)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/framework/public/public.go:
--------------------------------------------------------------------------------
1 | package public
2 |
3 | import (
4 | _ "embed"
5 |
6 | "github.com/livebud/bud/framework"
7 | "github.com/livebud/bud/package/genfs"
8 | "github.com/livebud/bud/package/gomod"
9 |
10 | "github.com/livebud/bud/package/gotemplate"
11 | )
12 |
13 | //go:embed public.gotext
14 | var template string
15 |
16 | var generator = gotemplate.MustParse("framework/public/public.gotext", template)
17 |
18 | // Generate the public file
19 | func Generate(state *State) ([]byte, error) {
20 | return generator.Generate(state)
21 | }
22 |
23 | // New public generator
24 | func New(flag *framework.Flag, module *gomod.Module) *Generator {
25 | return &Generator{
26 | flag: flag,
27 | module: module,
28 | }
29 | }
30 |
31 | type Generator struct {
32 | flag *framework.Flag
33 | module *gomod.Module
34 | }
35 |
36 | func (g *Generator) GenerateFile(fsys genfs.FS, file *genfs.File) error {
37 | state, err := Load(fsys, g.flag)
38 | if err != nil {
39 | return err
40 | }
41 | code, err := Generate(state)
42 | if err != nil {
43 | return err
44 | }
45 | file.Data = code
46 | return nil
47 | }
48 |
--------------------------------------------------------------------------------
/framework/public/public.gotext:
--------------------------------------------------------------------------------
1 | package public
2 |
3 | // GENERATED. DO NOT EDIT.
4 |
5 | {{- if $.Imports }}
6 |
7 | import (
8 | {{- range $import := $.Imports }}
9 | {{$import.Name}} "{{$import.Path}}"
10 | {{- end }}
11 | )
12 | {{- end }}
13 |
14 | func Load(handler publicrt.Handler) *Handler {
15 | return &Handler{handler}
16 | }
17 |
18 | type Handler struct {
19 | handler http.Handler
20 | }
21 |
22 | func (h *Handler) Register(r *router.Router) {
23 | {{- range $file := $.Files }}
24 | r.Get(`{{ $file.Route }}`, h.handler)
25 | {{- end }}
26 | }
27 |
28 | func LoadFS() FS {
29 | return virtual.List{
30 | {{- range $file := $.Files }}
31 | {{- if $file.Data }}
32 | &virtual.File{
33 | Path: "{{ $file.Path }}",
34 | {{/* Using double quotes matters because $file.Data is escaped hex */}}
35 | Data: []byte("{{ $file.Data }}"),
36 | },
37 | {{ end }}
38 | {{- end }}
39 | }
40 | }
41 |
42 | type FS = fs.FS
43 |
--------------------------------------------------------------------------------
/framework/public/publicrt/publicrt.go:
--------------------------------------------------------------------------------
1 | package publicrt
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/fs"
7 | "net/http"
8 | "path"
9 | "time"
10 | )
11 |
12 | type FS = fs.FS
13 |
14 | func NewHandler(fsys FS) *Handler {
15 | return &Handler{http.FS(fsys)}
16 | }
17 |
18 | type Handler struct {
19 | fsys http.FileSystem
20 | }
21 |
22 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
23 | file, err := h.fsys.Open(path.Join("public", r.URL.Path))
24 | if err != nil {
25 | http.Error(w, err.Error(), 500)
26 | return
27 | }
28 | defer file.Close()
29 | stat, err := file.Stat()
30 | if err != nil {
31 | http.Error(w, err.Error(), 500)
32 | return
33 | }
34 | if stat.IsDir() {
35 | http.Error(w, fmt.Sprintf("%q is a directory", r.URL.Path), 500)
36 | return
37 | }
38 | serveContent(w, r, r.URL.Path, stat.ModTime(), file)
39 | }
40 |
41 | func serveContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
42 | http.ServeContent(w, req, name, modtime, content)
43 | }
44 |
--------------------------------------------------------------------------------
/framework/public/state.go:
--------------------------------------------------------------------------------
1 | package public
2 |
3 | import (
4 | "github.com/livebud/bud/internal/embed"
5 | "github.com/livebud/bud/package/imports"
6 | )
7 |
8 | type State struct {
9 | Imports []*imports.Import
10 | Files []*File
11 | }
12 |
13 | type File struct {
14 | Path string
15 | Route string
16 | Data embed.Data
17 | }
18 |
--------------------------------------------------------------------------------
/framework/transform/transformrt/dom.go:
--------------------------------------------------------------------------------
1 | package transformrt
2 |
3 | import esbuild "github.com/evanw/esbuild/pkg/api"
4 |
5 | type DOM struct {
6 | Map *Map
7 | }
8 |
9 | var _ Transformer = (*DOM)(nil)
10 |
11 | func (d *DOM) Transform(fromPath, toPath string, code []byte) ([]byte, error) {
12 | return d.Map.DOM.Transform(fromPath, toPath, code)
13 | }
14 |
15 | func (d *DOM) Plugins() []esbuild.Plugin {
16 | return d.Map.DOM.Plugins()
17 | }
18 |
--------------------------------------------------------------------------------
/framework/transform/transformrt/ssr.go:
--------------------------------------------------------------------------------
1 | package transformrt
2 |
3 | import esbuild "github.com/evanw/esbuild/pkg/api"
4 |
5 | type SSR struct {
6 | Map *Map
7 | }
8 |
9 | var _ Transformer = (*SSR)(nil)
10 |
11 | func (d *SSR) Transform(fromPath, toPath string, code []byte) ([]byte, error) {
12 | return d.Map.SSR.Transform(fromPath, toPath, code)
13 | }
14 |
15 | func (d *SSR) Plugins() []esbuild.Plugin {
16 | return d.Map.SSR.Plugins()
17 | }
18 |
--------------------------------------------------------------------------------
/framework/transpiler/transpiler.gotext:
--------------------------------------------------------------------------------
1 | package transpiler
2 |
3 | {{- if $.Imports }}
4 |
5 | import (
6 | {{- range $import := $.Imports }}
7 | {{$import.Name}} "{{$import.Path}}"
8 | {{- end }}
9 | )
10 | {{- end }}
11 |
12 | // Load the transpiler
13 | func Load(
14 | {{- range $transpiler := $.Transpilers }}
15 | {{ $transpiler.Camel }} *{{ $transpiler.Import.Name }}.Transpiler,
16 | {{- end }}
17 | ) *Generator {
18 | tr := transpiler.New()
19 | {{- range $transpiler := $.Transpilers }}
20 | {{- range $method := $transpiler.Methods }}
21 | tr.Add(`{{ $method.From }}`, `{{ $method.To }}`, {{ $transpiler.Camel }}.{{ $method.Pascal }})
22 | {{- end }}
23 | {{- end }}
24 | return &Generator{tr}
25 | }
26 |
27 | type Generator struct {
28 | tr transpiler.Interface
29 | }
30 |
31 | func (g *Generator) Serve(fsys genfs.FS, file *genfs.File) error {
32 | return transpiler.Serve(g.tr, fsys, file)
33 | }
34 |
--------------------------------------------------------------------------------
/framework/view/dom/dom.gotext:
--------------------------------------------------------------------------------
1 | import { mount } from "livebud/runtime"
2 | import createView from "livebud/runtime/{{$.Type}}"
3 | {{- if $.Hot }}
4 | import Hot from "livebud/runtime/hot"
5 | {{- end }}
6 |
7 | import {{$.Page.Pascal}} from "./{{$.Page}}"
8 | {{- range $frame := $.Frames }}
9 | import {{ $frame.Pascal }} from "./{{$frame}}"
10 | {{- end }}
11 | {{- if $.Error }}
12 | import {{ $.Error.Pascal }} from "./{{$.Error}}"
13 | {{- end }}
14 |
15 | const components = {
16 | "/bud/{{$.Page}}": {{$.Page.Pascal}},
17 | {{- range $frame := $.Frames }}
18 | "/bud/{{$frame}}": {{ $frame.Pascal }},
19 | {{- end }}
20 | {{- if $.Error }}
21 | "/bud/{{$.Error}}": {{ $.Error.Pascal }},
22 | {{- end }}
23 | }
24 |
25 | // Mount the view
26 | export default mount({
27 | createView: createView,
28 | components: components,
29 | page: "/bud/{{$.Page}}",
30 | frames: [
31 | {{- range $frame := $.Frames }}
32 | "/bud/{{$frame}}"
33 | {{- end }}
34 | ],
35 | {{- if $.Error }}
36 | error: "/bud/{{$.Error}}",
37 | {{- end }}
38 | target: document.getElementById("bud_target"),
39 | {{- if $.Hot }}
40 | hot: new Hot("http://127.0.0.1:35729/bud/hot/{{$.Page}}", components),
41 | {{- end }}
42 | })
43 |
--------------------------------------------------------------------------------
/framework/view/ssr/jsx.gotext:
--------------------------------------------------------------------------------
1 | import { createView } from "./bud/view/_jsx.ts.js"
2 | {{- range $import := $.ServerImports }}
3 | import {{$import.Pascal}} from "./{{$import}}"
4 | {{- end }}
5 |
6 | export default createView({
7 | page: {{$.Page.Pascal}},
8 | {{- if $.Error }}
9 | error: {{$.Error.Pascal}},
10 | {{- end }}
11 | {{- if $.Layout }}
12 | layout: {{$.Layout.Pascal}},
13 | {{- end }}
14 | frames: [
15 | {{- range $frame := $.Frames }}
16 | {{ $frame.Pascal }},
17 | {{- end }}
18 | ],
19 | client: "/{{$.Client}}",
20 | })
21 |
--------------------------------------------------------------------------------
/framework/view/ssr/jsx.ts:
--------------------------------------------------------------------------------
1 | import ReactSSR from "react-dom/server"
2 | import React from "react"
3 |
4 | type View = {
5 | page: any
6 | frames: any[]
7 | layout: any
8 | error?: any
9 | client: string
10 | }
11 |
12 | export function createView(view: View) {
13 | return function ({ props, context }) {
14 | let component = React.createElement(view.page, props, [])
15 | for (let frame of view.frames) {
16 | component = React.createElement(frame, props, component)
17 | }
18 | let component2 = React.createElement("div", { id: "bud_target" }, component)
19 | const layout = view.layout || defaultLayout
20 | let component3 = React.createElement(layout, props, component2)
21 | let html = ReactSSR.renderToString(component3)
22 | let inject = ""
23 | const hydrate = JSON.stringify(props)
24 | inject += ``
25 | inject += ``
26 | html = html.replace("", inject + ``)
27 | return {
28 | status: 200,
29 | headers: {
30 | "Content-Type": "text/html",
31 | },
32 | body: html,
33 | }
34 | }
35 | }
36 |
37 | function defaultLayout(props) {
38 | return React.createElement(
39 | "html",
40 | null,
41 | React.createElement("head", null),
42 | React.createElement("body", null, props.children)
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/framework/view/ssr/ssr.gotext:
--------------------------------------------------------------------------------
1 | import { renderHTML } from "./bud/view/_ssr_runtime.ts"
2 | {{- range $view := $.Views }}
3 | import {{$view.Page.Pascal}} from "./bud/{{$view.Page}}"
4 | {{- end }}
5 |
6 | const views = {}
7 | {{- range $view := $.Views }}
8 | views["{{$view.Route}}"] = {{ $view.Page.Pascal }}
9 | {{- end }}
10 |
11 | // Render the view
12 | export function render(route, props, context) {
13 | const view = views[route]
14 | if (!view) {
15 | return JSON.stringify({
16 | status: 404
17 | })
18 | }
19 | return JSON.stringify(renderHTML({
20 | context: context,
21 | props: props,
22 | route: route,
23 | view: view,
24 | }))
25 | }
26 |
--------------------------------------------------------------------------------
/framework/view/ssr/ssr.ts:
--------------------------------------------------------------------------------
1 | type Input = {
2 | route: string
3 | view: any // TODO: type this
4 | props: Record
5 | context: Record
6 | }
7 |
8 | type Response = {
9 | status: number
10 | headers: Record
11 | body: string
12 | }
13 |
14 | export function renderHTML(input: Input): Response {
15 | // Handle the missing view
16 | if (!input.view) {
17 | return {
18 | status: 404,
19 | headers: {},
20 | body: fallback(new Error('Missing page "' + input.route + '"')),
21 | }
22 | }
23 | return input.view({ props: input.props, context: input.context })
24 | }
25 |
26 | function fallback(err: Error) {
27 | return `fallback error: ${err.message}`
28 | }
29 |
--------------------------------------------------------------------------------
/framework/view/ssr/svelte.gotext:
--------------------------------------------------------------------------------
1 | import { createView } from "./bud/view/_svelte.js"
2 | {{- range $import := $.ServerImports }}
3 | import {{$import.Pascal}} from "./{{$import}}"
4 | {{- end }}
5 |
6 | export default createView({
7 | page: {{$.Page.Pascal}},
8 | {{- if $.Error }}
9 | error: {{$.Error.Pascal}},
10 | {{- end }}
11 | {{- if $.Layout }}
12 | layout: {{$.Layout.Pascal}},
13 | {{- end }}
14 | frames: [
15 | {{- range $frame := $.Frames }}
16 | {{ $frame.Pascal }},
17 | {{- end }}
18 | ],
19 | client: "/{{$.Client}}",
20 | })
21 |
--------------------------------------------------------------------------------
/framework/view/state.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | "github.com/livebud/bud/internal/embed"
5 | "github.com/livebud/bud/package/imports"
6 | )
7 |
8 | type State struct {
9 | Imports []*imports.Import
10 | Routes []string
11 | Embeds []*embed.File
12 | }
13 |
--------------------------------------------------------------------------------
/framework/view/view.go:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | import (
4 | _ "embed"
5 |
6 | "github.com/livebud/bud/framework"
7 | "github.com/livebud/bud/framework/transform/transformrt"
8 | "github.com/livebud/bud/package/genfs"
9 | "github.com/livebud/bud/package/gomod"
10 | "github.com/livebud/bud/package/gotemplate"
11 | )
12 |
13 | //go:embed view.gotext
14 | var template string
15 |
16 | var generator = gotemplate.MustParse("framework/view/view.gotext", template)
17 |
18 | // Generate the view from state
19 | func Generate(state *State) ([]byte, error) {
20 | return generator.Generate(state)
21 | }
22 |
23 | func New(module *gomod.Module, transform *transformrt.Map, flag *framework.Flag) *Generator {
24 | return &Generator{
25 | flag: flag,
26 | module: module,
27 | transform: transform,
28 | }
29 | }
30 |
31 | type Generator struct {
32 | flag *framework.Flag
33 | module *gomod.Module
34 | transform *transformrt.Map
35 | }
36 |
37 | func (c *Generator) GenerateFile(fsys genfs.FS, file *genfs.File) error {
38 | state, err := Load(fsys, c.module, c.transform, c.flag)
39 | if err != nil {
40 | return err
41 | }
42 | code, err := Generate(state)
43 | if err != nil {
44 | return err
45 | }
46 | file.Data = code
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/framework/view/view.gotext:
--------------------------------------------------------------------------------
1 | package view
2 |
3 | // GENERATED. DO NOT EDIT.
4 |
5 | {{- if $.Imports }}
6 |
7 | import (
8 | {{- range $import := $.Imports }}
9 | {{$import.Name}} "{{$import.Path}}"
10 | {{- end }}
11 | )
12 | {{- end }}
13 |
14 | func NewHandler(handler *viewrt.Handler) *Handler {
15 | return &Handler{handler}
16 | }
17 |
18 | type Handler struct {
19 | handler *viewrt.Handler
20 | }
21 |
22 | func (h *Handler) Register(r *router.Router) {
23 | {{- range $route := $.Routes }}
24 | r.Get(`{{ $route }}`, h.handler)
25 | {{- end }}
26 | }
27 |
28 | func (h *Handler) Renderer(route string, props interface{}) http.Handler {
29 | return h.handler.Renderer(route, props)
30 | }
31 |
32 | type FS = fs.FS
33 |
34 | func LoadFS() FS {
35 | return virtual.List{
36 | {{- range $embed := $.Embeds }}
37 | &virtual.File{
38 | Path: "{{ $embed.Path }}",
39 | {{/* Using double quotes matters because $embed.Data is escaped hex */}}
40 | Data: []byte("{{ $embed.Data }}"),
41 | },
42 | {{- end }}
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/framework/web/state.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import "github.com/livebud/bud/package/imports"
4 |
5 | type State struct {
6 | Imports []*imports.Import
7 | Resources []*Resource
8 | }
9 |
10 | // Resource is a web package that will register its routes
11 | type Resource struct {
12 | Import *imports.Import
13 | Camel string
14 | }
15 |
--------------------------------------------------------------------------------
/framework/web/web.go:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | import (
4 | _ "embed"
5 |
6 | "github.com/livebud/bud/package/genfs"
7 | "github.com/livebud/bud/package/gomod"
8 | "github.com/livebud/bud/package/gotemplate"
9 | "github.com/livebud/bud/package/parser"
10 | )
11 |
12 | //go:embed web.gotext
13 | var template string
14 |
15 | var generator = gotemplate.MustParse("framework/web/web.gotext", template)
16 |
17 | // Generate the web server from state
18 | func Generate(state *State) ([]byte, error) {
19 | return generator.Generate(state)
20 | }
21 |
22 | func New(module *gomod.Module, parser *parser.Parser) *Generator {
23 | return &Generator{module, parser}
24 | }
25 |
26 | type Generator struct {
27 | module *gomod.Module
28 | parser *parser.Parser
29 | }
30 |
31 | func (g *Generator) GenerateFile(fsys genfs.FS, file *genfs.File) error {
32 | state, err := Load(fsys, g.module, g.parser)
33 | if err != nil {
34 | return err
35 | }
36 | code, err := generator.Generate(state)
37 | if err != nil {
38 | return err
39 | }
40 | file.Data = code
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/framework/web/web.gotext:
--------------------------------------------------------------------------------
1 | package web
2 |
3 | // GENERATED. DO NOT EDIT.
4 |
5 | {{- if $.Imports }}
6 |
7 | import (
8 | {{- range $import := $.Imports }}
9 | {{$import.Name}} "{{$import.Path}}"
10 | {{- end }}
11 | )
12 | {{- end }}
13 |
14 | // New web server
15 | func New(
16 | router *router.Router,
17 | {{- range $resource := $.Resources }}
18 | {{ $resource.Camel }} *{{ $resource.Import.Name }}.Handler,
19 | {{- end }}
20 | ) *Server {
21 | {{- if $.Resources }}
22 | // Register routes
23 | {{- range $resource := $.Resources }}
24 | {{ $resource.Camel }}.Register(router)
25 | {{- end }}
26 | {{- end }}
27 | // Compose the middleware together
28 | stack := middleware.Compose(
29 | methodoverride.New(),
30 | )
31 | // Add the router to the bottom of the middleware
32 | handler := stack(router)
33 | // Return the web server
34 | return &Server{handler}
35 | }
36 |
37 | type Server struct {
38 | http.Handler
39 | }
40 |
41 | func (s *Server) Serve(ctx context.Context, address string) error {
42 | listener, err := webrt.Listen("WEB", address)
43 | if err != nil {
44 | return err
45 | }
46 | return webrt.Serve(ctx, listener, s)
47 | }
48 |
--------------------------------------------------------------------------------
/framework/web/web_test.go:
--------------------------------------------------------------------------------
1 | package web_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/testcli"
9 | "github.com/livebud/bud/package/testdir"
10 | )
11 |
12 | func TestEmptyBuild(t *testing.T) {
13 | is := is.New(t)
14 | ctx := context.Background()
15 | td, err := testdir.Load()
16 | is.NoErr(err)
17 | is.NoErr(td.Write(ctx))
18 | cli := testcli.New(td.Directory())
19 | is.NoErr(td.NotExists("bud/internal/web"))
20 | result, err := cli.Run(ctx, "build")
21 | is.NoErr(err)
22 | is.Equal(result.Stdout(), "")
23 | is.Equal(result.Stderr(), "")
24 | // Empty builds generate the web directory
25 | is.NoErr(td.Exists("bud/internal/web"))
26 | }
27 |
--------------------------------------------------------------------------------
/framework/web/webrt/serve_test.go:
--------------------------------------------------------------------------------
1 | package webrt_test
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/livebud/bud/framework/web/webrt"
10 | "github.com/livebud/bud/internal/is"
11 | "golang.org/x/sync/errgroup"
12 | )
13 |
14 | func TestServe(t *testing.T) {
15 | is := is.New(t)
16 | ctx, cancel := context.WithCancel(context.Background())
17 | defer cancel()
18 | listener, err := webrt.Listen("APP", ":0")
19 | is.NoErr(err)
20 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21 | w.WriteHeader(205)
22 | })
23 | eg := new(errgroup.Group)
24 | eg.Go(func() error { return webrt.Serve(ctx, listener, handler) })
25 | res, err := http.Get("http://" + listener.Addr().String())
26 | is.NoErr(err)
27 | is.Equal(res.StatusCode, 205)
28 | cancel()
29 | eg.Wait()
30 | res, err = http.Get("http://" + listener.Addr().String())
31 | is.True(err != nil)
32 | is.True(res == nil)
33 | is.True(strings.Contains(err.Error(), `connection refused`)) // should have stopped
34 | }
35 |
--------------------------------------------------------------------------------
/framework/web/welcome/welcome.go:
--------------------------------------------------------------------------------
1 | package welcome
2 |
3 | import (
4 | _ "embed"
5 | "errors"
6 | "fmt"
7 | "io/fs"
8 | "net/http"
9 |
10 | "github.com/livebud/bud/package/router"
11 | "github.com/livebud/bud/package/virtual"
12 | )
13 |
14 | // Files are built in https://github.com/livebud/welcome and manually copied
15 | // over.
16 |
17 | //go:embed build/index.html
18 | var index []byte
19 |
20 | //go:embed build/bud/view/_index.svelte.js
21 | var clientJS []byte
22 |
23 | type Handler struct {
24 | }
25 |
26 | func (h *Handler) Register(r *router.Router) {
27 | handle := handler(http.FS(virtual.List{
28 | &virtual.File{
29 | Path: ".",
30 | Data: index,
31 | },
32 | &virtual.File{
33 | Path: "bud/view/_index.svelte.js",
34 | Data: clientJS,
35 | },
36 | }))
37 | r.Get("/", handle)
38 | r.Get("/bud/view/_index.svelte.js", handle)
39 | }
40 |
41 | func handler(fsys http.FileSystem) http.Handler {
42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43 | file, err := fsys.Open(r.URL.Path)
44 | if err != nil {
45 | if errors.Is(err, fs.ErrNotExist) {
46 | http.NotFound(w, r)
47 | return
48 | }
49 | http.Error(w, err.Error(), 500)
50 | return
51 | }
52 | defer file.Close()
53 | stat, err := file.Stat()
54 | if err != nil {
55 | fmt.Println(err.Error())
56 | http.Error(w, err.Error(), 500)
57 | return
58 | }
59 | if stat.IsDir() {
60 | http.Error(w, fmt.Sprintf("%q is a directory", r.URL.Path), 500)
61 | return
62 | }
63 | http.ServeContent(w, r, r.URL.Path, stat.ModTime(), file)
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/internal/ansi/color.go:
--------------------------------------------------------------------------------
1 | package ansi
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/aybabtme/rgbterm"
7 | )
8 |
9 | var noColor = os.Getenv("NO_COLOR") != ""
10 |
11 | func paint(msg string, r, g, b uint8) string {
12 | if noColor {
13 | return msg
14 | }
15 | return rgbterm.FgString(msg, r, g, b)
16 | }
17 |
18 | func Dim(msg string) string {
19 | if noColor {
20 | return msg
21 | }
22 | return "\033[37m" + msg + "\033[0m"
23 | }
24 |
25 | func Bold(msg string) string {
26 | if noColor {
27 | return msg
28 | }
29 | return "\033[1m" + msg + "\033[0m"
30 | }
31 |
32 | func White(msg string) string {
33 | return paint(msg, 226, 232, 240)
34 | }
35 |
36 | func Green(msg string) string {
37 | return paint(msg, 43, 255, 99)
38 | }
39 |
40 | func Blue(msg string) string {
41 | return paint(msg, 43, 199, 255)
42 | }
43 |
44 | func Yellow(msg string) string {
45 | return paint(msg, 255, 237, 43)
46 | }
47 |
48 | func Pink(msg string) string {
49 | return paint(msg, 192, 38, 211)
50 | }
51 |
52 | func Red(msg string) string {
53 | return paint(msg, 255, 43, 43)
54 | }
55 |
--------------------------------------------------------------------------------
/internal/bail/bail.go:
--------------------------------------------------------------------------------
1 | package bail
2 |
3 | import "fmt"
4 |
5 | type Struct struct {
6 | err error
7 | }
8 |
9 | type bail struct{}
10 |
11 | func (s *Struct) Recover(err *error) {
12 | if e := recover(); e != nil {
13 | // resume same panic if it's not bailing
14 | if _, ok := e.(bail); !ok {
15 | panic(e)
16 | }
17 | *err = s.err
18 | }
19 | }
20 |
21 | func (s *Struct) Recover2(err *error, prefix string) {
22 | if e := recover(); e != nil {
23 | // resume same panic if it's not bailing
24 | if _, ok := e.(bail); !ok {
25 | panic(e)
26 | }
27 | *err = fmt.Errorf(prefix+". %w", s.err)
28 | }
29 | }
30 |
31 | func (s *Struct) Bail(err error) {
32 | s.err = err
33 | panic(bail{})
34 | }
35 |
--------------------------------------------------------------------------------
/internal/cli/build.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/livebud/bud/framework"
7 | )
8 |
9 | type Build struct {
10 | Flag *framework.Flag
11 | }
12 |
13 | func (c *CLI) Build(ctx context.Context, in *Build) error {
14 | return c.Generate(ctx, &Generate{Flag: in.Flag})
15 | }
16 |
--------------------------------------------------------------------------------
/internal/cli/build_test.go:
--------------------------------------------------------------------------------
1 | package cli_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/testcli"
9 | "github.com/livebud/bud/package/testdir"
10 | )
11 |
12 | func TestBuildEmpty(t *testing.T) {
13 | is := is.New(t)
14 | ctx := context.Background()
15 | td, err := testdir.Load()
16 | is.NoErr(err)
17 | err = td.Write(ctx)
18 | is.NoErr(err)
19 | cli := testcli.New(td.Directory())
20 | is.NoErr(td.NotExists("bud/app"))
21 | result, err := cli.Run(ctx, "build")
22 | is.NoErr(err)
23 | is.Equal(result.Stdout(), "")
24 | is.Equal(result.Stderr(), "")
25 | is.NoErr(td.Exists("bud/app"))
26 | }
27 |
28 | func TestBuildTwice(t *testing.T) {
29 | is := is.New(t)
30 | ctx := context.Background()
31 | td, err := testdir.Load()
32 | is.NoErr(err)
33 | is.NoErr(td.Write(ctx))
34 | cli := testcli.New(td.Directory())
35 | is.NoErr(td.NotExists("bud/app"))
36 | result, err := cli.Run(ctx, "build")
37 | is.NoErr(err)
38 | is.Equal(result.Stdout(), "")
39 | is.Equal(result.Stderr(), "")
40 | is.NoErr(td.Exists("bud/app"))
41 | result, err = cli.Run(ctx, "build")
42 | is.NoErr(err)
43 | is.Equal(result.Stdout(), "")
44 | is.Equal(result.Stderr(), "")
45 | is.NoErr(td.Exists("bud/app"))
46 | }
47 |
--------------------------------------------------------------------------------
/internal/cli/create/gomod.gotext:
--------------------------------------------------------------------------------
1 | module {{ $.Name }}
2 |
3 | go {{ $.GoVersion }}
4 |
5 | {{- if $.Requires }}
6 |
7 | require (
8 | {{- range $req := $.Requires }}
9 | {{ $req.Import }} {{ $req.Version }}{{if $req.Indirect}} // indirect{{ end }}
10 | {{- end }}
11 | )
12 | {{- end }}
13 |
14 | {{- if $.Replaces }}
15 |
16 | replace (
17 | {{- range $rep := $.Replaces }}
18 | {{ $rep.From }} => {{ $rep.To }}
19 | {{- end }}
20 | )
21 | {{- end }}
--------------------------------------------------------------------------------
/internal/cli/custom.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/livebud/bud/internal/once"
7 | "github.com/livebud/bud/package/commander"
8 | )
9 |
10 | type Custom struct {
11 | Closer *once.Closer
12 | Help bool
13 | Args []string
14 | }
15 |
16 | func (c *CLI) Custom(ctx context.Context, in *Custom) error {
17 | if in.Help {
18 | return commander.Usage()
19 | }
20 | return commander.Usage()
21 | }
22 |
--------------------------------------------------------------------------------
/internal/cli/generators.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import "github.com/livebud/bud/framework/generator"
4 |
5 | // TODO: consider moving these into bud/cmd/bud/generator.go
6 | var fileGenerators = map[string]generator.Selector{
7 | "bud/cmd/app/main.go": {
8 | Import: "github.com/livebud/bud/framework/app",
9 | Type: "*Generator",
10 | },
11 | "bud/internal/web/web.go": {
12 | Import: "github.com/livebud/bud/framework/web",
13 | Type: "*Generator",
14 | },
15 | "bud/internal/web/controller/controller.go": {
16 | Import: "github.com/livebud/bud/framework/controller",
17 | Type: "*Generator",
18 | },
19 | "bud/internal/web/view/view.go": {
20 | Import: "github.com/livebud/bud/framework/view",
21 | Type: "*Generator",
22 | },
23 | "bud/internal/web/public/public.go": {
24 | Import: "github.com/livebud/bud/framework/public",
25 | Type: "*Generator",
26 | },
27 | "bud/view/_ssr.js": {
28 | Import: "github.com/livebud/bud/framework/view/ssr",
29 | Type: "*Generator",
30 | },
31 | }
32 |
33 | var fileServers = map[string]generator.Selector{
34 | "bud/view": {
35 | Import: "github.com/livebud/bud/framework/view/dom",
36 | Type: "*Generator",
37 | },
38 | "bud/node_modules": {
39 | Import: "github.com/livebud/bud/framework/view/nodemodules",
40 | Type: "*Generator",
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/internal/cli/new_controller/view_edit.gotext:
--------------------------------------------------------------------------------
1 |
4 |
5 | Edit {{ $.Title }}
6 |
7 |
12 |
13 |
14 |
15 | Back
16 | |
17 | Show {{ $.Title }}
18 |
--------------------------------------------------------------------------------
/internal/cli/new_controller/view_index.gotext:
--------------------------------------------------------------------------------
1 |
4 |
5 | {{ $.Title }} Index
6 |
7 |
8 | {#if {{ $.Plural }}.length > 0}
9 |
10 | {#each Object.keys({{ $.Plural }}[0]) as key}
11 | {key} |
12 | {/each}
13 |
14 | {/if}
15 | {#each {{ $.Plural }} as {{ $.Singular -}} }
16 |
17 | {#each Object.keys({{ $.Singular }}) as key}
18 | {#if key.toLowerCase() === "id"}
19 | { {{- $.Singular }}[key]} |
20 | {:else}
21 | { {{- $.Singular }}[key]} |
22 | {/if}
23 | {/each}
24 |
25 | {/each}
26 |
27 |
28 |
29 |
30 | New {{ $.Title }}
31 |
32 |
37 |
--------------------------------------------------------------------------------
/internal/cli/new_controller/view_new.gotext:
--------------------------------------------------------------------------------
1 | New {{ $.Title }}
2 |
3 |
7 |
8 |
9 |
10 | Back
11 |
--------------------------------------------------------------------------------
/internal/cli/new_controller/view_show.gotext:
--------------------------------------------------------------------------------
1 |
4 |
5 | Show {{ $.Title }}
6 |
7 |
8 |
9 | {#each Object.keys({{ $.Singular }}) as key}
10 | {key} |
11 | {/each}
12 |
13 |
14 | {#each Object.keys({{ $.Singular }}) as key}
15 | { {{- $.Singular }}[key]} |
16 | {/each}
17 |
18 |
19 |
20 |
21 |
22 | Back
23 | |
24 | Edit
25 |
26 |
31 |
--------------------------------------------------------------------------------
/internal/cli/tool_cache.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "os"
6 |
7 | "github.com/livebud/bud/framework"
8 | )
9 |
10 | type ToolCacheClean struct {
11 | Flag *framework.Flag
12 | ListenDev string
13 | }
14 |
15 | func (c *CLI) ToolCacheClean(ctx context.Context, in *ToolCacheClean) error {
16 | module, err := c.findModule()
17 | if err != nil {
18 | return err
19 | }
20 | // Remove the old cache
21 | // TODO: this can be removed in a future release
22 | if err := os.RemoveAll(module.Directory("bud", ".cache")); err != nil {
23 | return err
24 | }
25 | // Remove the new SQLite cache
26 | if err := os.RemoveAll(module.Directory("bud", "bud.db")); err != nil {
27 | return err
28 | }
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/internal/cli/tool_ds.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/livebud/bud/framework"
7 | )
8 |
9 | type ToolDS struct {
10 | Flag *framework.Flag
11 | ListenDev string
12 | }
13 |
14 | func (c *CLI) ToolDS(ctx context.Context, in *ToolDS) error {
15 | bus := c.bus()
16 |
17 | devLn, err := c.listenDev(in.ListenDev)
18 | if err != nil {
19 | return err
20 | }
21 |
22 | log, err := c.loadLog()
23 | if err != nil {
24 | return err
25 | }
26 | log.Info("Listening on http://" + devLn.Addr().String())
27 |
28 | v8, err := c.loadV8()
29 | if err != nil {
30 | return err
31 | }
32 |
33 | devServer := c.devServer(bus, devLn, in.Flag, log, v8)
34 | return devServer.Listen(ctx)
35 | }
36 |
--------------------------------------------------------------------------------
/internal/cli/tool_fs_cat.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/fs"
7 | "path"
8 | "path/filepath"
9 |
10 | "github.com/livebud/bud/framework"
11 | "github.com/livebud/bud/package/virtual"
12 | )
13 |
14 | type ToolFsCat struct {
15 | Flag *framework.Flag
16 | Path string
17 | }
18 |
19 | func (c *CLI) ToolFsCat(ctx context.Context, in *ToolFsCat) error {
20 | // Generate bud files
21 | generate := &Generate{Flag: in.Flag}
22 | if err := c.Generate(ctx, generate); err != nil {
23 | return err
24 | }
25 | abs, err := filepath.Abs(c.Dir)
26 | if err != nil {
27 | return err
28 | }
29 | fsys := virtual.OS(abs)
30 |
31 | // Read the file out
32 | code, err := fs.ReadFile(fsys, path.Clean(in.Path))
33 | if err != nil {
34 | return err
35 | }
36 |
37 | // Print it out
38 | fmt.Fprintln(c.Stdout, string(code))
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/internal/cli/tool_fs_ls.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/fs"
7 | "path"
8 | "path/filepath"
9 | "sort"
10 |
11 | "github.com/livebud/bud/framework"
12 | "github.com/livebud/bud/package/virtual"
13 | )
14 |
15 | type ToolFsLs struct {
16 | Flag *framework.Flag
17 | Path string
18 | }
19 |
20 | func (c *CLI) ToolFsLs(ctx context.Context, in *ToolFsLs) error {
21 | // Generate bud files
22 | generate := &Generate{Flag: in.Flag}
23 | if err := c.Generate(ctx, generate); err != nil {
24 | return err
25 | }
26 | abs, err := filepath.Abs(c.Dir)
27 | if err != nil {
28 | return err
29 | }
30 | fsys := virtual.OS(abs)
31 | // Read the directory out
32 | des, err := fs.ReadDir(fsys, path.Clean(in.Path))
33 | if err != nil {
34 | return err
35 | }
36 | // Directories come first
37 | sort.Slice(des, func(i, j int) bool {
38 | if des[i].IsDir() && !des[j].IsDir() {
39 | return true
40 | } else if !des[i].IsDir() && des[j].IsDir() {
41 | return false
42 | }
43 | return des[i].Name() < des[j].Name()
44 | })
45 | // Print out list
46 | for _, de := range des {
47 | name := de.Name()
48 | if de.IsDir() {
49 | name += "/"
50 | }
51 | fmt.Fprintln(c.Stdout, name)
52 | }
53 | return nil
54 | }
55 |
--------------------------------------------------------------------------------
/internal/cli/tool_fs_tree.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/fs"
7 | "path"
8 | "path/filepath"
9 |
10 | "github.com/livebud/bud/framework"
11 | "github.com/livebud/bud/package/virtual"
12 | )
13 |
14 | type ToolFsTree struct {
15 | Flag *framework.Flag
16 | Path string
17 | }
18 |
19 | func (c *CLI) ToolFsTree(ctx context.Context, in *ToolFsTree) error {
20 | // Generate bud files
21 | generate := &Generate{Flag: in.Flag}
22 | if err := c.Generate(ctx, generate); err != nil {
23 | return err
24 | }
25 | abs, err := filepath.Abs(c.Dir)
26 | if err != nil {
27 | return err
28 | }
29 | fsys := virtual.OS(abs)
30 | sub, err := fs.Sub(fsys, path.Clean(in.Path))
31 | if err != nil {
32 | return err
33 | }
34 | // Print out the tree
35 | tree, err := virtual.Print(sub)
36 | if err != nil {
37 | return err
38 | }
39 | fmt.Fprintln(c.Stdout, tree)
40 |
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/internal/cli/tool_fs_txtar.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io/fs"
7 | "path"
8 | "path/filepath"
9 |
10 | "github.com/livebud/bud/framework"
11 | "github.com/livebud/bud/package/virtual"
12 | "golang.org/x/tools/txtar"
13 | )
14 |
15 | type ToolFsTxtar struct {
16 | Flag *framework.Flag
17 | Path string
18 | }
19 |
20 | func (c *CLI) ToolFsTxtar(ctx context.Context, in *ToolFsTxtar) error {
21 | // Generate bud files
22 | generate := &Generate{Flag: in.Flag}
23 | if err := c.Generate(ctx, generate); err != nil {
24 | return err
25 | }
26 |
27 | // Get the current working directory
28 | abs, err := filepath.Abs(c.Dir)
29 | if err != nil {
30 | return err
31 | }
32 | fsys := virtual.OS(abs)
33 |
34 | // Walk the directory, adding all the files to the archive
35 | ar := new(txtar.Archive)
36 | dir := path.Clean(in.Path)
37 | err = fs.WalkDir(fsys, dir, func(path string, de fs.DirEntry, err error) error {
38 | if err != nil {
39 | return err
40 | } else if de.IsDir() {
41 | return nil
42 | }
43 | code, err := fs.ReadFile(fsys, path)
44 | if err != nil {
45 | return err
46 | }
47 | if isBinary(code) {
48 | return nil
49 | }
50 | ar.Files = append(ar.Files, txtar.File{
51 | Name: path,
52 | Data: code,
53 | })
54 | return nil
55 | })
56 | if err != nil {
57 | return err
58 | }
59 |
60 | // Print the archive to stdout
61 | fmt.Fprintln(c.Stdout, string(txtar.Format(ar)))
62 | return nil
63 | }
64 |
65 | // Check if the given byte slice contains any null bytes. Seems to be a good
66 | // enough heuristic for detecting binary files.
67 | func isBinary(code []byte) bool {
68 | for _, b := range code {
69 | if b == 0 {
70 | return true
71 | }
72 | }
73 | return false
74 | }
75 |
--------------------------------------------------------------------------------
/internal/cli/tool_v8.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | v8 "github.com/livebud/bud/package/js/v8"
8 | )
9 |
10 | type ToolV8 struct {
11 | }
12 |
13 | func (c *CLI) ToolV8(ctx context.Context, in *ToolV8) error {
14 | script, err := c.readStdin()
15 | if err != nil {
16 | return err
17 | }
18 | vm, err := v8.Load()
19 | if err != nil {
20 | return err
21 | }
22 |
23 | result, err := vm.Eval("script.js", script)
24 | if err != nil {
25 | return err
26 | }
27 | fmt.Fprintln(c.Stdout, result)
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/internal/cli/tool_v8_test.go:
--------------------------------------------------------------------------------
1 | package cli_test
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/livebud/bud/internal/is"
10 | "github.com/livebud/bud/internal/testcli"
11 | "github.com/livebud/bud/package/testdir"
12 | )
13 |
14 | func TestToolV8(t *testing.T) {
15 | is := is.New(t)
16 | ctx := context.Background()
17 | td, err := testdir.Load()
18 | is.NoErr(err)
19 | cli := testcli.New(td.Directory())
20 | cli.Stdin = bytes.NewBufferString("2+2")
21 | result, err := cli.Run(ctx, "v8")
22 | is.NoErr(err)
23 | is.Equal(result.Stderr(), "")
24 | is.Equal(strings.TrimSpace(result.Stdout()), "4")
25 | is.NoErr(td.NotExists(
26 | "bud/cmd/app",
27 | "bud/app",
28 | ))
29 | }
30 |
--------------------------------------------------------------------------------
/internal/cli/version.go:
--------------------------------------------------------------------------------
1 | package cli
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "text/tabwriter"
7 |
8 | "github.com/livebud/bud/internal/versions"
9 | )
10 |
11 | type Version struct {
12 | Key string
13 | }
14 |
15 | func (c *CLI) Version(ctx context.Context, in *Version) error {
16 | switch in.Key {
17 | case "bud":
18 | fmt.Fprintln(c.Stdout, versions.Bud)
19 | return nil
20 | case "svelte":
21 | fmt.Fprintln(c.Stdout, versions.Svelte)
22 | return nil
23 | case "react":
24 | fmt.Fprintln(c.Stdout, versions.React)
25 | return nil
26 | default:
27 | tw := tabwriter.NewWriter(c.Stdout, 0, 0, 2, ' ', tabwriter.AlignRight)
28 | tw.Write([]byte("bud: \t" + versions.Bud + "\n"))
29 | tw.Write([]byte("svelte: \t" + versions.Svelte + "\n"))
30 | tw.Write([]byte("react: \t" + versions.React + "\n"))
31 | return tw.Flush()
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/internal/current/current.go:
--------------------------------------------------------------------------------
1 | package current
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | )
9 |
10 | // Filename gets the current filename of the caller
11 | func Filename() (string, error) {
12 | return filename(2)
13 | }
14 |
15 | func filename(skip int) (string, error) {
16 | _, filename, _, ok := runtime.Caller(skip)
17 | if !ok {
18 | return "", errors.New("unable to get the current filename")
19 | }
20 | return filename, nil
21 | }
22 |
23 | // Directory gets the current directory of the caller
24 | func Directory() (string, error) {
25 | name, err := filename(2)
26 | if err != nil {
27 | return "", err
28 | }
29 | dir := filepath.Dir(name)
30 | // When we use --trimpath, attempt to find the absolute path anyway
31 | if !filepath.IsAbs(dir) {
32 | // Hail mary attempt to find it within $GOPATH/src
33 | if gopath := os.Getenv("GOPATH"); gopath != "" {
34 | dir = filepath.Join(gopath, "src", dir)
35 | }
36 | }
37 | return dir, nil
38 | }
39 |
--------------------------------------------------------------------------------
/internal/current/current_test.go:
--------------------------------------------------------------------------------
1 | package current_test
2 |
3 | import (
4 | "path/filepath"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/current"
8 | "github.com/livebud/bud/internal/is"
9 | )
10 |
11 | func TestDir(t *testing.T) {
12 | is := is.New(t)
13 | dirname, err := current.Directory()
14 | is.NoErr(err)
15 | is.Equal(filepath.Base(dirname), "current")
16 | }
17 |
18 | func TestFile(t *testing.T) {
19 | is := is.New(t)
20 | filename, err := current.Filename()
21 | is.NoErr(err)
22 | is.Equal(filepath.Base(filename), "current_test.go")
23 | }
24 |
--------------------------------------------------------------------------------
/internal/dag/dag.go:
--------------------------------------------------------------------------------
1 | package dag
2 |
3 | import "github.com/livebud/bud/package/virtual"
4 |
5 | type Cache interface {
6 | Get(path string) (*virtual.File, error)
7 | Set(path string, file *virtual.File) error
8 | Link(from string, toPatterns ...string) error
9 | Delete(paths ...string) error
10 | Reset() error
11 | Close() error
12 | }
13 |
--------------------------------------------------------------------------------
/internal/dag/discard.go:
--------------------------------------------------------------------------------
1 | package dag
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/livebud/bud/package/genfs"
7 | "github.com/livebud/bud/package/virtual"
8 | )
9 |
10 | var Discard = discard{}
11 |
12 | type discard struct{}
13 |
14 | var _ genfs.Cache = (*discard)(nil)
15 | var _ Cache = (*discard)(nil)
16 |
17 | func (discard) Get(path string) (*virtual.File, error) {
18 | return nil, errors.New("not found")
19 | }
20 |
21 | func (discard) Set(path string, file *virtual.File) error {
22 | return nil
23 | }
24 |
25 | func (discard) Link(from string, toPatterns ...string) error {
26 | return nil
27 | }
28 |
29 | func (discard) Delete(paths ...string) error {
30 | return nil
31 | }
32 |
33 | func (discard) Reset() error {
34 | return nil
35 | }
36 |
37 | func (discard) Close() error {
38 | return nil
39 | }
40 |
--------------------------------------------------------------------------------
/internal/embed/file.go:
--------------------------------------------------------------------------------
1 | package embed
2 |
3 | import "strings"
4 |
5 | type File struct {
6 | Path string
7 | Data Data
8 | }
9 |
10 | type Data []byte
11 |
12 | const lowerHex = "0123456789abcdef"
13 |
14 | // Based on:
15 | // https://github.com/go-bindata/go-bindata/blob/26949cc13d95310ffcc491c325da869a5aafce8f/stringwriter.go#L18-L36
16 | func (data Data) String() string {
17 | if len(data) == 0 {
18 | return ""
19 | }
20 | s := new(strings.Builder)
21 | buf := []byte(`\x00`)
22 | for _, b := range data {
23 | buf[2] = lowerHex[b/16]
24 | buf[3] = lowerHex[b%16]
25 | s.Write(buf)
26 | }
27 | return s.String()
28 | }
29 |
--------------------------------------------------------------------------------
/internal/embedded/embedded.go:
--------------------------------------------------------------------------------
1 | package embedded
2 |
3 | import (
4 | _ "embed"
5 | )
6 |
7 | //go:embed favicon.ico
8 | var favicon []byte
9 |
10 | // Favicon returns the favicon data
11 | func Favicon() []byte {
12 | return favicon
13 | }
14 |
15 | //go:embed gitignore.txt
16 | var gitignore []byte
17 |
18 | // Gitignore returns the gitignore data
19 | func Gitignore() []byte {
20 | return gitignore
21 | }
22 |
--------------------------------------------------------------------------------
/internal/embedded/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/livebud/bud/721420fadaf11a0539aa783e833f2e8aaf81b719/internal/embedded/favicon.ico
--------------------------------------------------------------------------------
/internal/embedded/gitignore.txt:
--------------------------------------------------------------------------------
1 | node_modules
2 | bud
--------------------------------------------------------------------------------
/internal/entrypoint/find.go:
--------------------------------------------------------------------------------
1 | package entrypoint
2 |
3 | import (
4 | "fmt"
5 | "io/fs"
6 | "path/filepath"
7 | )
8 |
9 | func FindByPage(fsys fs.FS, page string) (view *View, err error) {
10 | views, err := List(fsys, "view")
11 | if err != nil {
12 | return nil, err
13 | }
14 | page = filepath.Clean(page)
15 | for _, view := range views {
16 | if string(view.Page) == page {
17 | return view, nil
18 | }
19 | }
20 | return nil, fmt.Errorf("unable to find view by page %q", page)
21 | }
22 |
23 | func FindByClient(fsys fs.FS, client string) (view *View, err error) {
24 | views, err := List(fsys, "view")
25 | if err != nil {
26 | return nil, err
27 | }
28 | for _, view := range views {
29 | if view.Client == client {
30 | return view, nil
31 | }
32 | }
33 | return nil, fmt.Errorf("unable to find view by client path %q", client)
34 | }
35 |
--------------------------------------------------------------------------------
/internal/envs/envs.go:
--------------------------------------------------------------------------------
1 | package envs
2 |
3 | import (
4 | "sort"
5 | "strings"
6 | )
7 |
8 | // Map is an environment helper type
9 | type Map map[string]string
10 |
11 | // Parse an environment list into a map
12 | func From(list []string) Map {
13 | m := Map{}
14 | for _, row := range list {
15 | kvs := strings.SplitN(row, "=", 2)
16 | if len(kvs) != 2 {
17 | continue
18 | }
19 | m[kvs[0]] = kvs[1]
20 | }
21 | return m
22 | }
23 |
24 | // List out the environment keys in alphanumerical order.
25 | func (m Map) List() (env []string) {
26 | for k, v := range m {
27 | env = append(env, k+"="+v)
28 | }
29 | sort.Strings(env)
30 | return env
31 | }
32 |
33 | func (m Map) Append(list ...string) Map {
34 | lm := From(list)
35 | for k, v := range lm {
36 | m[k] = v
37 | }
38 | return m
39 | }
40 |
--------------------------------------------------------------------------------
/internal/envs/envs_test.go:
--------------------------------------------------------------------------------
1 | package envs_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/envs"
7 | "github.com/livebud/bud/internal/is"
8 | )
9 |
10 | func TestFrom(t *testing.T) {
11 | is := is.New(t)
12 | env := envs.From([]string{
13 | "HOME=/users/matt",
14 | "PATH=/usr/local/bin:/usr/bin",
15 | })
16 | is.Equal(env["HOME"], "/users/matt")
17 | is.Equal(env["PATH"], "/usr/local/bin:/usr/bin")
18 | }
19 |
20 | func TestList(t *testing.T) {
21 | is := is.New(t)
22 | env := envs.Map{
23 | "B": "B",
24 | "A": "A",
25 | "C": "C",
26 | }
27 | list := env.List()
28 | is.Equal(len(list), 3)
29 | is.Equal(list[0], "A=A")
30 | is.Equal(list[1], "B=B")
31 | is.Equal(list[2], "C=C")
32 | }
33 |
--------------------------------------------------------------------------------
/internal/errs/errs.go:
--------------------------------------------------------------------------------
1 | // Package errs makes it easier to join multiple errors into a single error.
2 | package errs
3 |
4 | import (
5 | "errors"
6 | "fmt"
7 | "strings"
8 |
9 | "github.com/livebud/bud/internal/ansi"
10 | )
11 |
12 | // Join multiple errors together into one error
13 | func Join(errs ...error) error {
14 | var agg error
15 | for _, err := range errs {
16 | if err == nil {
17 | continue
18 | } else if agg == nil {
19 | agg = err
20 | continue
21 | } else if errors.Is(err, agg) {
22 | agg = fmt.Errorf("%w. %s", agg, err)
23 | } else {
24 | agg = fmt.Errorf("%s. %s", agg, err)
25 | }
26 | }
27 | return agg
28 | }
29 |
30 | // Errors is an optional interface that be used to unwrap multiple errors
31 | type Errors interface {
32 | Errors() []error
33 | }
34 |
35 | // Format reverses the error order to make the cause come first
36 | func Format(err error) string {
37 | // Most errors in Bud are joined by a period
38 | lines := strings.Split(err.Error(), ". ")
39 | lineLen := len(lines)
40 | stack := make([]string, lineLen)
41 | j := lineLen - 1
42 | // Reverse the error order
43 | for i := 0; i < lineLen; i++ {
44 | line := lines[j]
45 | if i > 0 {
46 | line = " " + ansi.Dim(line)
47 | }
48 | stack[i] = line
49 | j--
50 | }
51 | return strings.Join(stack, "\n")
52 | }
53 |
--------------------------------------------------------------------------------
/internal/esmeta/esmeta.go:
--------------------------------------------------------------------------------
1 | package esmeta
2 |
3 | import (
4 | "encoding/json"
5 | "strings"
6 | )
7 |
8 | func Parse(metafile string) (*File, error) {
9 | file := new(File)
10 | if err := json.Unmarshal([]byte(metafile), file); err != nil {
11 | return nil, err
12 | }
13 | return file, nil
14 | }
15 |
16 | type File struct {
17 | Inputs map[string]*Input `json:"inputs,omitempty"`
18 | Outputs map[string]*Output `json:"outputs,omitempty"`
19 | }
20 |
21 | func (f *File) Dependencies() (deps []string) {
22 | for _, output := range f.Outputs {
23 | for input := range output.Inputs {
24 | idx := strings.IndexByte(input, ':')
25 | if idx >= 0 {
26 | continue
27 | }
28 | deps = append(deps, input)
29 | }
30 | }
31 | return deps
32 | }
33 |
34 | type Input struct {
35 | Bytes int `json:"bytes,omitempty"`
36 | Imports []*Import `json:"imports,omitempty"`
37 | }
38 |
39 | type Output struct {
40 | Bytes int `json:"bytes,omitempty"`
41 | Inputs map[string]*OutputInput `json:"inputs,omitempty"`
42 | Imports []*Import `json:"imports,omitempty"`
43 | Exports []string `json:"exports,omitempty"`
44 | EntryPoint *string `json:"entryPoint,omitempty"`
45 | }
46 |
47 | type OutputInput struct {
48 | BytesInOutput int `json:"bytesInOutput,omitempty"`
49 | }
50 |
51 | type Import struct {
52 | Path string `json:"path,omitempty"`
53 | Kind string `json:"kind,omitempty"`
54 | }
55 |
--------------------------------------------------------------------------------
/internal/gitignore/gitignore.go:
--------------------------------------------------------------------------------
1 | package gitignore
2 |
3 | import (
4 | "io/fs"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 |
9 | gitignore "github.com/sabhiram/go-gitignore"
10 | )
11 |
12 | var alwaysIgnore = []string{
13 | "node_modules",
14 | ".git",
15 | ".DS_Store",
16 | // Regardless of if this directory is committed or not, it should be ignored
17 | // because this will trigger unnecessary rebuilds during development.
18 | "bud",
19 | }
20 |
21 | var defaultIgnores = append([]string{"/bud"}, alwaysIgnore...)
22 |
23 | var defaultIgnore = gitignore.CompileIgnoreLines(defaultIgnores...).MatchesPath
24 |
25 | func FromFS(fsys fs.FS) (ignore func(path string) bool) {
26 | code, err := fs.ReadFile(fsys, ".gitignore")
27 | if err != nil {
28 | return defaultIgnore
29 | }
30 | lines := strings.Split(string(code), "\n")
31 | lines = append(lines, alwaysIgnore...)
32 | ignorer := gitignore.CompileIgnoreLines(lines...)
33 | return ignorer.MatchesPath
34 | }
35 |
36 | func From(dir string) (ignore func(path string) bool) {
37 | code, err := os.ReadFile(filepath.Join(dir, ".gitignore"))
38 | if err != nil {
39 | return defaultIgnore
40 | }
41 | lines := strings.Split(string(code), "\n")
42 | lines = append(lines, alwaysIgnore...)
43 | ignorer := gitignore.CompileIgnoreLines(lines...)
44 | return ignorer.MatchesPath
45 | }
46 |
--------------------------------------------------------------------------------
/internal/gitignore/gitignore_test.go:
--------------------------------------------------------------------------------
1 | package gitignore_test
2 |
3 | import (
4 | "testing"
5 | "testing/fstest"
6 |
7 | "github.com/livebud/bud/internal/gitignore"
8 | "github.com/livebud/bud/internal/is"
9 | )
10 |
11 | func TestBudRoot(t *testing.T) {
12 | is := is.New(t)
13 | ignore := gitignore.FromFS(fstest.MapFS{
14 | ".gitignore": &fstest.MapFile{Data: []byte(`/bud`)},
15 | })
16 | is.True(ignore("bud/internal/web/web.go"))
17 | is.True(!ignore("main.go"))
18 | }
19 |
20 | func TestGitDir(t *testing.T) {
21 | is := is.New(t)
22 | ignore := gitignore.FromFS(fstest.MapFS{
23 | ".gitignore": &fstest.MapFile{Data: []byte(``)},
24 | })
25 | is.True(ignore(".git"))
26 | is.True(ignore(".git/objects"))
27 | }
28 |
29 | func TestNodeModules(t *testing.T) {
30 | is := is.New(t)
31 | ignore := gitignore.FromFS(fstest.MapFS{
32 | ".gitignore": &fstest.MapFile{Data: []byte(``)},
33 | })
34 | is.True(ignore("node_modules"))
35 | is.True(ignore("node_modules/svelte/internal/compiler.js"))
36 | }
37 |
--------------------------------------------------------------------------------
/internal/glob/base.go:
--------------------------------------------------------------------------------
1 | package glob
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 |
7 | "github.com/gobwas/glob/syntax/lexer"
8 | )
9 |
10 | const sep = string(filepath.Separator)
11 |
12 | // Base gets the non-magical part of the glob
13 | func Base(pattern string) string {
14 | parts := strings.Split(pattern, sep)
15 | var base []string
16 | outer:
17 | for _, part := range parts {
18 | lex := lexer.NewLexer(part)
19 | inner:
20 | for {
21 | token := lex.Next()
22 | switch token.Type {
23 | case lexer.Text:
24 | continue
25 | case lexer.EOF:
26 | break inner
27 | default:
28 | break outer
29 | }
30 | }
31 | base = append(base, part)
32 | }
33 | if len(base) == 0 {
34 | return "."
35 | } else if len(base) == 1 && base[0] == "" {
36 | return sep
37 | }
38 | return filepath.Clean(strings.Join(base, sep))
39 | }
40 |
--------------------------------------------------------------------------------
/internal/glob/base_test.go:
--------------------------------------------------------------------------------
1 | package glob_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/glob"
7 | "github.com/livebud/bud/internal/is"
8 | )
9 |
10 | func TestBase(t *testing.T) {
11 | is := is.New(t)
12 | test := func(input, expect string) {
13 | is.Helper()
14 | is.Equal(glob.Base(input), expect)
15 | }
16 | test(".", ".")
17 | test(".*", ".")
18 | test("a/*/b", "a")
19 | test("a*/.*/b", ".")
20 | test("*/a/b/c", ".")
21 | test("*", ".")
22 | test("*/", ".")
23 | test("*/*", ".")
24 | test("*/*/", ".")
25 | test("**", ".")
26 | test("**/", ".")
27 | test("**/*", ".")
28 | test("**/*/", ".")
29 | test("/*.js", "/")
30 | test("*.js", ".")
31 | test("**/*.js", ".")
32 | test("{a,b}", ".")
33 | test("/{a,b}", "/")
34 | test("/{a,b}/", "/")
35 | test("{a,b}", ".")
36 | test("/{a,b}", "/")
37 | test("./{a,b}", ".")
38 | test("path/to/*.js", "path/to")
39 | test("/root/path/to/*.js", "/root/path/to")
40 | test("chapter/foo [bar]/", "chapter")
41 | test("path/[a-z]", "path")
42 | test("[a-z]", ".")
43 | test("path/{to,from}", "path")
44 | test("path/!/foo", "path/!/foo")
45 | test("path/?/foo", "path")
46 | test("path/+/foo", "path/+/foo")
47 | test("path/*/foo", "path")
48 | test("path/@/foo", "path/@/foo")
49 | test("path/!/foo/", "path/!/foo")
50 | test("path/?/foo/", "path")
51 | test("path/+/foo/", "path/+/foo")
52 | test("path/*/foo/", "path")
53 | test("path/@/foo/", "path/@/foo")
54 | test("path/**/*", "path")
55 | test("path/**/subdir/foo.*", "path")
56 | test("path/subdir/**/foo.js", "path/subdir")
57 | test("path/!subdir/foo.js", "path/!subdir/foo.js")
58 | test("path/{foo,bar}/", "path")
59 | test("{controller/**.go,view/**}", ".")
60 | test("{controller/**.go,view/**}", ".")
61 | }
62 |
--------------------------------------------------------------------------------
/internal/glob/bases.go:
--------------------------------------------------------------------------------
1 | package glob
2 |
3 | import "github.com/livebud/bud/internal/orderedset"
4 |
5 | // Bases returns all non-magical parts of the glob
6 | func Bases(pattern string) ([]string, error) {
7 | expands, err := Expand(pattern)
8 | if err != nil {
9 | return nil, err
10 | }
11 | bases := make([]string, len(expands))
12 | for i, expand := range expands {
13 | bases[i] = Base(expand)
14 | }
15 | return orderedset.Strings(bases...), nil
16 | }
17 |
--------------------------------------------------------------------------------
/internal/glob/compile.go:
--------------------------------------------------------------------------------
1 | package glob
2 |
3 | import "github.com/gobwas/glob"
4 |
5 | type Matcher = glob.Glob
6 |
7 | // Comple
8 | func Compile(pattern string) (Matcher, error) {
9 | // Expand patterns like {a,b} into multiple globs a & b. This avoids an
10 | // infinite loop described in this comment:
11 | // https://github.com/gobwas/glob/issues/50#issuecomment-1330182417
12 | patterns, err := Expand(pattern)
13 | if err != nil {
14 | return nil, err
15 | }
16 | globs := make(globs, len(patterns))
17 | for i, pattern := range patterns {
18 | glob, err := glob.Compile(pattern)
19 | if err != nil {
20 | return nil, err
21 | }
22 | globs[i] = glob
23 | }
24 | return globs, nil
25 | }
26 |
27 | type globs []glob.Glob
28 |
29 | func (globs globs) Match(path string) bool {
30 | for _, glob := range globs {
31 | if glob.Match(path) {
32 | return true
33 | }
34 | }
35 | return false
36 | }
37 |
--------------------------------------------------------------------------------
/internal/glob/compile_test.go:
--------------------------------------------------------------------------------
1 | package glob_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/glob"
7 | "github.com/livebud/bud/internal/is"
8 | )
9 |
10 | func TestMatch(t *testing.T) {
11 | is := is.New(t)
12 | matcher, err := glob.Compile("{controller/**.go,view/**}")
13 | is.NoErr(err)
14 | is.True(matcher.Match("controller/controller.go"))
15 | is.True(matcher.Match("view/index.svelte"))
16 | }
17 |
18 | func TestDirMatch(t *testing.T) {
19 | is := is.New(t)
20 | matcher, err := glob.Compile("controller/*/**.go")
21 | is.NoErr(err)
22 | is.True(!matcher.Match("controller"))
23 | is.True(!matcher.Match("controller/controller.go"))
24 | is.True(matcher.Match("controller/view/view.go"))
25 | is.True(matcher.Match("controller/public/public.go"))
26 | }
27 |
28 | func TestMatchSubdir(t *testing.T) {
29 | is := is.New(t)
30 | matcher, err := glob.Compile(`{generator/**.go,bud/internal/generator/*/**.go}`)
31 | is.NoErr(err)
32 | is.True(!matcher.Match("bud/internal/generator/generator.go"))
33 | }
34 |
--------------------------------------------------------------------------------
/internal/gois/builtin.go:
--------------------------------------------------------------------------------
1 | package gois
2 |
3 | import "strings"
4 |
5 | // Builtin checks if the dataType is built-in.
6 | // TODO handle more complex types e.g. map[string]LocalControllerStruct
7 | func Builtin(dataType string) bool {
8 | dataType = strings.TrimLeft(dataType, "[]*")
9 | if _, ok := builtin[dataType]; ok {
10 | return true
11 | }
12 | return false
13 | }
14 |
15 | // builtin types
16 | var builtin = map[string]struct{}{
17 | "string": {},
18 | "bool": {},
19 | "error": {},
20 | "int8": {},
21 | "uint8": {},
22 | "byte": {},
23 | "int16": {},
24 | "uint16": {},
25 | "int32": {},
26 | "rune": {},
27 | "uint32": {},
28 | "int64": {},
29 | "uint64": {},
30 | "int": {},
31 | "uint": {},
32 | "uintptr": {},
33 | "float32": {},
34 | "float64": {},
35 | "complex64": {},
36 | "complex128": {},
37 | }
38 |
--------------------------------------------------------------------------------
/internal/npm/npm_test.go:
--------------------------------------------------------------------------------
1 | package npm_test
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "testing"
7 |
8 | "github.com/livebud/bud/internal/is"
9 | "github.com/livebud/bud/internal/npm"
10 | )
11 |
12 | func exists(t testing.TB, path string) {
13 | t.Helper()
14 | if _, err := os.Stat(path); err != nil {
15 | t.Fatal(err)
16 | }
17 | }
18 |
19 | func TestInstallSvelte(t *testing.T) {
20 | is := is.New(t)
21 | is.NoErr(os.RemoveAll("_tmp"))
22 | defer func() {
23 | if !t.Failed() {
24 | is.NoErr(os.RemoveAll("_tmp"))
25 | }
26 | }()
27 | err := npm.Install("_tmp", "svelte@3.42.3", "uid@2.0.0")
28 | is.NoErr(err)
29 | exists(t, filepath.Join("_tmp", "node_modules", "svelte", "package.json"))
30 | exists(t, filepath.Join("_tmp", "node_modules", "uid", "package.json"))
31 | exists(t, filepath.Join("_tmp", "node_modules", "svelte", "internal", "index.js"))
32 | }
33 |
--------------------------------------------------------------------------------
/internal/once/bytes.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import "sync"
4 |
5 | type Bytes struct {
6 | o sync.Once
7 | v []byte
8 | e error
9 | }
10 |
11 | func (b *Bytes) Do(fn func() ([]byte, error)) ([]byte, error) {
12 | b.o.Do(func() { b.v, b.e = fn() })
13 | return b.v, b.e
14 | }
15 |
--------------------------------------------------------------------------------
/internal/once/bytes_test.go:
--------------------------------------------------------------------------------
1 | package once_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/once"
9 | )
10 |
11 | func TestBytesNil(t *testing.T) {
12 | is := is.New(t)
13 | var once once.Bytes
14 | called := 0
15 | res, err := once.Do(func() ([]byte, error) {
16 | called++
17 | return []byte("1"), nil
18 | })
19 | is.NoErr(err)
20 | is.Equal(called, 1)
21 | is.Equal(res, []byte("1"))
22 | res, err = once.Do(func() ([]byte, error) {
23 | called++
24 | return []byte("2"), errors.New("oh noz")
25 | })
26 | is.NoErr(err)
27 | is.Equal(called, 1)
28 | is.Equal(res, []byte("1"))
29 | }
30 |
31 | func TestBytesError(t *testing.T) {
32 | is := is.New(t)
33 | var once once.Bytes
34 | called := 0
35 | res, err := once.Do(func() ([]byte, error) {
36 | called++
37 | return []byte("1"), errors.New("oh noz")
38 | })
39 | is.True(err != nil)
40 | is.Equal(err.Error(), "oh noz")
41 | is.Equal(called, 1)
42 | is.Equal(res, []byte("1"))
43 | res, err = once.Do(func() ([]byte, error) {
44 | called++
45 | return []byte("2"), nil
46 | })
47 | is.True(err != nil)
48 | is.Equal(err.Error(), "oh noz")
49 | is.Equal(called, 1)
50 | is.Equal(res, []byte("1"))
51 | }
52 |
--------------------------------------------------------------------------------
/internal/once/closer.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import (
4 | "github.com/livebud/bud/internal/errs"
5 | )
6 |
7 | type Closer struct {
8 | closes []func() error
9 | once Error
10 | }
11 |
12 | func (c *Closer) Add(fn func() error) {
13 | c.closes = append(c.closes, fn)
14 | }
15 |
16 | func (c *Closer) Close() (err error) {
17 | return c.once.Do(func() error {
18 | for i := len(c.closes) - 1; i >= 0; i-- {
19 | err = errs.Join(err, c.closes[i]())
20 | }
21 | return err
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/internal/once/closer_test.go:
--------------------------------------------------------------------------------
1 | package once_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/once"
9 | )
10 |
11 | func TestCloserOk(t *testing.T) {
12 | is := is.New(t)
13 | var closer once.Closer
14 | a := func() error { return nil }
15 | b := func() error { return nil }
16 | closer.Add(a)
17 | closer.Add(b)
18 | err := closer.Close()
19 | is.NoErr(err)
20 | }
21 |
22 | func TestCloserErrors(t *testing.T) {
23 | is := is.New(t)
24 | e1 := errors.New("error 1")
25 | e2 := errors.New("error 2")
26 | var closer once.Closer
27 | a := func() error { return e1 }
28 | b := func() error { return e2 }
29 | closer.Add(a)
30 | closer.Add(b)
31 | err := closer.Close()
32 | is.True(err != nil)
33 | is.Equal(err.Error(), "error 2. error 1")
34 | }
35 |
--------------------------------------------------------------------------------
/internal/once/direntries.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import (
4 | "io/fs"
5 | "sync"
6 | )
7 |
8 | // DirEntries ensures we only read the directories once
9 | type DirEntries struct {
10 | o sync.Once
11 | v []fs.DirEntry
12 | e error
13 | }
14 |
15 | func (d *DirEntries) Do(fn func() ([]fs.DirEntry, error)) ([]fs.DirEntry, error) {
16 | d.o.Do(func() { d.v, d.e = fn() })
17 | return d.v, d.e
18 | }
19 |
--------------------------------------------------------------------------------
/internal/once/error.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import "sync"
4 |
5 | type Error struct {
6 | o sync.Once
7 | err error
8 | }
9 |
10 | func (e *Error) Do(fn func() error) (err error) {
11 | e.o.Do(func() { e.err = fn() })
12 | return e.err
13 | }
14 |
--------------------------------------------------------------------------------
/internal/once/error_test.go:
--------------------------------------------------------------------------------
1 | package once_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/once"
9 | )
10 |
11 | func TestErrorNil(t *testing.T) {
12 | is := is.New(t)
13 | var once once.Error
14 | called := 0
15 | err := once.Do(func() error {
16 | called++
17 | return nil
18 | })
19 | is.NoErr(err)
20 | is.Equal(called, 1)
21 | err = once.Do(func() error {
22 | called++
23 | return errors.New("oh noz")
24 | })
25 | is.NoErr(err)
26 | is.Equal(called, 1)
27 | }
28 |
29 | func TestError(t *testing.T) {
30 | is := is.New(t)
31 | var once once.Error
32 | called := 0
33 | err := once.Do(func() error {
34 | called++
35 | return errors.New("oh noz")
36 | })
37 | is.True(err != nil)
38 | is.Equal(err.Error(), "oh noz")
39 | is.Equal(called, 1)
40 | err = once.Do(func() error {
41 | called++
42 | return err
43 | })
44 | is.True(err != nil)
45 | is.Equal(err.Error(), "oh noz")
46 | is.Equal(called, 1)
47 | }
48 |
--------------------------------------------------------------------------------
/internal/once/fileinfo.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import (
4 | "io/fs"
5 | "sync"
6 | )
7 |
8 | // FileInfo ensures we only read fileinfo once
9 | type FileInfo struct {
10 | o sync.Once
11 | v fs.FileInfo
12 | e error
13 | }
14 |
15 | func (d *FileInfo) Do(fn func() (fs.FileInfo, error)) (fs.FileInfo, error) {
16 | d.o.Do(func() { d.v, d.e = fn() })
17 | return d.v, d.e
18 | }
19 |
--------------------------------------------------------------------------------
/internal/once/string.go:
--------------------------------------------------------------------------------
1 | package once
2 |
3 | import "sync"
4 |
5 | type String struct {
6 | o sync.Once
7 | s string
8 | e error
9 | }
10 |
11 | func (s *String) Do(fn func() (string, error)) (string, error) {
12 | s.o.Do(func() { s.s, s.e = fn() })
13 | return s.s, s.e
14 | }
15 |
--------------------------------------------------------------------------------
/internal/once/string_test.go:
--------------------------------------------------------------------------------
1 | package once_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/once"
9 | )
10 |
11 | func TestStringNil(t *testing.T) {
12 | is := is.New(t)
13 | var once once.String
14 | called := 0
15 | res, err := once.Do(func() (string, error) {
16 | called++
17 | return "1", nil
18 | })
19 | is.NoErr(err)
20 | is.Equal(called, 1)
21 | is.Equal(res, "1")
22 | res, err = once.Do(func() (string, error) {
23 | called++
24 | return "2", errors.New("oh noz")
25 | })
26 | is.NoErr(err)
27 | is.Equal(called, 1)
28 | is.Equal(res, "1")
29 | }
30 |
31 | func TestStringError(t *testing.T) {
32 | is := is.New(t)
33 | var once once.String
34 | called := 0
35 | res, err := once.Do(func() (string, error) {
36 | called++
37 | return "1", errors.New("oh noz")
38 | })
39 | is.True(err != nil)
40 | is.Equal(err.Error(), "oh noz")
41 | is.Equal(called, 1)
42 | is.Equal(res, "1")
43 | res, err = once.Do(func() (string, error) {
44 | called++
45 | return "2", nil
46 | })
47 | is.True(err != nil)
48 | is.Equal(err.Error(), "oh noz")
49 | is.Equal(called, 1)
50 | is.Equal(res, "1")
51 | }
52 |
--------------------------------------------------------------------------------
/internal/orderedset/strings.go:
--------------------------------------------------------------------------------
1 | package orderedset
2 |
3 | func Strings(list ...string) []string {
4 | seen := map[string]struct{}{}
5 | set := make([]string, 0, len(list))
6 | for _, item := range list {
7 | if _, ok := seen[item]; ok {
8 | continue
9 | }
10 | seen[item] = struct{}{}
11 | set = append(set, item)
12 | }
13 | return set
14 | }
15 |
--------------------------------------------------------------------------------
/internal/orderedset/strings_test.go:
--------------------------------------------------------------------------------
1 | package orderedset_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/is"
7 | "github.com/livebud/bud/internal/orderedset"
8 | )
9 |
10 | func TestOrderedStringSet(t *testing.T) {
11 | is := is.New(t)
12 | is.Equal(orderedset.Strings(
13 | []string{"a", "c", "b", "b", "d", "a", "c", "d", "e"}...),
14 | []string{"a", "c", "b", "d", "e"},
15 | )
16 | is.Equal(orderedset.Strings(
17 | []string{"a", "a", "a"}...),
18 | []string{"a"},
19 | )
20 | is.Equal(orderedset.Strings(), []string{})
21 | }
22 |
--------------------------------------------------------------------------------
/internal/printfs/printfs.go:
--------------------------------------------------------------------------------
1 | package printfs
2 |
3 | import (
4 | "io/fs"
5 | "path/filepath"
6 | "strings"
7 |
8 | "github.com/xlab/treeprint"
9 | )
10 |
11 | func New() *Tree {
12 | return &Tree{
13 | tree: treeprint.New(),
14 | }
15 | }
16 |
17 | type Tree struct {
18 | tree treeprint.Tree
19 | }
20 |
21 | func (t *Tree) Add(path string) {
22 | parent := t.tree
23 | for _, element := range strings.Split(filepath.ToSlash(path), "/") {
24 | existing := parent.FindByValue(element)
25 | if existing != nil {
26 | parent = existing
27 | } else {
28 | parent = parent.AddBranch(element)
29 | }
30 | }
31 | }
32 |
33 | func (t *Tree) String() string {
34 | return t.tree.String()
35 | }
36 |
37 | func Walk(fsys fs.FS) (*Tree, error) {
38 | tree := New()
39 | err := fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error {
40 | if err != nil {
41 | return err
42 | } else if path == "." {
43 | return nil
44 | }
45 | tree.Add(path)
46 | return nil
47 | })
48 | if err != nil {
49 | return nil, err
50 | }
51 | return tree, nil
52 | }
53 |
54 | func Print(fsys fs.FS, dir string) (string, error) {
55 | tree := New()
56 | // Set the top-node
57 | tree.tree.SetValue(dir)
58 | subfs, err := fs.Sub(fsys, dir)
59 | if err != nil {
60 | return "", err
61 | }
62 | // Only walk the sub-tree
63 | err = fs.WalkDir(subfs, ".", func(path string, de fs.DirEntry, err error) error {
64 | if err != nil {
65 | return err
66 | } else if path == "." {
67 | return nil
68 | }
69 | tree.Add(path)
70 | return nil
71 | })
72 | if err != nil {
73 | return "", err
74 | }
75 | return tree.String(), nil
76 | }
77 |
--------------------------------------------------------------------------------
/internal/pubsub/discard.go:
--------------------------------------------------------------------------------
1 | package pubsub
2 |
3 | func Discard() *discarder {
4 | return &discarder{}
5 | }
6 |
7 | type discarder struct {
8 | }
9 |
10 | var _ Publisher = (*discarder)(nil)
11 | var _ Subscriber = (*discarder)(nil)
12 |
13 | func (d *discarder) Publish(topic string, data []byte) {
14 | }
15 |
16 | func (d *discarder) Subscribe(topics ...string) Subscription {
17 | ch := make(chan []byte)
18 | close(ch) // Close the channel immediately
19 | closer := func() {}
20 | return &discardable{ch, closer}
21 | }
22 |
23 | type discardable struct {
24 | ch chan []byte
25 | closer func()
26 | }
27 |
28 | var _ Subscription = (*discardable)(nil)
29 |
30 | func (s *discardable) Wait() <-chan []byte {
31 | return s.ch
32 | }
33 |
34 | func (s *discardable) Close() {
35 | s.closer()
36 | }
37 |
--------------------------------------------------------------------------------
/internal/pubsub/pubsub_test.go:
--------------------------------------------------------------------------------
1 | package pubsub_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/is"
7 | "github.com/livebud/bud/internal/pubsub"
8 | "golang.org/x/sync/errgroup"
9 | )
10 |
11 | func TestPubSub(t *testing.T) {
12 | is := is.New(t)
13 | ps := pubsub.New()
14 | ps.Publish("toast", []byte("nothing to publish to yet"))
15 | sub := ps.Subscribe("toast")
16 | eg := new(errgroup.Group)
17 | eg.Go(func() error {
18 | msg := <-sub.Wait()
19 | is.Equal(string(msg), "toast is ready")
20 | return nil
21 | })
22 | ps.Publish("toast", []byte("toast is ready"))
23 | is.NoErr(eg.Wait())
24 | sub.Close()
25 | }
26 |
27 | func TestCloseTwice(t *testing.T) {
28 | ps := pubsub.New()
29 | sub := ps.Subscribe("toast")
30 | sub.Close()
31 | sub.Close()
32 | }
33 |
34 | func TestSubTwice(t *testing.T) {
35 | ps := pubsub.New()
36 | sub := ps.Subscribe("toast")
37 | ps.Publish("toast", nil)
38 | <-sub.Wait()
39 | sub.Close()
40 | sub = ps.Subscribe("toast")
41 | select {
42 | case <-sub.Wait():
43 | t.Fatal("lingering event")
44 | default:
45 | }
46 | sub.Close()
47 | }
48 |
--------------------------------------------------------------------------------
/internal/sig/sig.go:
--------------------------------------------------------------------------------
1 | package sig
2 |
3 | import (
4 | "context"
5 | "os"
6 | "os/signal"
7 | )
8 |
9 | // Trap cancels the context based on a signal
10 | func Trap(ctx context.Context, signals ...os.Signal) context.Context {
11 | ret, cancel := context.WithCancel(ctx)
12 | ch := make(chan os.Signal, len(signals))
13 | go func() {
14 | <-ch
15 | signal.Stop(ch)
16 | cancel()
17 | }()
18 | signal.Notify(ch, signals...)
19 | return ret
20 | }
21 |
--------------------------------------------------------------------------------
/internal/targz/all_test.go:
--------------------------------------------------------------------------------
1 | package targz
2 |
3 | import (
4 | "io/fs"
5 | "testing"
6 | "testing/fstest"
7 | "time"
8 |
9 | "github.com/livebud/bud/internal/is"
10 | )
11 |
12 | var modTime = time.Date(2021, 12, 31, 0, 0, 0, 0, time.UTC)
13 |
14 | func TestZipUnzip(t *testing.T) {
15 | is := is.New(t)
16 | gzip, err := Zip(fstest.MapFS{
17 | "a.go": &fstest.MapFile{Data: []byte("package a")},
18 | "b/c/d.go": &fstest.MapFile{Data: []byte("package d"), Mode: 0644, ModTime: modTime},
19 | "e": &fstest.MapFile{Mode: fs.ModeDir, ModTime: modTime},
20 | })
21 | is.NoErr(err)
22 | is.True(len(gzip) != 0)
23 | fsys, err := Unzip(gzip)
24 | is.NoErr(err)
25 | data, err := fs.ReadFile(fsys, "a.go")
26 | is.NoErr(err)
27 | is.Equal(string(data), "package a")
28 | fi, err := fs.Stat(fsys, "b/c/d.go")
29 | is.NoErr(err)
30 | is.Equal(fi.Mode(), fs.FileMode(0644))
31 | is.True(fi.ModTime().Equal(modTime))
32 | data, err = fs.ReadFile(fsys, "b/c/d.go")
33 | is.NoErr(err)
34 | is.Equal(string(data), "package d")
35 | fi, err = fs.Stat(fsys, "e")
36 | is.NoErr(err)
37 | is.Equal(fi.Mode(), fs.ModeDir)
38 | is.True(fi.ModTime().Equal(modTime))
39 | }
40 |
--------------------------------------------------------------------------------
/internal/targz/unzip.go:
--------------------------------------------------------------------------------
1 | package targz
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "compress/gzip"
7 | "errors"
8 | "io"
9 | "io/fs"
10 | "testing/fstest"
11 | )
12 |
13 | func Unzip(b []byte) (fs.FS, error) {
14 | gr, err := gzip.NewReader(bytes.NewBuffer(b))
15 | if err != nil {
16 | return nil, err
17 | }
18 | tr := tar.NewReader(gr)
19 | fsys := fstest.MapFS{}
20 | for {
21 | header, err := tr.Next()
22 | if err != nil {
23 | if errors.Is(err, io.EOF) {
24 | break
25 | }
26 | return nil, err
27 | }
28 | fi := header.FileInfo()
29 | fsys[header.Name] = &fstest.MapFile{
30 | Mode: fi.Mode(),
31 | ModTime: fi.ModTime(),
32 | Sys: fi.Sys(),
33 | }
34 | data, err := io.ReadAll(tr)
35 | if err != nil {
36 | return nil, err
37 | }
38 | fsys[header.Name].Data = data
39 | }
40 | // Close the gzip reader
41 | if err := gr.Close(); err != nil {
42 | return nil, err
43 | }
44 | return fsys, nil
45 | }
46 |
--------------------------------------------------------------------------------
/internal/targz/zip.go:
--------------------------------------------------------------------------------
1 | package targz
2 |
3 | import (
4 | "archive/tar"
5 | "bytes"
6 | "compress/gzip"
7 | "io"
8 | "io/fs"
9 | )
10 |
11 | func Zip(fsys fs.FS) ([]byte, error) {
12 | b := new(bytes.Buffer)
13 | zw := gzip.NewWriter(b)
14 | tw := tar.NewWriter(zw)
15 | // Walk the directory
16 | err := fs.WalkDir(fsys, ".", func(path string, de fs.DirEntry, err error) error {
17 | if err != nil {
18 | return err
19 | }
20 | fi, err := de.Info()
21 | if err != nil {
22 | return err
23 | }
24 | header, err := tar.FileInfoHeader(fi, path)
25 | if err != nil {
26 | return err
27 | }
28 | // Override to use the full path name
29 | header.Name = path
30 | if err = tw.WriteHeader(header); err != nil {
31 | return err
32 | }
33 | // Don't try reading from directories or symlinks
34 | if de.IsDir() || fi.Mode()&fs.ModeSymlink != 0 {
35 | return nil
36 | }
37 | file, err := fsys.Open(path)
38 | if err != nil {
39 | return err
40 | }
41 | defer file.Close()
42 | if _, err := io.Copy(tw, file); err != nil {
43 | return err
44 | }
45 | return nil
46 | })
47 | if err != nil {
48 | return nil, err
49 | }
50 | // Close the tar writer
51 | if err := tw.Close(); err != nil {
52 | return nil, err
53 | }
54 | // Close the gzip writer
55 | if err := zw.Close(); err != nil {
56 | return nil, err
57 | }
58 | return b.Bytes(), nil
59 | }
60 |
--------------------------------------------------------------------------------
/internal/terminal/html.go:
--------------------------------------------------------------------------------
1 | package terminal
2 |
3 | import (
4 | "bytes"
5 | _ "embed"
6 |
7 | terminal "github.com/buildkite/terminal-to-html"
8 | )
9 |
10 | // Pre tag
11 | func Pre(data []byte) []byte {
12 | var b bytes.Buffer
13 | b.WriteString(``)
14 | b.Write(terminal.Render(data))
15 | b.WriteString(`
`)
16 | return b.Bytes()
17 | }
18 |
19 | //go:embed terminal.css
20 | var css []byte
21 |
22 | // CSS that needs to be rendered
23 | func CSS() []byte {
24 | return css
25 | }
26 |
--------------------------------------------------------------------------------
/internal/testsub/testsub.go:
--------------------------------------------------------------------------------
1 | package testsub
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "strings"
7 | "testing"
8 | )
9 |
10 | func Run(t testing.TB, parent func(t testing.TB, cmd *exec.Cmd), child func(t testing.TB)) {
11 | if value := os.Getenv("CHILD"); value != "" {
12 | child(t)
13 | return
14 | }
15 | var args []string
16 | for _, arg := range os.Args[1:] {
17 | if strings.HasPrefix(arg, "-test.count=") ||
18 | strings.HasPrefix(arg, "-test.v") ||
19 | strings.HasPrefix(arg, "-test.run") {
20 | continue
21 | }
22 | args = append(args, arg)
23 | }
24 | cmd := exec.Command(os.Args[0], append(args, "-test.v=true", "-test.run=^"+t.Name()+"$")...)
25 | cmd.Env = append(os.Environ(), "CHILD=1")
26 | parent(t, cmd)
27 | }
28 |
--------------------------------------------------------------------------------
/internal/testsub/testsub_test.go:
--------------------------------------------------------------------------------
1 | package testsub_test
2 |
3 | import (
4 | "io"
5 | "os"
6 | "os/exec"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/livebud/bud/internal/is"
11 | "github.com/livebud/bud/internal/testsub"
12 | )
13 |
14 | func TestRun(t *testing.T) {
15 | is := is.New(t)
16 | parent := func(t testing.TB, cmd *exec.Cmd) {
17 | cmd.Stdin = strings.NewReader("hello")
18 | is.NoErr(cmd.Start())
19 | is.NoErr(cmd.Wait())
20 | }
21 | child := func(t testing.TB) {
22 | is := is.New(t)
23 | buf, err := io.ReadAll(os.Stdin)
24 | is.NoErr(err)
25 | is.Equal(string(buf), "hello")
26 | }
27 | testsub.Run(t, parent, child)
28 | }
29 |
30 | func TestRunError(t *testing.T) {
31 | is := is.New(t)
32 | parent := func(t testing.TB, cmd *exec.Cmd) {
33 | cmd.Stdin = strings.NewReader("hello")
34 | is.NoErr(cmd.Start())
35 | err := cmd.Wait()
36 | is.True(err != nil)
37 | is.Equal(err.Error(), "exit status 1")
38 | }
39 | child := func(t testing.TB) {
40 | is := is.New(t)
41 | buf, err := io.ReadAll(os.Stdin)
42 | is.NoErr(err)
43 | is.Equal(string(buf), "helloz")
44 | }
45 | testsub.Run(t, parent, child)
46 | }
47 |
--------------------------------------------------------------------------------
/internal/txtar/testdata/one.txt:
--------------------------------------------------------------------------------
1 | -- a.go --
2 | package a
3 |
4 | -- b/b.go --
5 | package b
6 |
7 | -- c/c/c.txt --
8 | c
9 |
--------------------------------------------------------------------------------
/internal/txtar/txtar.go:
--------------------------------------------------------------------------------
1 | package txtar
2 |
3 | import (
4 | "github.com/livebud/bud/package/vfs"
5 | "golang.org/x/tools/txtar"
6 | )
7 |
8 | // ParseFile parse a txtar file into a virtual filesystem. Used for tests
9 | func ParseFile(path string) (vfs.Memory, error) {
10 | archive, err := txtar.ParseFile(path)
11 | if err != nil {
12 | return nil, err
13 | }
14 | memory := vfs.Memory{}
15 | for _, file := range archive.Files {
16 | memory[file.Name] = &vfs.File{Data: file.Data}
17 | }
18 | return memory, nil
19 | }
20 |
--------------------------------------------------------------------------------
/internal/txtar/txtar_test.go:
--------------------------------------------------------------------------------
1 | package txtar_test
2 |
3 | import (
4 | "io/fs"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/txtar"
9 | )
10 |
11 | func TestParseFile(t *testing.T) {
12 | is := is.New(t)
13 | fsys, err := txtar.ParseFile("testdata/one.txt")
14 | is.NoErr(err)
15 | des, err := fs.ReadDir(fsys, ".")
16 | is.NoErr(err)
17 | is.Equal(len(des), 3)
18 | is.Equal(des[0].Name(), "a.go")
19 | is.Equal(des[1].Name(), "b")
20 | is.Equal(des[2].Name(), "c")
21 | code, err := fs.ReadFile(fsys, "a.go")
22 | is.NoErr(err)
23 | is.Equal(string(code), "package a\n\n")
24 | code, err = fs.ReadFile(fsys, "b/b.go")
25 | is.NoErr(err)
26 | is.Equal(string(code), "package b\n\n")
27 | code, err = fs.ReadFile(fsys, "c/c/c.txt")
28 | is.NoErr(err)
29 | is.Equal(string(code), "c\n")
30 | }
31 |
--------------------------------------------------------------------------------
/internal/urlx/parse.peg:
--------------------------------------------------------------------------------
1 | package urlx
2 |
3 | type parser Peg {
4 | url uri
5 | }
6 |
7 | URL <- URI
8 | / Path
9 | / Scheme
10 | / Host
11 | / OnlyPort
12 | End
13 |
14 | URI <- < Scheme '//' Host Path? > {
15 | p.url.uri = text
16 | }
17 |
18 | Scheme <- < [a-zA-Z][a-zA-Z+0-9]* ':' > {
19 | p.url.scheme = text[:len(text)-1]
20 | }
21 |
22 | Host <- IPPort / HostNamePort / IPV4 / HostName / BracketsPort / Brackets
23 |
24 | IPPort <- IP ':' Port
25 | HostNamePort <- HostName ':' Port
26 | BracketsPort <- Brackets ':' Port
27 |
28 | IP <- IPV4
29 |
30 | IPV4 <- < [0-9]+ '.' [0-9]+ '.' [0-9]+ '.' [0-9]+ > {
31 | p.url.host = text
32 | }
33 |
34 | HostName <- < [a-zA-Z][a-zA-Z0-9]* > {
35 | p.url.host = text
36 | }
37 |
38 | OnlyPort <- ':' Port / Port
39 |
40 | Port <- < [0-9]+ > {
41 | p.url.port = text
42 | }
43 |
44 | Path <- RelPath / AbsPath
45 |
46 | RelPath <- < '.' '/' .* > {
47 | p.url.path = text
48 | }
49 |
50 | AbsPath <- < '/' .* > {
51 | p.url.path = text
52 | }
53 |
54 | Brackets <- '[::]' {
55 | p.url.host = "[::]"
56 | }
57 |
58 | End
59 | <- !.
--------------------------------------------------------------------------------
/internal/versions/align_test.go:
--------------------------------------------------------------------------------
1 | package versions_test
2 |
3 | import (
4 | "context"
5 | "io/fs"
6 | "testing"
7 |
8 | "github.com/livebud/bud/internal/is"
9 | "github.com/livebud/bud/internal/versions"
10 | "github.com/livebud/bud/package/gomod"
11 | "github.com/livebud/bud/package/testdir"
12 | )
13 |
14 | func TestAlignRuntime(t *testing.T) {
15 | is := is.New(t)
16 | ctx := context.Background()
17 | td, err := testdir.Load()
18 | is.NoErr(err)
19 | td.Modules["github.com/livebud/bud"] = "v0.1.7"
20 | is.NoErr(td.Write(ctx))
21 | module, err := gomod.Find(td.Directory())
22 | is.NoErr(err)
23 | err = versions.AlignRuntime(ctx, module, "0.1.8")
24 | is.NoErr(err)
25 | modFile, err := fs.ReadFile(td, "go.mod")
26 | is.NoErr(err)
27 | module, err = gomod.Parse("go.mod", modFile)
28 | is.NoErr(err)
29 | version := module.File().Require("github.com/livebud/bud")
30 | is.Equal(version.Version, "v0.1.8")
31 | }
32 |
--------------------------------------------------------------------------------
/internal/versions/check.go:
--------------------------------------------------------------------------------
1 | package versions
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "golang.org/x/mod/semver"
8 | )
9 |
10 | const minGoVersion = "v1.17"
11 |
12 | // ErrMinGoVersion error is returned when Bud needs a newer version of Go
13 | var ErrMinGoVersion = fmt.Errorf("bud requires Go %s or later", minGoVersion)
14 |
15 | // CheckGo checks if the current version of Go is greater than the
16 | // minimum required Go version.
17 | func CheckGo(currentVersion string) error {
18 | currentVersion = "v" + strings.TrimPrefix(currentVersion, "go")
19 | // If we encounter an invalid version, it's probably a development version of
20 | // Go. We'll let those pass through. Reference:
21 | // https://github.com/golang/go/blob/3cf79d96105d890d7097d274804644b2a2093df1/src/runtime/extern.go#L273-L275
22 | if !semver.IsValid(currentVersion) {
23 | return nil
24 | }
25 | if semver.Compare(currentVersion, minGoVersion) < 0 {
26 | return ErrMinGoVersion
27 | }
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/internal/versions/check_test.go:
--------------------------------------------------------------------------------
1 | package versions_test
2 |
3 | import (
4 | "errors"
5 | "testing"
6 |
7 | "github.com/livebud/bud/internal/is"
8 | "github.com/livebud/bud/internal/versions"
9 | )
10 |
11 | func TestGoVersion(t *testing.T) {
12 | is := is.New(t)
13 | is.NoErr(versions.CheckGo("go1.17"))
14 | is.NoErr(versions.CheckGo("go1.18"))
15 | is.True(errors.Is(versions.CheckGo("go1.16"), versions.ErrMinGoVersion))
16 | is.True(errors.Is(versions.CheckGo("go1.16.5"), versions.ErrMinGoVersion))
17 | is.True(errors.Is(versions.CheckGo("go1.8"), versions.ErrMinGoVersion))
18 | is.NoErr(versions.CheckGo("abc123"))
19 | }
20 |
--------------------------------------------------------------------------------
/internal/versions/versions.go:
--------------------------------------------------------------------------------
1 | package versions
2 |
3 | // Bud gets changed at link time using ldflags.
4 | var Bud = "latest"
5 |
6 | // Svelte version used and tested across bud.
7 | const Svelte = "3.47.0"
8 |
9 | // React version used and tested across bud.
10 | // Currently not fully baked in.
11 | const React = "18.0.0"
12 |
--------------------------------------------------------------------------------
/livebud/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "livebud",
3 | "description": "JS Runtime for Bud",
4 | "version": "0.0.0",
5 | "private": true,
6 | "type": "module",
7 | "keywords": [
8 | "livebud",
9 | "bud",
10 | "web",
11 | "framework"
12 | ],
13 | "author": "Matthew Mueller ",
14 | "url": "https://livebud.com",
15 | "license": "MIT",
16 | "dependencies": {
17 | "react": "18.0.0",
18 | "react-dom": "18.0.0",
19 | "svelte": "3.47.0"
20 | },
21 | "devDependencies": {
22 | "@types/mocha": "9.1.0",
23 | "@types/node": "17.0.24",
24 | "@types/react": "18.0.0",
25 | "@types/react-dom": "18.0.0",
26 | "internal": "2.0.27",
27 | "mocha": "9.2.2",
28 | "ts-eager": "2.0.2",
29 | "typescript": "4.3.4"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/livebud/qs/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Imports
3 | */
4 |
5 | import { ParsedUrlQuery } from "querystring"
6 |
7 | /**
8 | * Simple query string parser.
9 | *
10 | * @param {String} query The query string that needs to be parsed.
11 | * @returns {Query}
12 | * @api public
13 | */
14 |
15 | export function parse(query: string): ParsedUrlQuery {
16 | if (!query) return {}
17 | const parser = /([^=?&]+)=?([^&]*)/g
18 | const result: ParsedUrlQuery = {}
19 | let part
20 |
21 | // Little nifty parsing hack, leverage the fact that RegExp.exec
22 | // increments the lastIndex property so we can continue executing
23 | // this loop until we've parsed all results.
24 | while ((part = parser.exec(query))) {
25 | /** @type {string|boolean} */
26 | const val = decodeComponent(part[2])
27 | const key = decodeComponent(part[1])
28 |
29 | // support arrays (item=a&item=b)
30 | let existing = result[key]
31 | if (typeof existing !== "undefined") {
32 | result[key] = ([] as string[]).concat(existing, val)
33 | continue
34 | }
35 | // Add query to result
36 | result[key] = val
37 | }
38 |
39 | return result
40 | }
41 |
42 | /**
43 | * Decode the URI component
44 | */
45 |
46 | function decodeComponent(input: string): string {
47 | return decodeURIComponent(input.replace(/\+/g, " "))
48 | }
49 |
--------------------------------------------------------------------------------
/livebud/qs/index_test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Imports
3 | */
4 |
5 | import { ParsedUrlQuery } from "querystring"
6 | import assert from "internal/assert"
7 | import { parse } from "."
8 |
9 | describe("qs/parse", () => {
10 | tests().forEach((test) => {
11 | it(test.title || test.input, () => {
12 | assert.deepEqual(parse(test.input), test.expected)
13 | })
14 | })
15 | })
16 |
17 | /**
18 | * Test type definition
19 | */
20 |
21 | type Test = {
22 | title?: string
23 | input: string
24 | expected: ParsedUrlQuery
25 | }
26 |
27 | /**
28 | * Tests
29 | */
30 |
31 | function tests(): Test[] {
32 | return [
33 | { input: "", expected: {}, title: "empty" },
34 | { input: "name&species", expected: { name: "", species: "" } },
35 | { input: "name=false", expected: { name: "false" } },
36 | {
37 | input: "name=tobi&species=ferret",
38 | expected: { name: "tobi", species: "ferret" },
39 | },
40 | {
41 | input: "?names=friends+and+family",
42 | expected: { names: "friends and family" },
43 | },
44 | {
45 | input: "?name=tobi&species=ferret",
46 | expected: { name: "tobi", species: "ferret" },
47 | },
48 | {
49 | input: "items=1&items=2&items=3&key=a",
50 | expected: { items: ["1", "2", "3"], key: "a" },
51 | },
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/livebud/runtime/index.ts:
--------------------------------------------------------------------------------
1 | import Hot from "./hot"
2 |
3 | export type HydrateInput> = {
4 | page: any
5 | frames: any[]
6 | error?: any
7 | props: Props
8 | target: HTMLElement | null
9 | }
10 |
11 | type Hydrate> = (input: HydrateInput) => void
12 |
13 | /**
14 | * Mount function
15 | */
16 |
17 | type MountInput = {
18 | components: Record
19 | page: string
20 | frames: string[]
21 | target: HTMLElement
22 | error?: string
23 | createView: Hydrate
24 | hot?: Hot
25 | }
26 |
27 | export function mount(input: MountInput): void {
28 | const props = getProps(document.getElementById("bud_props"))
29 | input.createView({
30 | page: input.components[input.page],
31 | frames: input.frames.map((frame) => input.components[frame]),
32 | error: input.error ? input.components[input.error] : undefined,
33 | target: input.target,
34 | props: props,
35 | })
36 | if (input.hot) {
37 | input.hot.listen(() => {
38 | input.createView({
39 | page: input.components[input.page],
40 | frames: input.frames.map((frame) => input.components[frame]),
41 | error: input.error ? input.components[input.error] : undefined,
42 | target: input.target,
43 | props: props,
44 | })
45 | })
46 | }
47 | }
48 |
49 | function getProps(node: HTMLElement | null) {
50 | if (!node || !node.textContent) {
51 | return {}
52 | }
53 | try {
54 | return JSON.parse(node.textContent)
55 | } catch (err) {
56 | return {}
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/livebud/runtime/jsx/index.ts:
--------------------------------------------------------------------------------
1 | import { HydrateInput } from ".."
2 | import ReactDOM from "react-dom"
3 | import React from "react"
4 |
5 | export default function createView(input: HydrateInput) {
6 | let component = React.createElement(input.page, input.props)
7 | for (let frame of input.frames) {
8 | component = React.createElement(frame, input.props, component)
9 | }
10 | ReactDOM.hydrate(component, input.target)
11 | }
12 |
--------------------------------------------------------------------------------
/livebud/runtime/svelte/index.ts:
--------------------------------------------------------------------------------
1 | import { HydrateInput } from ".."
2 |
3 | // TODO:
4 | // - Support frames
5 | // - Handle errors
6 | export default function createView(input: HydrateInput) {
7 | if (input.target != null) {
8 | // TODO: for some reason Svelte isn't able to re-hydrate over itself during
9 | // a live reload. I wonder if they've figured this out in SvelteKit, but you
10 | // end up with a runtime error: "Cannot read properties of null (reading
11 | // 'removeChild')". Encountering this error will depend on your Svelte code.
12 | // For now, we'll clear the DOM in our target before hydrating.
13 | input.target.innerHTML = ""
14 | }
15 | new input.page({
16 | target: input.target,
17 | props: input.props,
18 | hydrate: true,
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/livebud/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "esnext",
5 | "strict": true,
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | // Don't emit files when running tsc
11 | "noEmit": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "os"
7 |
8 | cli "github.com/livebud/bud/internal/cli"
9 | "github.com/livebud/bud/internal/errs"
10 | "github.com/livebud/bud/internal/once"
11 | "github.com/livebud/bud/package/log/console"
12 | )
13 |
14 | //go:generate go run scripts/set-package-json/main.go
15 |
16 | // main is bud's entrypoint
17 | func main() {
18 | ctx := context.Background()
19 | if err := run(ctx); err != nil {
20 | console.Error(errs.Format(err))
21 | os.Exit(1)
22 | }
23 | os.Exit(0)
24 | }
25 |
26 | // Run the CLI with the default configuration and return any resulting errors.
27 | func run(ctx context.Context) error {
28 | closer := new(once.Closer)
29 | defer closer.Close()
30 | // Initialize the CLI
31 | cli := cli.New(closer)
32 | // Run the cli
33 | if err := cli.Parse(ctx, os.Args[1:]...); err != nil {
34 | if errors.Is(err, context.Canceled) {
35 | return nil
36 | }
37 | return err
38 | }
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bud",
3 | "private": true,
4 | "devDependencies": {
5 | "@types/jsesc": "3.0.1",
6 | "@types/react": "18.0.0",
7 | "@types/react-dom": "18.0.0",
8 | "js-confetti": "0.10.2",
9 | "jsesc": "3.0.2",
10 | "react": "18.0.0",
11 | "react-dom": "18.0.0",
12 | "svelte": "3.47.0"
13 | }
14 | }
--------------------------------------------------------------------------------
/package/budhttp/discard.go:
--------------------------------------------------------------------------------
1 | package budhttp
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/livebud/bud/framework/view/ssr"
7 | )
8 |
9 | // Discard client implements Client
10 | type discard struct {
11 | }
12 |
13 | var _ Client = discard{}
14 |
15 | func (discard) Render(route string, props interface{}) (*ssr.Response, error) {
16 | return nil, fmt.Errorf("budhttp: discard client does not support render")
17 | }
18 |
19 | func (discard) Script(path, script string) error {
20 | return fmt.Errorf("budhttp: discard client does not support script")
21 | }
22 |
23 | func (discard) Eval(path, expression string) (string, error) {
24 | return "", fmt.Errorf("budhttp: discard client does not support eval")
25 | }
26 |
27 | // Publish nothing
28 | func (discard) Publish(topic string, data []byte) error {
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/package/commander/args.go:
--------------------------------------------------------------------------------
1 | package commander
2 |
3 | type Args struct {
4 | Name string
5 | value value
6 | }
7 |
8 | func (a *Args) Optional() *OptionalArgs {
9 | return &OptionalArgs{a}
10 | }
11 |
12 | func (a *Args) Strings(target *[]string) *Strings {
13 | *target = []string{}
14 | value := &Strings{target: target}
15 | a.value = &stringsValue{inner: value}
16 | return value
17 | }
18 |
19 | func (a *Args) verify(name string) error {
20 | return a.value.verify("<" + name + "...>")
21 | }
22 |
23 | type OptionalArgs struct {
24 | a *Args
25 | }
26 |
27 | func (a *OptionalArgs) Strings(target *[]string) *Strings {
28 | *target = []string{}
29 | value := &Strings{target: target, optional: true}
30 | a.a.value = &stringsValue{inner: value}
31 | return value
32 | }
33 |
--------------------------------------------------------------------------------
/package/commander/color.go:
--------------------------------------------------------------------------------
1 | package commander
2 |
3 | import (
4 | "os"
5 | "text/template"
6 | )
7 |
8 | var reset = color("\033[0m")
9 | var dim = color("\033[37m")
10 |
11 | var colors = template.FuncMap{
12 | "reset": reset,
13 | "bold": color("\033[1m"),
14 | "dim": dim,
15 | "underline": color("\033[4m"),
16 | "teal": color("\033[36m"),
17 | "blue": color("\033[34m"),
18 | "yellow": color("\033[33m"),
19 | "red": color("\033[31m"),
20 | "green": color("\033[32m"),
21 | }
22 |
23 | var nocolor = os.Getenv("NO_COLOR") != ""
24 |
25 | func color(code string) func() string {
26 | return func() string {
27 | if nocolor {
28 | return ""
29 | }
30 | return code
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/package/commander/custom.go:
--------------------------------------------------------------------------------
1 | package commander
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type Custom struct {
8 | target func(s string) error
9 | defval *string // default value
10 | }
11 |
12 | func (v *Custom) Default(value string) {
13 | v.defval = &value
14 | }
15 |
16 | func (v *Custom) Optional() {
17 | v.defval = new(string)
18 | }
19 |
20 | type customValue struct {
21 | inner *Custom
22 | set bool
23 | }
24 |
25 | var _ value = (*customValue)(nil)
26 |
27 | func (v *customValue) verify(displayName string) error {
28 | if v.set {
29 | return nil
30 | } else if v.inner.defval != nil {
31 | return v.inner.target(*v.inner.defval)
32 | }
33 | return fmt.Errorf("missing %s", displayName)
34 | }
35 |
36 | func (v *customValue) Set(val string) error {
37 | err := v.inner.target(val)
38 | v.set = true
39 | return err
40 | }
41 |
42 | func (v *customValue) String() string {
43 | if v.inner == nil {
44 | return ""
45 | } else if v.set {
46 | return ""
47 | } else if v.inner.defval != nil {
48 | return *v.inner.defval
49 | }
50 | return ""
51 | }
52 |
--------------------------------------------------------------------------------
/package/commander/stringmap.go:
--------------------------------------------------------------------------------
1 | package commander
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type StringMap struct {
9 | target *map[string]string
10 | defval *map[string]string // default value
11 | }
12 |
13 | func (v *StringMap) Default(value map[string]string) {
14 | v.defval = &value
15 | }
16 |
17 | func (v *StringMap) Optional() {
18 | v.defval = new(map[string]string)
19 | }
20 |
21 | type stringMapValue struct {
22 | inner *StringMap
23 | set bool
24 | }
25 |
26 | func (v *stringMapValue) verify(displayName string) error {
27 | if v.set {
28 | return nil
29 | } else if v.inner.defval != nil {
30 | *v.inner.target = *v.inner.defval
31 | return nil
32 | }
33 | return fmt.Errorf("missing %s", displayName)
34 | }
35 |
36 | func (v *stringMapValue) Set(val string) error {
37 | kv := strings.SplitN(val, ":", 2)
38 | if len(kv) != 2 {
39 | return fmt.Errorf("invalid key:value pair for %q", val)
40 | }
41 | if *v.inner.target == nil {
42 | *v.inner.target = map[string]string{}
43 | }
44 | (*v.inner.target)[kv[0]] = kv[1]
45 | v.set = true
46 | return nil
47 | }
48 |
49 | func (v *stringMapValue) String() string {
50 | if v.inner == nil {
51 | return ""
52 | } else if v.set {
53 | return v.format(*v.inner.target)
54 | } else if v.inner.defval != nil {
55 | return v.format(*v.inner.defval)
56 | }
57 | return ""
58 | }
59 |
60 | // Format as a string
61 | func (v *stringMapValue) format(kv map[string]string) (out string) {
62 | i := 0
63 | for k, v := range kv {
64 | if i > 0 {
65 | out += " "
66 | }
67 | out += k + ":" + v
68 | i++
69 | }
70 | return out
71 | }
72 |
--------------------------------------------------------------------------------
/package/commander/strings.go:
--------------------------------------------------------------------------------
1 | package commander
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | type Strings struct {
9 | target *[]string
10 | defval *[]string // default value
11 | optional bool
12 | }
13 |
14 | func (v *Strings) Default(values ...string) {
15 | v.defval = &values
16 | }
17 |
18 | type stringsValue struct {
19 | inner *Strings
20 | set bool
21 | }
22 |
23 | func (v *stringsValue) verify(displayName string) error {
24 | if v.set {
25 | return nil
26 | } else if v.inner.defval != nil {
27 | *v.inner.target = *v.inner.defval
28 | return nil
29 | } else if v.inner.optional {
30 | return nil
31 | }
32 | return fmt.Errorf("missing %s", displayName)
33 | }
34 |
35 | func (v *stringsValue) Set(val string) error {
36 | *v.inner.target = append(*v.inner.target, val)
37 | v.set = true
38 | return nil
39 | }
40 |
41 | func (v *stringsValue) String() string {
42 | if v.inner == nil {
43 | return ""
44 | } else if v.set {
45 | return strings.Join(*v.inner.target, ", ")
46 | } else if v.inner.defval != nil {
47 | return strings.Join(*v.inner.defval, ", ")
48 | }
49 | return ""
50 | }
51 |
--------------------------------------------------------------------------------
/package/commander/usage.gotext:
--------------------------------------------------------------------------------
1 |
2 | {{bold}}Usage:{{reset}}
3 | $ {{ $.Name }} {{- if $.Usage }} {{ $.Usage }}{{ end }}
4 |
5 | {{- if $.Description}}
6 |
7 | {{bold}}Description:{{reset}}
8 | {{ $.Description }}
9 | {{- end }}
10 |
11 | {{- if $.Flags}}
12 |
13 | {{bold}}Flags:{{reset}}
14 | {{ $.Flags.Usage }}
15 | {{- end }}
16 |
17 | {{- if $.Commands }}
18 |
19 | {{bold}}Commands:{{reset}}
20 | {{ $.Commands.Usage }}
21 | {{- end }}
22 |
23 | {{- if $.Advanced }}
24 |
25 | {{bold}}Advanced Commands:{{reset}}
26 | {{ $.Advanced.Usage }}
27 | {{- end }}
28 |
29 |
--------------------------------------------------------------------------------
/package/di/alias.go:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/livebud/bud/package/parser"
7 | )
8 |
9 | func tryTypeAlias(alias *parser.Alias, dataType string) (*typeAlias, error) {
10 | if alias.Private() {
11 | return nil, ErrNoMatch
12 | }
13 | if strings.TrimPrefix(dataType, "*") != alias.Name() {
14 | return nil, ErrNoMatch
15 | }
16 | aliasType := alias.Type()
17 | decl, err := parser.Definition(aliasType)
18 | if err != nil {
19 | return nil, err
20 | }
21 | importPath, err := decl.Package().Import()
22 | if err != nil {
23 | return nil, err
24 | }
25 | to := &Type{
26 | Import: importPath,
27 | Type: decl.Name(),
28 | kind: decl.Kind(),
29 | module: decl.Package().Module(),
30 | }
31 | return &typeAlias{
32 | Import: importPath,
33 | Name: alias.Name(),
34 | Type: to,
35 | }, nil
36 | }
37 |
38 | // typeAlias is a declaration that can provide a dependency
39 | type typeAlias struct {
40 | Import string
41 | Name string
42 | Type *Type
43 | }
44 |
45 | var _ Declaration = (*typeAlias)(nil)
46 |
47 | func (a *typeAlias) ID() string {
48 | return `'` + a.Import + `'.` + a.Name
49 | }
50 |
51 | func (a *typeAlias) Dependencies() []Dependency {
52 | return []Dependency{a.Type}
53 | }
54 |
55 | func (a *typeAlias) Generate(gen Generator, inputs []*Variable) (outputs []*Variable) {
56 | return inputs
57 | }
58 |
--------------------------------------------------------------------------------
/package/di/di.go:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import "github.com/livebud/bud/package/parser"
4 |
5 | type Aliases map[Dependency]Dependency
6 |
7 | type Dependency interface {
8 | ID() string
9 | ImportPath() string
10 | TypeName() string
11 | Find(Finder) (Declaration, error)
12 | }
13 |
14 | func getID(importPath, typeName string) string {
15 | return `'` + importPath + `'.` + typeName
16 | }
17 |
18 | type Generator interface {
19 | WriteString(code string) (n int, err error)
20 | Identifier(importPath, name string) string
21 | Variable(importPath, name string) string
22 | MarkError(hasError bool)
23 | }
24 |
25 | type Variable struct {
26 | Import string // Import path
27 | Type string // Type of the variable
28 | Name string // Name of the variable
29 | Kind parser.Kind // Kind of type (struct, interface, etc.)
30 | }
31 |
32 | func (v *Variable) ID() string {
33 | return getID(v.Import, v.Type)
34 | }
35 |
36 | type External struct {
37 | Variable *Variable
38 | Key string // Name to be used as a key in a struct
39 | Hoisted bool // True if this external was hoisted up
40 | FullType string // Type name including package name
41 | }
42 |
43 | type Declaration interface {
44 | ID() string
45 | Dependencies() []Dependency
46 | Generate(gen Generator, inputs []*Variable) (outputs []*Variable)
47 | }
48 |
49 | // Check if the field or variable is an interface
50 | func isInterface(k parser.Kind) bool {
51 | return k == parser.KindInterface
52 | }
53 |
--------------------------------------------------------------------------------
/package/di/error.go:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import "github.com/livebud/bud/package/parser"
4 |
5 | // Error type
6 | type Error struct {
7 | }
8 |
9 | var _ Dependency = (*Error)(nil)
10 |
11 | func (*Error) ID() string {
12 | return "error"
13 | }
14 |
15 | func (*Error) ImportPath() string {
16 | return ""
17 | }
18 |
19 | func (*Error) TypeName() string {
20 | return "error"
21 | }
22 |
23 | func (e *Error) Find(Finder) (Declaration, error) {
24 | return e, nil
25 | }
26 |
27 | func (*Error) Dependencies() (deps []Dependency) {
28 | return deps
29 | }
30 |
31 | func (*Error) Generate(gen Generator, inputs []*Variable) (outputs []*Variable) {
32 | return append(outputs, &Variable{
33 | Import: "", // error doesn't have an import
34 | Name: "err",
35 | Type: "error",
36 | Kind: parser.KindInterface,
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/package/di/hoist.go:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | // Hoist the nodes that don't depend on the external nodes and turn these
4 | // nodes into external nodes. This allows for dependencies that don't depend
5 | // on externals to be initialized once, rather than each time the generated
6 | // function is called.
7 | //
8 | // Start with hoisting true, but if we encounter any external along the way, the
9 | // hoisting of all children becomes false
10 | func Hoist(root *Node) *Node {
11 | // Hoisting only applies to ancestor dependencies
12 | for _, result := range root.Dependencies {
13 | for _, dep := range result.Dependencies {
14 | hoist(dep)
15 | }
16 | }
17 | return root
18 | }
19 |
20 | func hoist(node *Node) (shouldHoist bool) {
21 | // Default to hoisting
22 | shouldHoist = true
23 | // Dependencies that rely on an external node cannot be hoisted.
24 | if node.External && !node.Hoist {
25 | return false
26 | }
27 | // Loop over the inputs. If any input is non-hoistable, this node is becomes
28 | // non-hoistable. Order of the conditional matters here. We intentionally call
29 | // hoist(dep) before shouldHoist because we don't want the algorithm skipping
30 | // over the subtrees.
31 | for _, dep := range node.Dependencies {
32 | shouldHoist = hoist(dep) && shouldHoist
33 | }
34 | // If shouldHoist is true, we externalize the node.
35 | node.Hoist = shouldHoist
36 | return shouldHoist
37 | }
38 |
--------------------------------------------------------------------------------
/package/di/type.go:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import (
4 | "github.com/livebud/bud/package/gomod"
5 | "github.com/livebud/bud/package/parser"
6 | )
7 |
8 | type Type struct {
9 | Import string
10 | Type string
11 |
12 | module *gomod.Module // Optional, defaults to project module
13 | kind parser.Kind // Kind of type (e.g. struct, interface, etc.)
14 | }
15 |
16 | var _ Dependency = (*Type)(nil)
17 |
18 | func (t *Type) ID() string {
19 | return getID(t.Import, t.Type)
20 | }
21 |
22 | func (t *Type) ImportPath() string {
23 | return t.Import
24 | }
25 |
26 | func (t *Type) TypeName() string {
27 | return t.Type
28 | }
29 |
30 | // Find a declaration that provides this type
31 | func (t *Type) Find(finder Finder) (Declaration, error) {
32 | return finder.Find(t.module, t)
33 | }
34 |
35 | func ToType(importPath, dataType string) *Type {
36 | return &Type{
37 | Import: importPath,
38 | Type: dataType,
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/package/es/importmap_test.go:
--------------------------------------------------------------------------------
1 | package es_test
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "testing"
7 |
8 | "github.com/livebud/bud/framework"
9 | "github.com/livebud/bud/internal/is"
10 | "github.com/livebud/bud/internal/versions"
11 | "github.com/livebud/bud/package/es"
12 | "github.com/livebud/bud/package/log/testlog"
13 | "github.com/livebud/bud/package/testdir"
14 | )
15 |
16 | func TestImportMap(t *testing.T) {
17 | is := is.New(t)
18 | ctx := context.Background()
19 | log := testlog.New()
20 | td, err := testdir.Load()
21 | is.NoErr(err)
22 | td.Files["view/index.js"] = `
23 | import { SvelteComponent } from 'svelte'
24 | import { create_component } from 'svelte/internal'
25 | export function createElement() { console.log(SvelteComponent, create_component) }
26 | `
27 | is.NoErr(td.Write(ctx))
28 | flag := &framework.Flag{}
29 | esb := es.New(flag, log)
30 | file, err := esb.Serve(&es.Serve{
31 | AbsDir: td.Directory(),
32 | Entry: "./view/index.js",
33 | Platform: es.DOM,
34 | Plugins: []es.Plugin{
35 | es.HTTP(http.DefaultClient),
36 | es.ImportMap(log, map[string]string{
37 | "svelte": "https://esm.run/svelte@" + versions.Svelte,
38 | "svelte/": "https://esm.run/svelte@" + versions.Svelte + "/",
39 | }),
40 | },
41 | })
42 | is.NoErr(err)
43 | code := string(file.Contents)
44 | is.In(code, `function createElement() {`)
45 | is.In(code, `"Component was already destroyed"`)
46 | is.In(code, `on_mount`)
47 | }
48 |
--------------------------------------------------------------------------------
/package/es/nodemodules.go:
--------------------------------------------------------------------------------
1 | package es
2 |
3 | import (
4 | "path"
5 |
6 | esbuild "github.com/evanw/esbuild/pkg/api"
7 | )
8 |
9 | // ExternalNodeModules externalizes node_modules
10 | func ExternalNodeModules(prefix string) esbuild.Plugin {
11 | return esbuild.Plugin{
12 | Name: "external_node_modules",
13 | Setup: func(epb esbuild.PluginBuild) {
14 | // Regexp doesn't start with "." or "/" or "\"
15 | epb.OnResolve(esbuild.OnResolveOptions{Filter: `^[^\.\/\\]`}, func(args esbuild.OnResolveArgs) (result esbuild.OnResolveResult, err error) {
16 | result.Path = path.Join(prefix, args.Path)
17 | result.External = true
18 | return result, nil
19 | })
20 | },
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/package/finder/finder.go:
--------------------------------------------------------------------------------
1 | package finder
2 |
3 | import (
4 | "errors"
5 | "io/fs"
6 |
7 | "github.com/livebud/bud/internal/glob"
8 | "github.com/livebud/bud/internal/orderedset"
9 | "github.com/livebud/bud/package/valid"
10 | )
11 |
12 | // Find files that match the pattern and are added as entries to the selector
13 | func Find(fsys fs.FS, pattern string, selector func(path string, isDir bool) (entries []string)) (matches []string, err error) {
14 | matcher, err := glob.Compile(pattern)
15 | if err != nil {
16 | return nil, err
17 | }
18 | bases, err := glob.Bases(pattern)
19 | if err != nil {
20 | return nil, err
21 | }
22 | // Compute the matches for each base
23 | for _, base := range bases {
24 | // Walk the directory tree, filtering out non-valid paths
25 | err = fs.WalkDir(fsys, base, valid.WalkDirFunc(func(path string, de fs.DirEntry, err error) error {
26 | if err != nil {
27 | if errors.Is(err, fs.ErrNotExist) {
28 | return nil
29 | }
30 | return err
31 | } else if !matcher.Match(path) {
32 | return nil
33 | }
34 | matched := selector(path, de.IsDir())
35 | if len(matched) == 0 {
36 | return nil
37 | }
38 | // Ensure all matches paths exist
39 | if _, err := fs.Stat(fsys, path); err != nil {
40 | if errors.Is(err, fs.ErrNotExist) {
41 | return nil
42 | }
43 | return err
44 | }
45 | matches = append(matches, matched...)
46 | return nil
47 | }))
48 | if err != nil {
49 | if errors.Is(err, fs.ErrNotExist) {
50 | continue
51 | }
52 | return nil, err
53 | }
54 | }
55 | return orderedset.Strings(matches...), nil
56 | }
57 |
--------------------------------------------------------------------------------
/package/genfs/direntry.go:
--------------------------------------------------------------------------------
1 | package genfs
2 |
3 | import (
4 | "io/fs"
5 | "sort"
6 |
7 | "github.com/livebud/bud/internal/once"
8 | )
9 |
10 | func newDirEntrySet() *dirEntrySet {
11 | return &dirEntrySet{
12 | seen: map[string]struct{}{},
13 | }
14 | }
15 |
16 | // dirEntrySet is an ordered set of directory entries.
17 | type dirEntrySet struct {
18 | seen map[string]struct{}
19 | entries []fs.DirEntry
20 | }
21 |
22 | func (s *dirEntrySet) Add(entry fs.DirEntry) {
23 | name := entry.Name()
24 | if _, ok := s.seen[name]; ok {
25 | return
26 | }
27 | s.seen[name] = struct{}{}
28 | s.entries = append(s.entries, entry)
29 | }
30 |
31 | func (s *dirEntrySet) List() []fs.DirEntry {
32 | sort.Slice(s.entries, func(i, j int) bool {
33 | return s.entries[i].Name() < s.entries[j].Name()
34 | })
35 | return s.entries
36 | }
37 |
38 | func newDirEntry(genfs fs.FS, name string, mode fs.FileMode, path string) *dirEntry {
39 | return &dirEntry{
40 | genfs: genfs,
41 | name: name,
42 | mode: mode,
43 | path: path,
44 | statOnce: new(once.FileInfo),
45 | }
46 | }
47 |
48 | type dirEntry struct {
49 | genfs fs.FS
50 | name string
51 | mode fs.FileMode
52 | path string
53 | statOnce *once.FileInfo
54 | }
55 |
56 | var _ fs.DirEntry = (*dirEntry)(nil)
57 |
58 | func (d *dirEntry) Name() string {
59 | return d.name
60 | }
61 |
62 | func (d *dirEntry) Type() fs.FileMode {
63 | return d.mode
64 | }
65 |
66 | func (d *dirEntry) IsDir() bool {
67 | return d.mode.IsDir()
68 | }
69 |
70 | func (d *dirEntry) Info() (fs.FileInfo, error) {
71 | stat, err := d.statOnce.Do(func() (fs.FileInfo, error) { return fs.Stat(d.genfs, d.path) })
72 | if err != nil {
73 | return nil, err
74 | }
75 | return stat, nil
76 | }
77 |
--------------------------------------------------------------------------------
/package/genfs/embed.go:
--------------------------------------------------------------------------------
1 | package genfs
2 |
3 | type Embed struct {
4 | Data []byte
5 | }
6 |
7 | var _ FileGenerator = (*Embed)(nil)
8 |
9 | func (e *Embed) GenerateFile(fsys FS, file *File) error {
10 | file.Data = e.Data
11 | return nil
12 | }
13 |
--------------------------------------------------------------------------------
/package/genfs/external.go:
--------------------------------------------------------------------------------
1 | package genfs
2 |
3 | import (
4 | "io/fs"
5 |
6 | "github.com/livebud/bud/package/virtual"
7 | )
8 |
9 | type External struct {
10 | target string
11 | }
12 |
13 | func (e *External) Path() string {
14 | return e.target
15 | }
16 |
17 | func (e *External) Target() string {
18 | return e.target
19 | }
20 |
21 | func (e *External) Mode() fs.FileMode {
22 | return fs.FileMode(0)
23 | }
24 |
25 | type ExternalGenerator interface {
26 | GenerateExternal(fsys FS, file *External) error
27 | }
28 |
29 | type externalGenerator struct {
30 | cache Cache
31 | fn func(fsys FS, e *External) error
32 | genfs fs.FS
33 | path string
34 | }
35 |
36 | func (e *externalGenerator) Generate(target string) (fs.File, error) {
37 | if target != e.path {
38 | return nil, formatError(fs.ErrNotExist, "%q path doesn't match %q target", e.path, target)
39 | }
40 | if _, err := e.cache.Get(target); err == nil {
41 | return nil, fs.ErrNotExist
42 | }
43 | scoped := &scopedFS{e.cache, e.genfs, target}
44 | file := &External{target}
45 | if err := e.fn(scoped, file); err != nil {
46 | return nil, err
47 | }
48 | vfile := &virtual.File{
49 | Path: target,
50 | Mode: file.Mode(),
51 | }
52 | if err := e.cache.Set(target, vfile); err != nil {
53 | return nil, err
54 | }
55 | return nil, fs.ErrNotExist
56 | }
57 |
--------------------------------------------------------------------------------
/package/genfs/fileserver.go:
--------------------------------------------------------------------------------
1 | package genfs
2 |
3 | import (
4 | "io/fs"
5 |
6 | "github.com/livebud/bud/package/virtual"
7 | )
8 |
9 | type FileServer interface {
10 | ServeFile(fsys FS, file *File) error
11 | }
12 |
13 | type ServeFile func(fsys FS, file *File) error
14 |
15 | func (fn ServeFile) ServeFile(fsys FS, file *File) error {
16 | return fn(fsys, file)
17 | }
18 |
19 | type fileServer struct {
20 | cache Cache
21 | fn func(fsys FS, file *File) error
22 | genfs fs.FS
23 | path string
24 | }
25 |
26 | var _ generator = (*fileServer)(nil)
27 |
28 | func (f *fileServer) Generate(target string) (fs.File, error) {
29 | if file, err := f.cache.Get(target); err == nil {
30 | return virtual.Open(file), nil
31 | }
32 | // Always return an empty directory if we request the root
33 | if f.path == target {
34 | return virtual.Open(&virtual.File{
35 | Path: f.path,
36 | Mode: fs.ModeDir,
37 | }), nil
38 | }
39 | scopedFS := &scopedFS{f.cache, f.genfs, target}
40 | file := &File{nil, f.path, target}
41 | // g.fsys.log.Fields(log.Fields{
42 | // "target": target,
43 | // "path": g.node.Path(),
44 | // }).Debug("budfs: running file server function")
45 | if err := f.fn(scopedFS, file); err != nil {
46 | return nil, err
47 | }
48 | vfile := &virtual.File{
49 | Path: target,
50 | Mode: fs.FileMode(0),
51 | Data: file.Data,
52 | }
53 | if err := f.cache.Set(target, vfile); err != nil {
54 | return nil, err
55 | }
56 | return virtual.Open(vfile), nil
57 | }
58 |
--------------------------------------------------------------------------------
/package/genfs/mode.go:
--------------------------------------------------------------------------------
1 | package genfs
2 |
3 | import "io/fs"
4 |
5 | type mode uint8
6 |
7 | const (
8 | modeDir mode = 1 << iota
9 | modeGen
10 | )
11 |
12 | const modeGenDir = modeGen | modeDir
13 |
14 | func (m mode) IsDir() bool {
15 | return m&modeDir != 0
16 | }
17 |
18 | func (m mode) IsGen() bool {
19 | return m&modeGen != 0
20 | }
21 |
22 | func (m mode) FileMode() fs.FileMode {
23 | mode := fs.FileMode(0)
24 | if m.IsDir() {
25 | mode |= fs.ModeDir
26 | }
27 | return mode
28 | }
29 |
30 | func (m mode) String() string {
31 | var s string
32 | if m.IsDir() {
33 | s += "d"
34 | } else {
35 | s += "-"
36 | }
37 | if m.IsGen() {
38 | s += "g"
39 | } else {
40 | s += "-"
41 | }
42 | return s
43 | }
44 |
--------------------------------------------------------------------------------
/package/gomod/goroot.go:
--------------------------------------------------------------------------------
1 | package gomod
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "runtime"
9 | "strings"
10 | )
11 |
12 | // This function is a heavily modified version of the following:
13 | // https://github.com/golang/go/blob/89044b6d423a07bea3b6f80210f780e859dd2700/src/cmd/go/internal/cfg/cfg.go#L369
14 | func findGoRoot() string {
15 | if env := os.Getenv("GOROOT"); env != "" {
16 | return filepath.Clean(env)
17 | }
18 | def := ""
19 | if r := runtime.GOROOT(); r != "" {
20 | def = filepath.Clean(r)
21 | }
22 | if runtime.Compiler == "gccgo" {
23 | // gccgo has no real GOROOT, and it certainly doesn't
24 | // depend on the executable's location.
25 | return def
26 | }
27 | // Run `go env GOROOT` to compute the GOROOT.
28 | // TODO: this takes about 8ms on boot, see if we can find a better way to
29 | // compute the correct GOROOT.
30 | // Note: The tricky part is making it work with --trimpath.
31 | cmd := exec.Command("go", "env", "GOROOT")
32 | stdout := new(bytes.Buffer)
33 | cmd.Stdout = stdout
34 | if err := cmd.Run(); err != nil {
35 | return def
36 | }
37 | return strings.TrimSpace(stdout.String())
38 | }
39 |
--------------------------------------------------------------------------------
/package/gotemplate/gotemplate.go:
--------------------------------------------------------------------------------
1 | package gotemplate
2 |
3 | import (
4 | "bytes"
5 | "go/format"
6 | "text/template"
7 | )
8 |
9 | type Template interface {
10 | Generate(state interface{}) ([]byte, error)
11 | }
12 |
13 | // MustParse panics if unable to parse
14 | func MustParse(name, code string) Template {
15 | template, err := Parse(name, code)
16 | if err != nil {
17 | panic(err)
18 | }
19 | return template
20 | }
21 |
22 | // Parse parses Go code
23 | func Parse(name, code string) (Template, error) {
24 | tpl, err := template.New(name).Parse(code)
25 | if err != nil {
26 | return nil, err
27 | }
28 | return &gotemplate{name, tpl}, nil
29 | }
30 |
31 | // Template struct
32 | type gotemplate struct {
33 | name string
34 | tpl *template.Template
35 | }
36 |
37 | // Name returns the name of the template
38 | func (t *gotemplate) Name() string {
39 | return t.name
40 | }
41 |
42 | // Generate the code
43 | func (t *gotemplate) Generate(state interface{}) ([]byte, error) {
44 | buf := new(bytes.Buffer)
45 | if err := t.tpl.Execute(buf, state); err != nil {
46 | return nil, err
47 | }
48 | if val, err := format.Source(buf.Bytes()); err == nil {
49 | return val, nil
50 | }
51 | return buf.Bytes(), nil
52 | }
53 |
--------------------------------------------------------------------------------
/package/gotemplate/gotemplate_test.go:
--------------------------------------------------------------------------------
1 | package gotemplate_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/is"
7 | "github.com/livebud/bud/package/gotemplate"
8 | )
9 |
10 | func TestGenerateGoFile(t *testing.T) {
11 | is := is.New(t)
12 | template := `package main
13 |
14 | func main() {
15 | println("{{ .name }}")
16 | }`
17 | expect := `package main
18 |
19 | func main() {
20 | println("jason")
21 | }
22 | `
23 | generator := gotemplate.MustParse("test.gotext", template)
24 | b, err := generator.Generate(map[string]string{"name": "jason"})
25 | is.NoErr(err)
26 | is.Equal(string(b), expect)
27 | }
28 |
29 | func TestGenerateFreeText(t *testing.T) {
30 | is := is.New(t)
31 | template := `Hi {{ .name }}`
32 | expect := `Hi Kim`
33 | generator := gotemplate.MustParse("test.gotext", template)
34 | b, err := generator.Generate(map[string]string{"name": "Kim"})
35 | is.NoErr(err)
36 | is.Equal(string(b), expect)
37 | }
38 |
--------------------------------------------------------------------------------
/package/imports/imports_test.go:
--------------------------------------------------------------------------------
1 | package imports_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/livebud/bud/internal/is"
7 | "github.com/livebud/bud/package/imports"
8 | )
9 |
10 | func TestAdd(t *testing.T) {
11 | is := is.New(t)
12 | im := imports.New()
13 | is.Equal(im.Add("net/http"), "http")
14 | is.Equal(im.Add("net/http"), "http")
15 | is.Equal(im.Add("hop/http"), "http1")
16 | }
17 |
18 | func TestAddNamed(t *testing.T) {
19 | is := is.New(t)
20 | im := imports.New()
21 | is.Equal(im.AddNamed("www", "net/http"), "www")
22 | is.Equal(im.AddNamed("www", "net/http"), "www")
23 | is.Equal(im.AddNamed("www", "hop/http"), "www1")
24 | is.Equal(im.AddNamed("v8", "app.com/js/v8"), "v8")
25 | }
26 |
27 | func TestReserveBefore(t *testing.T) {
28 | is := is.New(t)
29 | im := imports.New()
30 | is.Equal(im.Reserve("web"), "web")
31 | is.Equal(len(im.List()), 0)
32 | is.Equal(im.Add("web"), "web")
33 | is.Equal(len(im.List()), 1)
34 | }
35 | func TestReserveAfter(t *testing.T) {
36 | is := is.New(t)
37 | im := imports.New()
38 | is.Equal(im.Add("web"), "web")
39 | is.Equal(len(im.List()), 1)
40 | is.Equal(im.Reserve("web"), "web")
41 | is.Equal(len(im.List()), 1)
42 | is.Equal(im.Reserve("duo/web"), "web1")
43 | is.Equal(len(im.List()), 1)
44 | is.Equal(im.Add("duo/web"), "web1")
45 | is.Equal(len(im.List()), 2)
46 | }
47 |
48 | func TestAddStd(t *testing.T) {
49 | is := is.New(t)
50 | im := imports.New()
51 | im.AddStd("os", "fmt", "net/http")
52 | is.Equal(len(im.List()), 3)
53 | is.Equal(im.List()[0].Name, "fmt")
54 | is.Equal(im.List()[0].Path, "fmt")
55 | is.Equal(im.List()[1].Name, "http")
56 | is.Equal(im.List()[1].Path, "net/http")
57 | is.Equal(im.List()[2].Name, "os")
58 | is.Equal(im.List()[2].Path, "os")
59 | }
60 |
--------------------------------------------------------------------------------
/package/js/js.go:
--------------------------------------------------------------------------------
1 | package js
2 |
3 | // VM for evaluating javascript
4 | type VM interface {
5 | Script(path, script string) error
6 | Eval(path, expression string) (string, error)
7 | }
8 |
--------------------------------------------------------------------------------
/package/log/console/console_test.go:
--------------------------------------------------------------------------------
1 | package console_test
2 |
3 | import (
4 | "errors"
5 | "os"
6 | "testing"
7 |
8 | log "github.com/livebud/bud/package/log"
9 | "github.com/livebud/bud/package/log/console"
10 | )
11 |
12 | func TestConsole(t *testing.T) {
13 | console.Field("file", "console_test.go").Field("another", "cool story").Debugf("hello %s", "mars")
14 | console.Field("file", "console_test.go").Field("another", "cool story").Infof("hello %s", "mars")
15 | console.Field("file", "console_test.go").Field("another", "cool story").Noticef("hello %s", "mars")
16 | console.Field("file", "console_test.go").Field("another", "cool story").Warnf("hello %s", "mars")
17 | console.Field("file", "console_test.go").Field("another", "cool story").Errorf("hello %s", "mars")
18 | console.Field("file", "console_test.go").Field("another", "cool story").Error("hello", "mars")
19 | console.Error(errors.New("one"), "two", "three")
20 | logger := log.New(console.New(os.Stdout))
21 | logger.Error(errors.New("one"), 4, "three")
22 | }
23 |
--------------------------------------------------------------------------------
/package/log/discard.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | var Discard = discard{}
4 |
5 | type discard struct{}
6 |
7 | var _ Log = discard{}
8 |
9 | func (d discard) Field(key string, value interface{}) Log {
10 | return d
11 | }
12 |
13 | func (d discard) Fields(fields map[string]interface{}) Log {
14 | return d
15 | }
16 |
17 | func (discard) Debug(args ...interface{}) error {
18 | return nil
19 | }
20 |
21 | func (discard) Debugf(msg string, args ...interface{}) error {
22 | return nil
23 | }
24 |
25 | func (discard) Info(args ...interface{}) error {
26 | return nil
27 | }
28 |
29 | func (discard) Infof(msg string, args ...interface{}) error {
30 | return nil
31 | }
32 |
33 | func (discard) Notice(args ...interface{}) error {
34 | return nil
35 | }
36 |
37 | func (discard) Noticef(msg string, args ...interface{}) error {
38 | return nil
39 | }
40 |
41 | func (discard) Warn(args ...interface{}) error {
42 | return nil
43 | }
44 |
45 | func (discard) Warnf(msg string, args ...interface{}) error {
46 | return nil
47 | }
48 |
49 | func (discard) Error(args ...interface{}) error {
50 | return nil
51 | }
52 |
53 | func (discard) Errorf(msg string, args ...interface{}) error {
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/package/log/level.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // Level of the logger
8 | type Level uint8
9 |
10 | // Log level
11 | const (
12 | DebugLevel Level = iota + 1
13 | InfoLevel
14 | NoticeLevel
15 | WarnLevel
16 | ErrorLevel
17 | )
18 |
19 | func (level Level) String() string {
20 | switch level {
21 | case DebugLevel:
22 | return "debug"
23 | case InfoLevel:
24 | return "info"
25 | case NoticeLevel:
26 | return "notice"
27 | case WarnLevel:
28 | return "warn"
29 | case ErrorLevel:
30 | return "error"
31 | default:
32 | return ""
33 | }
34 | }
35 |
36 | func ParseLevel(level string) (Level, error) {
37 | switch level {
38 | case "debug":
39 | return DebugLevel, nil
40 | case "info":
41 | return InfoLevel, nil
42 | case "notice":
43 | return NoticeLevel, nil
44 | case "warn":
45 | return WarnLevel, nil
46 | case "error":
47 | return ErrorLevel, nil
48 | }
49 | return 0, fmt.Errorf("log: %q is not a valid level", level)
50 | }
51 |
--------------------------------------------------------------------------------
/package/log/levelfilter/levelfilter.go:
--------------------------------------------------------------------------------
1 | package levelfilter
2 |
3 | import (
4 | log "github.com/livebud/bud/package/log"
5 | )
6 |
7 | func New(handler log.Handler, level log.Level) *Handler {
8 | return &Handler{handler, level}
9 | }
10 |
11 | // Handler logs by level
12 | type Handler struct {
13 | handler log.Handler
14 | level log.Level
15 | }
16 |
17 | func (f *Handler) Log(entry *log.Entry) error {
18 | if entry.Level < f.level {
19 | return nil
20 | }
21 | return f.handler.Log(entry)
22 | }
23 |
--------------------------------------------------------------------------------
/package/log/logfmt/logfmt.go:
--------------------------------------------------------------------------------
1 | package logfmt
2 |
3 | import (
4 | "io"
5 | "sync"
6 |
7 | "github.com/go-logfmt/logfmt"
8 | log "github.com/livebud/bud/package/log"
9 | )
10 |
11 | func New(w io.Writer) *Handler {
12 | return &Handler{enc: logfmt.NewEncoder(w)}
13 | }
14 |
15 | type Handler struct {
16 | mu sync.Mutex
17 | enc *logfmt.Encoder
18 | }
19 |
20 | func (h *Handler) Log(entry *log.Entry) error {
21 | keys := entry.Fields.Keys()
22 | h.mu.Lock()
23 | defer h.mu.Unlock()
24 | h.enc.EncodeKeyval("timestamp", entry.Timestamp)
25 | h.enc.EncodeKeyval("level", entry.Level.String())
26 | h.enc.EncodeKeyval("message", entry.Message)
27 | for _, key := range keys {
28 | h.enc.EncodeKeyval(key, entry.Fields.Get(key))
29 | }
30 | h.enc.EndRecord()
31 | return nil
32 | }
33 |
--------------------------------------------------------------------------------
/package/log/memory/memory.go:
--------------------------------------------------------------------------------
1 | package memory
2 |
3 | import log "github.com/livebud/bud/package/log"
4 |
5 | func New() *Handler {
6 | return &Handler{}
7 | }
8 |
9 | type Handler struct {
10 | Entries []*log.Entry
11 | }
12 |
13 | var _ log.Handler = (*Handler)(nil)
14 |
15 | func (h *Handler) Log(entry *log.Entry) error {
16 | h.Entries = append(h.Entries, entry)
17 | return nil
18 | }
19 |
--------------------------------------------------------------------------------
/package/log/multi/multi.go:
--------------------------------------------------------------------------------
1 | package multi
2 |
3 | import (
4 | "github.com/livebud/bud/internal/errs"
5 | log "github.com/livebud/bud/package/log"
6 | )
7 |
8 | func New(handlers ...log.Handler) *Handler {
9 | return &Handler{handlers}
10 | }
11 |
12 | // Handler logs by level
13 | type Handler struct {
14 | handlers []log.Handler
15 | }
16 |
17 | func (f *Handler) Log(entry *log.Entry) (err error) {
18 | for _, handler := range f.handlers {
19 | err = errs.Join(err, handler.Log(entry))
20 | }
21 | return err
22 | }
23 |
--------------------------------------------------------------------------------
/package/log/ndjson/ndjson.go:
--------------------------------------------------------------------------------
1 | package json
2 |
3 | import log "github.com/livebud/bud/package/log"
4 |
5 | func New() *Handler {
6 | return &Handler{}
7 | }
8 |
9 | type Handler struct {
10 | }
11 |
12 | func (h *Handler) Log(entry *log.Entry) error {
13 | return nil
14 | }
15 |
--------------------------------------------------------------------------------
/package/log/testlog/testlog.go:
--------------------------------------------------------------------------------
1 | package testlog
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/livebud/bud/package/log/levelfilter"
9 |
10 | log "github.com/livebud/bud/package/log"
11 | "github.com/livebud/bud/package/log/console"
12 | )
13 |
14 | var logFlag = flag.String("log", "info", "choose a log level")
15 |
16 | // Pattern returns the log level logFlag so we can pass it through arguments.
17 | func Pattern() string {
18 | return *logFlag
19 | }
20 |
21 | // New logger for testing. You can set the log level for a given test by
22 | // using the --log= flag. For example, `go test ./... --log=debug` will
23 | // run tests with debug logs on.
24 | func New() log.Log {
25 | level, err := log.ParseLevel(*logFlag)
26 | if err != nil {
27 | panic(fmt.Sprintf("testlog: invalid --log=[level] %q" + *logFlag))
28 | }
29 | return log.New(levelfilter.New(console.New(os.Stderr), level))
30 | }
31 |
--------------------------------------------------------------------------------
/package/middleware/httpbuffer/middleware.go:
--------------------------------------------------------------------------------
1 | package httpbuffer
2 |
3 | import (
4 | "bytes"
5 | "net/http"
6 |
7 | "github.com/felixge/httpsnoop"
8 | "github.com/livebud/bud/package/log"
9 | "github.com/livebud/bud/package/middleware"
10 | )
11 |
12 | func New(log log.Log) middleware.Middleware {
13 | rw := &responseWriter{
14 | code: 0,
15 | body: new(bytes.Buffer),
16 | }
17 | return func(next http.Handler) http.Handler {
18 | return http.HandlerFunc(func(original http.ResponseWriter, r *http.Request) {
19 | w := httpsnoop.Wrap(original, httpsnoop.Hooks{
20 | WriteHeader: func(_ httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
21 | return rw.WriteHeader
22 | },
23 | Write: func(_ httpsnoop.WriteFunc) httpsnoop.WriteFunc {
24 | return rw.Write
25 | },
26 | Flush: func(flush httpsnoop.FlushFunc) httpsnoop.FlushFunc {
27 | rw.writeTo(original)
28 | return flush
29 | },
30 | })
31 | next.ServeHTTP(w, r)
32 | rw.writeTo(original)
33 | })
34 | }
35 | }
36 |
37 | type responseWriter struct {
38 | body *bytes.Buffer
39 | code int
40 | wrote bool
41 | }
42 |
43 | func (rw *responseWriter) WriteHeader(statusCode int) {
44 | rw.code = statusCode
45 | }
46 |
47 | func (rw *responseWriter) Write(b []byte) (int, error) {
48 | return rw.body.Write(b)
49 | }
50 |
51 | func (rw *responseWriter) writeTo(w http.ResponseWriter) {
52 | // Only write status code once to avoid:
53 | // "http: superfluous response.WriteHeader"
54 | // Not concurrency safe.
55 | if !rw.wrote {
56 | if rw.code == 0 {
57 | rw.code = http.StatusOK
58 | }
59 | w.WriteHeader(rw.code)
60 | rw.wrote = true
61 | }
62 | rw.body.WriteTo(w)
63 | }
64 |
--------------------------------------------------------------------------------
/package/middleware/methodoverride/methodoverride.go:
--------------------------------------------------------------------------------
1 | package methodoverride
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 |
7 | "github.com/livebud/bud/package/middleware"
8 | )
9 |
10 | // Methods eligible for overriding
11 | var eligible = map[string]struct{}{
12 | http.MethodDelete: {},
13 | http.MethodPut: {},
14 | http.MethodPatch: {},
15 | }
16 |
17 | const formType = "application/x-www-form-urlencoded"
18 |
19 | // New allows HTML