├── .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 |
8 | 9 | 10 | 11 |
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 | 12 | {/each} 13 | 14 | {/if} 15 | {#each posts as post} 16 | 17 | {#each Object.keys(post) as key} 18 | {#if key.toLowerCase() === "id"} 19 | 20 | {:else} 21 | 22 | {/if} 23 | {/each} 24 | 25 | {/each} 26 |
{key}
{post[key]}{post[key]}
27 | 28 |
29 | 30 | New Post 31 | 32 | 37 | -------------------------------------------------------------------------------- /example/basic/view/new.svelte: -------------------------------------------------------------------------------- 1 |

New Post

2 | 3 |
4 | 5 | 6 |
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 | 11 | {/each} 12 | 13 | 14 | {#each Object.keys(post) as key} 15 | 16 | {/each} 17 | 18 |
{key}
{post[key]}
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 |
11 |
12 | {show ? "↓" : `→`} 13 | {comment.author} 14 | {timeago(comment.created_at)} 15 |
16 | {#if show} 17 |
18 | {@html comment.text} 19 |
20 | {#if comment.children} 21 | {#each comment.children as comment} 22 | 23 | {/each} 24 | {/if} 25 | {/if} 26 |
27 | 28 | 52 | -------------------------------------------------------------------------------- /example/hn/view/Header.svelte: -------------------------------------------------------------------------------- 1 |
2 | Hacker News 3 |

Hacker News

4 | 9 |
10 | 11 | 54 | -------------------------------------------------------------------------------- /example/hn/view/Story.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 | {story.title} 23 | {#if story.url} 24 | ({formatURL(story.url)}) 25 | {/if} 26 |
27 |
28 | {story.points} points by {story.author} • {timeago(story.created_at)} • 29 | {formatComments(story.num_comments)} 30 |
31 |
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 |
2 |

Login

3 | 4 | 8 | 12 |

13 | 14 |

15 |
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 |
8 | 9 | 10 | 11 |
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 | 12 | {/each} 13 | 14 | {/if} 15 | {#each {{ $.Plural }} as {{ $.Singular -}} } 16 | 17 | {#each Object.keys({{ $.Singular }}) as key} 18 | {#if key.toLowerCase() === "id"} 19 | 20 | {:else} 21 | 22 | {/if} 23 | {/each} 24 | 25 | {/each} 26 |
{key}
{ {{- $.Singular }}[key]}{ {{- $.Singular }}[key]}
27 | 28 |
29 | 30 | New {{ $.Title }} 31 | 32 | 37 | -------------------------------------------------------------------------------- /internal/cli/new_controller/view_new.gotext: -------------------------------------------------------------------------------- 1 |

New {{ $.Title }}

2 | 3 |
4 | 5 | 6 |
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 | 11 | {/each} 12 | 13 | 14 | {#each Object.keys({{ $.Singular }}) as key} 15 | 16 | {/each} 17 | 18 |
{key}
{ {{- $.Singular }}[key]}
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
's to dispatch PATCH, PUT and 20 | // DELETE requests by overriding the request method using a hidden "_method" 21 | // field in the form body. 22 | func New() middleware.Middleware { 23 | return func(next http.Handler) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | // Only override POST requests 26 | if r.Method != http.MethodPost { 27 | next.ServeHTTP(w, r) 28 | return 29 | } 30 | // Must have a request body and set the content-type to 31 | // application/x-www-form-urlencoded. 32 | if r.Body == nil || r.Header.Get("Content-Type") != formType { 33 | next.ServeHTTP(w, r) 34 | return 35 | } 36 | // Try parsing the request form 37 | if err := r.ParseForm(); err != nil { 38 | http.Error(w, err.Error(), http.StatusBadRequest) 39 | return 40 | } 41 | // Check if the _method form value is set 42 | override := strings.ToUpper(r.Form.Get("_method")) 43 | // Ensure the method is eligible for overriding 44 | if _, ok := eligible[override]; !ok { 45 | next.ServeHTTP(w, r) 46 | return 47 | } 48 | // Override the request method 49 | r.Method = override 50 | next.ServeHTTP(w, r) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Middleware func(http.Handler) http.Handler 8 | 9 | // Compose a stack of middleware into a single middleware 10 | func Compose(middlewares ...Middleware) Middleware { 11 | return func(h http.Handler) http.Handler { 12 | if len(middlewares) == 0 { 13 | return h 14 | } 15 | for i := len(middlewares) - 1; i >= 0; i-- { 16 | if middlewares[i] == nil { 17 | continue 18 | } 19 | h = middlewares[i](h) 20 | } 21 | return h 22 | } 23 | } 24 | 25 | // // Interface for implementing middleware 26 | // type Middleware interface { 27 | // Middleware(next http.Handler) http.Handler 28 | // } 29 | 30 | // // Function for creating middleware 31 | // type Function func(next http.Handler) http.Handler 32 | 33 | // func (fn Function) Middleware(next http.Handler) http.Handler { 34 | // return fn(next) 35 | // } 36 | 37 | // // Stack of middleware 38 | // type Stack []Middleware 39 | 40 | // // Middleware fn 41 | // func (stack Stack) Middleware(next http.Handler) http.Handler { 42 | // return Compose(stack...).Middleware(next) 43 | // } 44 | 45 | // // Compose a stack of middleware into a single middleware 46 | // func Compose(stack ...Middleware) Middleware { 47 | // return Function(func(h http.Handler) http.Handler { 48 | // if len(stack) == 0 { 49 | // return h 50 | // } 51 | // for i := len(stack) - 1; i >= 0; i-- { 52 | // if stack[i] == nil { 53 | // continue 54 | // } 55 | // h = stack[i].Middleware(h) 56 | // } 57 | // return h 58 | // }) 59 | // } 60 | -------------------------------------------------------------------------------- /package/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import "testing" 4 | 5 | func TestCompose(t *testing.T) { 6 | // TODO 7 | } 8 | 9 | func TestComposeWithNil(t *testing.T) { 10 | // TODO 11 | } 12 | -------------------------------------------------------------------------------- /package/parser/alias.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "go/ast" 5 | ) 6 | 7 | type Alias struct { 8 | file *File 9 | node *ast.TypeSpec 10 | kind Kind // Resolved kind 11 | } 12 | 13 | var _ Declaration = (*Alias)(nil) 14 | 15 | func (a *Alias) File() *File { 16 | return a.file 17 | } 18 | 19 | func (a *Alias) Name() string { 20 | return a.node.Name.Name 21 | } 22 | 23 | func (a *Alias) Kind() Kind { 24 | return a.kind 25 | } 26 | 27 | // Private returns true if the field is private 28 | func (a *Alias) Private() bool { 29 | return isPrivate(a.node.Name.Name) 30 | } 31 | 32 | func (a *Alias) Package() *Package { 33 | return a.file.Package() 34 | } 35 | 36 | func (a *Alias) Type() Type { 37 | return getType(a, a.node.Type) 38 | } 39 | 40 | // Definition goes to the aliases definition 41 | func (a *Alias) Definition() (Declaration, error) { 42 | return Definition(a.Type()) 43 | } 44 | -------------------------------------------------------------------------------- /package/parser/builtin.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // builtin declaration 4 | type builtin string 5 | 6 | // Name is the built-in type 7 | func (b builtin) Name() string { 8 | return string(b) 9 | } 10 | 11 | func (b builtin) Kind() Kind { 12 | return KindBuiltin 13 | } 14 | 15 | // Directory for builtin is blank 16 | func (b builtin) Directory() string { 17 | return "" 18 | } 19 | 20 | // Package for builtin is blank 21 | // TODO: there should probably be a built-in package to avoid panics 22 | func (b builtin) Package() *Package { 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /package/parser/field.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Fielder is an interface containing the common operations on fields 4 | type Fielder interface { 5 | File() *File 6 | Name() string 7 | Type() Type 8 | } 9 | 10 | func fieldString(f Fielder) string { 11 | return f.Name() + " " + f.Type().String() 12 | } 13 | -------------------------------------------------------------------------------- /package/parser/testdata/alias-lookup.txt: -------------------------------------------------------------------------------- 1 | -- mod/bud.test@v0.0.2/go.mod -- 2 | module bud.test 3 | 4 | -- mod/bud.test@v0.0.2/public/public.go -- 5 | package public 6 | 7 | import ( 8 | "net/http" 9 | ) 10 | 11 | type Middleware = Interface 12 | 13 | // Interface for implementing middleware 14 | type Interface interface { 15 | Middleware(next http.Handler) http.Handler 16 | } 17 | 18 | -- app/go.mod -- 19 | module app.com 20 | 21 | require ( 22 | bud.test v0.0.2 23 | ) 24 | 25 | -- app/main.go -- 26 | package main 27 | 28 | import ( 29 | "bud.test/public" 30 | ) 31 | 32 | type Middleware = public.Middleware 33 | 34 | func main () { 35 | println(Middleware) 36 | } 37 | 38 | -------------------------------------------------------------------------------- /package/parser/testdata/interface-lookup.txt: -------------------------------------------------------------------------------- 1 | -- mod/mod.test/three@v1.0.0/go.mod -- 2 | module mod.test/three 3 | 4 | -- mod/mod.test/three@v1.0.0/inner/inner.go -- 5 | package inner 6 | 7 | type Interface interface { 8 | String() string 9 | } 10 | 11 | -- mod/mod.test/two@v0.0.2/go.mod -- 12 | module mod.test/two 13 | 14 | require ( 15 | mod.test/three v1.0.0 16 | ) 17 | 18 | -- mod/mod.test/two@v0.0.2/struct.go -- 19 | package two 20 | 21 | import "mod.test/three/inner" 22 | 23 | type Interface interface { 24 | Test() inner.Interface 25 | } 26 | 27 | -- app/go.mod -- 28 | module app.com 29 | 30 | require ( 31 | mod.test/two v0.0.2 32 | ) 33 | 34 | -- app/hello/hello.go -- 35 | package hello 36 | 37 | import ( 38 | "mod.test/two" 39 | ) 40 | 41 | type A struct { 42 | S two.Interface 43 | } 44 | -------------------------------------------------------------------------------- /package/parser/testdata/struct-lookup.txt: -------------------------------------------------------------------------------- 1 | -- mod/mod.test/three@v1.0.0/go.mod -- 2 | module mod.test/three 3 | 4 | -- mod/mod.test/three@v1.0.0/inner/inner.go -- 5 | package inner 6 | 7 | type Dep struct {} 8 | 9 | -- mod/mod.test/two@v0.0.1/go.mod -- 10 | module mod.test/two 11 | 12 | require ( 13 | mod.test/three v1.0.0 14 | ) 15 | 16 | -- mod/mod.test/two@v0.0.1/struct.go -- 17 | package two 18 | 19 | type Struct struct { 20 | } 21 | 22 | -- mod/mod.test/two@v0.0.2/go.mod -- 23 | module mod.test/two 24 | 25 | require ( 26 | mod.test/three v1.0.0 27 | ) 28 | 29 | -- mod/mod.test/two@v0.0.2/struct.go -- 30 | package two 31 | 32 | import "mod.test/three/inner" 33 | 34 | type Struct struct { 35 | inner.Dep 36 | } 37 | 38 | -- app/go.mod -- 39 | module app.com 40 | 41 | require ( 42 | mod.test/two v0.0.2 43 | ) 44 | 45 | -- app/hello/hello.go -- 46 | package hello 47 | 48 | import ( 49 | "mod.test/two" 50 | ) 51 | 52 | type A struct { 53 | S *two.Struct 54 | } 55 | -------------------------------------------------------------------------------- /package/parser/typespec.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "go/ast" 4 | 5 | type TypeSpec struct { 6 | file *File 7 | node *ast.TypeSpec 8 | } 9 | 10 | var _ Declaration = (*TypeSpec)(nil) 11 | 12 | func (t *TypeSpec) Name() string { 13 | return t.node.Name.Name 14 | } 15 | 16 | func (t *TypeSpec) Private() bool { 17 | return isPrivate(t.Name()) 18 | } 19 | 20 | func (t *TypeSpec) File() *File { 21 | return t.file 22 | } 23 | 24 | func (t *TypeSpec) Package() *Package { 25 | return t.file.Package() 26 | } 27 | 28 | func (t *TypeSpec) Kind() Kind { 29 | return KindTypeSpec 30 | } 31 | 32 | func (t *TypeSpec) Type() Type { 33 | return getType(t, t.node.Type) 34 | } 35 | -------------------------------------------------------------------------------- /package/pluginmod/pluginmod.go: -------------------------------------------------------------------------------- 1 | package pluginmod 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "path" 7 | "strings" 8 | 9 | "github.com/livebud/bud/package/gomod" 10 | ) 11 | 12 | func Glob(module *gomod.Module, dir string) (plugins []*gomod.Module, err error) { 13 | // Get all the bud plugins that start with bud-* 14 | modules, err := module.FindBy(func(req *gomod.Require) bool { 15 | // Plugins must be directly imported, they cannot come indirectly through 16 | // another dependency 17 | if req.Indirect { 18 | return false 19 | } 20 | return strings.HasPrefix(path.Base(req.Mod.Path), "bud-") 21 | }) 22 | if err != nil { 23 | return nil, err 24 | } 25 | // Add the app module to the top of the list 26 | modules = append([]*gomod.Module{module}, modules...) 27 | for _, module := range modules { 28 | if _, err := fs.Stat(module, dir); err != nil { 29 | if errors.Is(err, fs.ErrNotExist) { 30 | continue 31 | } 32 | return nil, err 33 | } 34 | plugins = append(plugins, module) 35 | } 36 | return plugins, nil 37 | } 38 | -------------------------------------------------------------------------------- /package/remotefs/server.go: -------------------------------------------------------------------------------- 1 | package remotefs 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/fs" 8 | "net" 9 | "net/rpc" 10 | 11 | "github.com/livebud/bud/internal/extrafile" 12 | "github.com/livebud/bud/package/socket" 13 | ) 14 | 15 | // ServeFrom serves the filesystem from a listener passed in by a parent process 16 | func ServeFrom(ctx context.Context, fsys fs.FS, prefix string) error { 17 | if prefix == "" { 18 | prefix = defaultPrefix 19 | } 20 | files := extrafile.Load(prefix) 21 | if len(files) == 0 { 22 | return fmt.Errorf("remotefs: no extra files passed into the process") 23 | } 24 | ln, err := socket.From(files[0]) 25 | if err != nil { 26 | return fmt.Errorf("remotefs: unable to turn extra file into listener. %w", err) 27 | } 28 | defer ln.Close() 29 | go Serve(fsys, ln) 30 | <-ctx.Done() 31 | return nil 32 | } 33 | 34 | // Serve the filesystem from a listener 35 | func Serve(fsys fs.FS, ln net.Listener) error { 36 | server := rpc.NewServer() 37 | server.RegisterName("remotefs", NewService(fsys)) 38 | return accept(server, ln) 39 | } 40 | 41 | // Accept connections from the listener. This will block until the listener is 42 | // closed 43 | func accept(server *rpc.Server, ln net.Listener) error { 44 | for { 45 | conn, err := ln.Accept() 46 | if err != nil { 47 | if errors.Is(err, net.ErrClosed) { 48 | return nil 49 | } 50 | return err 51 | } 52 | go server.ServeConn(conn) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package/router/parse.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import "github.com/livebud/bud/package/router/lex" 4 | 5 | func Parse(route string) (tokens []lex.Token) { 6 | lexer := lex.New(route) 7 | for token := lexer.Next(); token.Type != lex.EndToken; { 8 | tokens = append(tokens, token) 9 | } 10 | return tokens 11 | } 12 | -------------------------------------------------------------------------------- /package/scaffold/template.go: -------------------------------------------------------------------------------- 1 | package scaffold 2 | 3 | import ( 4 | "go/format" 5 | "path" 6 | "path/filepath" 7 | 8 | "github.com/livebud/bud/package/gotemplate" 9 | "github.com/livebud/bud/package/vfs" 10 | "golang.org/x/sync/errgroup" 11 | ) 12 | 13 | type Templates []*Template 14 | 15 | func (templates Templates) Write(fsys vfs.ReadWritable) error { 16 | eg := new(errgroup.Group) 17 | for _, template := range templates { 18 | template := template 19 | eg.Go(func() error { return template.Write(fsys) }) 20 | } 21 | return eg.Wait() 22 | 23 | } 24 | 25 | type Template struct { 26 | Path string 27 | Code string 28 | State interface{} 29 | } 30 | 31 | func (t *Template) Write(fsys vfs.ReadWritable) error { 32 | generator, err := gotemplate.Parse(t.Path, t.Code) 33 | if err != nil { 34 | return err 35 | } 36 | code, err := generator.Generate(t.State) 37 | if err != nil { 38 | return err 39 | } 40 | // Format Go code automatically 41 | if filepath.Ext(t.Path) == ".go" { 42 | code, err = format.Source(code) 43 | if err != nil { 44 | return err 45 | } 46 | } 47 | if err := fsys.MkdirAll(path.Dir(t.Path), 0755); err != nil { 48 | return err 49 | } 50 | if err := fsys.WriteFile(t.Path, code, 0644); err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /package/svelte/compiler.ts: -------------------------------------------------------------------------------- 1 | import { compile as compileSvelte } from "svelte/compiler" 2 | 3 | type Input = { 4 | code: string 5 | path: string 6 | target: "ssr" | "dom" 7 | dev: boolean 8 | css: boolean 9 | } 10 | 11 | // Capitalized for Go 12 | type Output = 13 | | { 14 | JS: string 15 | CSS: string 16 | } 17 | | { 18 | Error: { 19 | Path: string 20 | Name: string 21 | Message: string 22 | Stack?: string 23 | } 24 | } 25 | 26 | // Compile svelte code 27 | export function compile(input: Input): string { 28 | const { code, path, target, dev, css } = input 29 | const svelte = compileSvelte(code, { 30 | filename: path, 31 | generate: target, 32 | hydratable: true, 33 | format: "esm", 34 | dev: dev, 35 | css: css, 36 | }) 37 | return JSON.stringify({ 38 | CSS: svelte.css.code, 39 | JS: svelte.js.code, 40 | } as Output) 41 | } 42 | -------------------------------------------------------------------------------- /package/svelte/shimssr.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shim for getting the svelte compiler to run in a V8 isolate. 3 | */ 4 | 5 | // URL shim for the browser 6 | // TODO: properly shim URL 7 | export class URL { 8 | constructor(url: string) { 9 | console.log(url) 10 | } 11 | } 12 | 13 | // TODO: properly shim performance.now() 14 | export const self = { 15 | performance: { 16 | now(): number { 17 | return 0 18 | }, 19 | }, 20 | } 21 | 22 | // In development mode when compiling for the browser we hit this codepath: 23 | // https://github.com/Rich-Harris/magic-string/blob/8f666889136ac2580356e48610b3ac95c276191e/src/SourceMap.js#L3-L10 24 | // Since we're running in a V8 isolate, we don't have a window or a Buffer. 25 | // TODO: shim btoa properly 26 | export const window = { 27 | btoa: (data: string): string => { 28 | return "" 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /package/testgen/main.gotext: -------------------------------------------------------------------------------- 1 | package main 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 main() { 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /package/vfs/exist.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "sync" 7 | 8 | "golang.org/x/sync/errgroup" 9 | ) 10 | 11 | // Exist returns an error if any of the paths don't exist 12 | func Exist(fsys fs.FS, paths ...string) (err error) { 13 | eg := new(errgroup.Group) 14 | for _, path := range paths { 15 | path := path 16 | eg.Go(func() error { 17 | if _, err := fs.Stat(fsys, path); err != nil { 18 | return err 19 | } 20 | return nil 21 | }) 22 | } 23 | return eg.Wait() 24 | } 25 | 26 | // Exists will check if files exist at once, returning a map of the results. 27 | // If there are any errors (besides ErrNotExist), the whole call fails. 28 | func SomeExist(fsys fs.FS, paths ...string) (map[string]bool, error) { 29 | m := map[string]bool{} 30 | mu := sync.Mutex{} 31 | eg := new(errgroup.Group) 32 | for _, path := range paths { 33 | path := path 34 | eg.Go(func() error { 35 | if _, err := fs.Stat(fsys, path); err != nil { 36 | if !errors.Is(err, fs.ErrNotExist) { 37 | return err 38 | } 39 | return nil 40 | } 41 | mu.Lock() 42 | m[path] = true 43 | mu.Unlock() 44 | return nil 45 | }) 46 | } 47 | return m, eg.Wait() 48 | } 49 | -------------------------------------------------------------------------------- /package/vfs/map.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | type Map map[string][]byte 8 | 9 | // TODO: support the vfs.ReadWritable interface 10 | var _ fs.FS = (Map)(nil) 11 | 12 | func toMemory(m Map) Memory { 13 | memory := Memory{} 14 | for path, data := range m { 15 | memory[path] = &File{Data: []byte(data)} 16 | } 17 | return memory 18 | } 19 | 20 | func (m Map) Open(name string) (fs.File, error) { 21 | return toMemory(m).Open(name) 22 | } 23 | 24 | func (m Map) MkdirAll(path string, perm fs.FileMode) error { 25 | return toMemory(m).MkdirAll(path, perm) 26 | } 27 | 28 | func (m Map) WriteFile(name string, data []byte, perm fs.FileMode) error { 29 | return toMemory(m).WriteFile(name, data, perm) 30 | } 31 | 32 | func (m Map) RemoveAll(path string) error { 33 | return toMemory(m).RemoveAll(path) 34 | } 35 | -------------------------------------------------------------------------------- /package/vfs/map_test.go: -------------------------------------------------------------------------------- 1 | package vfs_test 2 | 3 | import ( 4 | "io/fs" 5 | "testing" 6 | 7 | "github.com/livebud/bud/internal/is" 8 | "github.com/livebud/bud/package/vfs" 9 | ) 10 | 11 | func TestMap(t *testing.T) { 12 | is := is.New(t) 13 | fsys := vfs.Map{ 14 | "duo/view/index.svelte": []byte(`

index

`), 15 | } 16 | 17 | // Read duo/view/index.svelte 18 | code, err := fs.ReadFile(fsys, "duo/view/index.svelte") 19 | is.NoErr(err) 20 | is.Equal(string(code), `

index

`) 21 | 22 | // stat duo/ 23 | stat, err := fs.Stat(fsys, "duo") 24 | is.NoErr(err) 25 | is.Equal(stat.Name(), "duo") 26 | is.Equal(stat.IsDir(), true) 27 | is.Equal(stat.Mode(), fs.FileMode(fs.ModeDir)) 28 | } 29 | -------------------------------------------------------------------------------- /package/vfs/memory.go: -------------------------------------------------------------------------------- 1 | package vfs 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | "strings" 8 | "testing/fstest" 9 | ) 10 | 11 | type Memory fstest.MapFS 12 | type File = fstest.MapFile 13 | 14 | var _ ReadWritable = (Memory)(nil) 15 | 16 | func (m Memory) Open(name string) (fs.File, error) { 17 | return fstest.MapFS(m).Open(name) 18 | } 19 | 20 | func (m Memory) MkdirAll(path string, perm fs.FileMode) error { 21 | // Don't create a directory unless we have to 22 | if _, err := fs.Stat(m, path); nil == err { 23 | return nil 24 | } 25 | m[path] = &fstest.MapFile{ModTime: Now(), Mode: perm | os.ModeDir} 26 | return nil 27 | } 28 | 29 | func (m Memory) WriteFile(name string, data []byte, perm fs.FileMode) error { 30 | m[name] = &fstest.MapFile{Data: data, ModTime: Now(), Mode: perm} 31 | return nil 32 | } 33 | 34 | func (m Memory) RemoveAll(path string) error { 35 | stat, err := fs.Stat(m, path) 36 | if err != nil { 37 | if errors.Is(err, fs.ErrNotExist) { 38 | return nil 39 | } 40 | return err 41 | } 42 | // Delete the path 43 | delete(m, path) 44 | // Only delete the file 45 | if !stat.IsDir() { 46 | return nil 47 | } 48 | // Need to delete the rest of the files 49 | dirpath := path + "/" 50 | for fpath := range m { 51 | if strings.HasPrefix(fpath, dirpath) { 52 | delete(m, fpath) 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /package/vfs/vfs.go: -------------------------------------------------------------------------------- 1 | // Package vfs is deprecated and will be deleted once remaining code is migrated 2 | // All new functionality should go in the virtual package. 3 | package vfs 4 | 5 | import ( 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | ) 11 | 12 | type Writable interface { 13 | MkdirAll(path string, perm fs.FileMode) error 14 | WriteFile(name string, data []byte, perm fs.FileMode) error 15 | RemoveAll(path string) error 16 | } 17 | 18 | type ReadWritable interface { 19 | fs.FS 20 | Writable 21 | } 22 | 23 | // Now may be overridden for testing purposes 24 | var Now = func() time.Time { 25 | return time.Now() 26 | } 27 | 28 | // WriteAll the filesystem at "from" to "to" 29 | func WriteAll(from, to string, fsys fs.FS) error { 30 | return fs.WalkDir(fsys, from, func(path string, de fs.DirEntry, err error) error { 31 | if err != nil { 32 | return err 33 | } 34 | toPath := filepath.Join(to, path) 35 | if de.IsDir() { 36 | mode := de.Type() 37 | if mode == fs.ModeDir { 38 | mode = fs.FileMode(0755) 39 | } 40 | return os.MkdirAll(toPath, mode) 41 | } 42 | data, err := fs.ReadFile(fsys, path) 43 | if err != nil { 44 | return err 45 | } 46 | mode := de.Type() 47 | if mode == 0 { 48 | mode = fs.FileMode(0644) 49 | } 50 | return os.WriteFile(toPath, data, mode) 51 | }) 52 | } 53 | 54 | func Write(to string, fsys fs.FS) error { 55 | return WriteAll(".", to, fsys) 56 | } 57 | -------------------------------------------------------------------------------- /package/viewer/svelte/compiler.ts: -------------------------------------------------------------------------------- 1 | import { compile as compileSvelte } from "svelte/compiler" 2 | 3 | type Input = { 4 | code: string 5 | path: string 6 | target: "ssr" | "dom" 7 | dev: boolean 8 | css: boolean 9 | } 10 | 11 | // Capitalized for Go 12 | type Output = 13 | | { 14 | JS: string 15 | CSS: string 16 | } 17 | | { 18 | Error: { 19 | Path: string 20 | Name: string 21 | Message: string 22 | Stack?: string 23 | } 24 | } 25 | 26 | // Compile svelte code 27 | export function compile(input: Input): string { 28 | const { code, path, target, dev, css } = input 29 | const svelte = compileSvelte(code, { 30 | filename: path, 31 | generate: target, 32 | hydratable: true, 33 | format: "esm", 34 | dev: dev, 35 | css: css, 36 | }) 37 | return JSON.stringify({ 38 | CSS: svelte.css.code, 39 | JS: svelte.js.code, 40 | } as Output) 41 | } -------------------------------------------------------------------------------- /package/viewer/svelte/compiler_shim.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shim for getting the svelte compiler to run in a V8 isolate. 3 | */ 4 | 5 | // URL shim for the browser 6 | // TODO: properly shim URL 7 | export class URL { 8 | constructor(url: string) { 9 | console.log(url) 10 | } 11 | } 12 | 13 | // TODO: properly shim performance.now() 14 | export const self = { 15 | performance: { 16 | now(): number { 17 | return 0 18 | }, 19 | }, 20 | } 21 | 22 | // In development mode when compiling for the browser we hit this codepath: 23 | // https://github.com/Rich-Harris/magic-string/blob/8f666889136ac2580356e48610b3ac95c276191e/src/SourceMap.js#L3-L10 24 | // Since we're running in a V8 isolate, we don't have a window or a Buffer. 25 | // TODO: shim btoa properly 26 | export const window = { 27 | btoa: (data: string): string => { 28 | return "" 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /package/viewer/svelte/dom_entry.gotext: -------------------------------------------------------------------------------- 1 | {{/* dom_entry.gotext is the entrypoint for hydrating a page */}} 2 | 3 | {{- range $import := $.Imports }} 4 | import {{ $import.Name }} from "{{ $import.Path }}" 5 | {{- end }} 6 | 7 | import { mount } from ".svelte_dom_runtime"; 8 | {{- if $.Hot }} 9 | import Hot from "livebud/runtime/hot" 10 | {{- end }} 11 | 12 | const components = { 13 | "{{ $.Page.Key }}": {{ $.Page.Component }}, 14 | {{- range $i, $frame := $.Page.Frames }} 15 | "{{ $frame.Key }}": {{ $frame.Component }}, 16 | {{- end }} 17 | {{- if $.Page.Error }} 18 | "{{ $.Page.Error.Key }}": {{ $.Page.Error.Component }}, 19 | {{- end }} 20 | } 21 | 22 | mount({ 23 | key: "{{ $.Page.Key }}", 24 | frames: [ 25 | {{- range $i, $frame := $.Page.Frames }} 26 | "{{ $frame.Key }}", 27 | {{- end }} 28 | ], 29 | {{- if $.Page.Error }} 30 | error: "{{ $.Page.Error.Key }}", 31 | {{- end }} 32 | components: components, 33 | {{- if $.Hot }} 34 | hot: new Hot("http://127.0.0.1:35729/bud/hot/{{$.Page.Key}}", components), 35 | {{- end }} 36 | }) -------------------------------------------------------------------------------- /package/viewer/svelte/ssr_entry.gotext: -------------------------------------------------------------------------------- 1 | {{/* ssr_entry.gotext is the entrypoint for server-pages */}} 2 | 3 | {{- range $import := $.Imports }} 4 | import {{ $import.Name }} from "./{{ $import.Path }}" 5 | {{- end }} 6 | 7 | import { Page } from ".svelte_ssr_runtime"; 8 | 9 | const page = new Page({ 10 | key: "{{ $.Page.Key }}", 11 | Component: {{ $.Page.Component }}, 12 | client: "{{ $.Page.Client.Route }}", 13 | {{- if $.Page.Layout }} 14 | layout: { 15 | key: "{{ $.Page.Layout.Key }}", 16 | Component: {{ $.Page.Layout.Component }}, 17 | }, 18 | {{- end }} 19 | frames: [ 20 | {{- range $i, $frame := $.Page.Frames }} 21 | { 22 | key: "{{ $frame.Key }}", 23 | Component: {{ $frame.Component }}, 24 | }, 25 | {{- end }} 26 | ], 27 | {{- if $.Page.Error }} 28 | error: { 29 | key: "{{ $.Page.Error.Key }}", 30 | Component: {{ $.Page.Error.Component }}, 31 | }, 32 | {{- end }} 33 | }) 34 | 35 | // Render the page 36 | export function render(props) { 37 | return page.render(props) 38 | }; -------------------------------------------------------------------------------- /package/virtual/copy.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "path" 6 | 7 | "github.com/livebud/bud/package/log" 8 | ) 9 | 10 | // Copy files from one filesystem to another at subpath 11 | func Copy(log log.Log, from fs.FS, to FS, subpaths ...string) error { 12 | target := path.Join(subpaths...) 13 | if target == "" { 14 | target = "." 15 | } 16 | return fs.WalkDir(from, target, func(fpath string, d fs.DirEntry, err error) error { 17 | if err != nil { 18 | return err 19 | } else if fpath == "." { 20 | return nil 21 | } 22 | if d.IsDir() { 23 | mode := d.Type() 24 | // Many of the virtual filesystems don't set a mode. Copying these to an 25 | // actual filesystem will cause permission errors, so we'll use common 26 | // permissions when not explicitly set. 27 | if mode == 0 || mode == fs.ModeDir { 28 | mode = 0755 | fs.ModeDir 29 | } 30 | log.Debug("virtual: copying dir", fpath, mode) 31 | return to.MkdirAll(fpath, mode) 32 | } 33 | data, err := fs.ReadFile(from, fpath) 34 | if err != nil { 35 | return err 36 | } 37 | // Many of the virtual filesystems don't set a mode. Copying these to an 38 | // actual filesystem will cause permission errors, so we'll use common 39 | // permissions when not explicitly set. 40 | mode := d.Type() 41 | if mode == 0 { 42 | mode = 0644 43 | } 44 | log.Debug("virtual: copying file", fpath, mode) 45 | return to.WriteFile(fpath, data, mode) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /package/virtual/dir.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | ) 7 | 8 | type openDir struct { 9 | *File 10 | offset int 11 | } 12 | 13 | var _ fs.File = (*openDir)(nil) 14 | var _ fs.ReadDirFile = (*openDir)(nil) 15 | var _ fs.DirEntry = (*openDir)(nil) 16 | 17 | var _ fs.File = (*openDir)(nil) 18 | var _ fs.ReadDirFile = (*openDir)(nil) 19 | 20 | func (d *openDir) Close() error { 21 | return nil 22 | } 23 | 24 | func (d *openDir) Stat() (fs.FileInfo, error) { 25 | return d.Info() 26 | } 27 | 28 | func (d *openDir) Read(p []byte) (int, error) { 29 | return 0, &fs.PathError{Op: "read", Path: d.Path, Err: fs.ErrInvalid} 30 | } 31 | 32 | func (d *openDir) ReadDir(count int) ([]fs.DirEntry, error) { 33 | n := len(d.Entries) - d.offset 34 | if count > 0 && n > count { 35 | n = count 36 | } 37 | if n == 0 && count > 0 { 38 | return nil, io.EOF 39 | } 40 | list := make([]fs.DirEntry, n) 41 | for i := range list { 42 | list[i] = d.Entries[d.offset+i] 43 | } 44 | d.offset += n 45 | return list, nil 46 | } 47 | -------------------------------------------------------------------------------- /package/virtual/direntry.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "path" 6 | "time" 7 | ) 8 | 9 | type DirEntry struct { 10 | Path string 11 | Size int64 12 | Mode fs.FileMode 13 | ModTime time.Time 14 | } 15 | 16 | var _ fs.DirEntry = (*DirEntry)(nil) 17 | 18 | func (e *DirEntry) Name() string { 19 | return path.Base(e.Path) 20 | } 21 | 22 | func (e *DirEntry) IsDir() bool { 23 | return e.Mode&fs.ModeDir != 0 24 | } 25 | 26 | func (e *DirEntry) Type() fs.FileMode { 27 | return e.Mode.Type() 28 | } 29 | 30 | func (e *DirEntry) Info() (fs.FileInfo, error) { 31 | return &fileInfo{ 32 | path: e.Path, 33 | mode: e.Mode, 34 | modTime: e.ModTime, 35 | size: e.Size, 36 | }, nil 37 | } 38 | -------------------------------------------------------------------------------- /package/virtual/exclude.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "path" 6 | ) 7 | 8 | func Exclude(fsys FS, fn func(path string) bool) FS { 9 | return &exclude{fsys, fn} 10 | } 11 | 12 | type exclude struct { 13 | FS 14 | fn func(path string) bool 15 | } 16 | 17 | func (e *exclude) Open(path string) (fs.File, error) { 18 | if e.fn(path) { 19 | return nil, fs.ErrNotExist 20 | } 21 | return e.FS.Open(path) 22 | } 23 | 24 | func (e *exclude) ReadDir(dir string) (results []fs.DirEntry, err error) { 25 | if e.fn(dir) { 26 | return nil, fs.ErrNotExist 27 | } 28 | des, err := fs.ReadDir(e.FS, dir) 29 | if err != nil { 30 | return nil, err 31 | } 32 | for _, de := range des { 33 | if e.fn(path.Join(dir, de.Name())) { 34 | continue 35 | } 36 | results = append(results, de) 37 | } 38 | return results, nil 39 | } 40 | -------------------------------------------------------------------------------- /package/virtual/exclude_test.go: -------------------------------------------------------------------------------- 1 | package virtual_test 2 | 3 | import ( 4 | "io/fs" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/livebud/bud/internal/is" 9 | "github.com/livebud/bud/package/virtual" 10 | ) 11 | 12 | func TestExclude(t *testing.T) { 13 | is := is.New(t) 14 | tree := virtual.Tree{ 15 | "view/a.txt": &virtual.File{Data: []byte("a")}, 16 | "view/b.txt": &virtual.File{Data: []byte("b")}, 17 | "bud/bud.go": &virtual.File{Data: []byte("bud")}, 18 | } 19 | fsys := virtual.Exclude(tree, func(path string) bool { 20 | return path == "bud" || strings.HasPrefix(path, "bud/") 21 | }) 22 | des, err := fs.ReadDir(fsys, ".") 23 | is.Equal(err, nil) 24 | is.Equal(len(des), 1) 25 | is.Equal(des[0].Name(), "view") 26 | } 27 | -------------------------------------------------------------------------------- /package/virtual/fileinfo.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "path" 6 | "time" 7 | ) 8 | 9 | // A fileInfo implements fs.FileInfo and fs.DirEntry for a given map file. 10 | type fileInfo struct { 11 | path string 12 | size int64 13 | mode fs.FileMode 14 | modTime time.Time 15 | } 16 | 17 | var _ fs.FileInfo = (*fileInfo)(nil) 18 | var _ fs.DirEntry = (*fileInfo)(nil) 19 | 20 | func (i *fileInfo) Name() string { return path.Base(i.path) } 21 | func (i *fileInfo) Mode() fs.FileMode { return fs.FileMode(i.mode) } 22 | func (i *fileInfo) Type() fs.FileMode { return i.mode.Type() } 23 | func (i *fileInfo) ModTime() time.Time { return i.modTime } 24 | func (i *fileInfo) IsDir() bool { return i.mode&fs.ModeDir != 0 } 25 | func (i *fileInfo) Sys() interface{} { return nil } 26 | func (i *fileInfo) Info() (fs.FileInfo, error) { return i, nil } 27 | func (i *fileInfo) Size() int64 { return i.size } 28 | -------------------------------------------------------------------------------- /package/virtual/from.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io" 5 | "io/fs" 6 | ) 7 | 8 | // From a file to a virtual file 9 | func From(file fs.File) (entry *File, err error) { 10 | // Get the stats 11 | stat, err := file.Stat() 12 | if err != nil { 13 | return nil, err 14 | } 15 | // Copy the directory data over 16 | if stat.IsDir() { 17 | return fromDir(file, stat) 18 | } 19 | return fromFile(file, stat) 20 | } 21 | 22 | func fromDir(file fs.File, stat fs.FileInfo) (entry *File, err error) { 23 | vdir := &File{ 24 | Path: stat.Name(), 25 | ModTime: stat.ModTime(), 26 | Mode: stat.Mode(), 27 | } 28 | if dir, ok := file.(fs.ReadDirFile); ok { 29 | des, err := dir.ReadDir(-1) 30 | if err != nil { 31 | return nil, err 32 | } 33 | vdir.Entries = append(vdir.Entries, des...) 34 | } 35 | return vdir, nil 36 | } 37 | 38 | func fromFile(file fs.File, stat fs.FileInfo) (entry *File, err error) { 39 | // Read the data fully 40 | data, err := io.ReadAll(file) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &File{ 45 | Path: stat.Name(), 46 | Data: data, 47 | ModTime: stat.ModTime(), 48 | Mode: stat.Mode(), 49 | }, nil 50 | } 51 | -------------------------------------------------------------------------------- /package/virtual/json.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "encoding/json" 5 | "io/fs" 6 | "time" 7 | ) 8 | 9 | func MarshalJSON(file fs.File) ([]byte, error) { 10 | entry, err := From(file) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return json.Marshal(entry) 15 | } 16 | 17 | type jsonEntry struct { 18 | Path string 19 | Data []byte 20 | Mode fs.FileMode 21 | ModTime time.Time 22 | Sys interface{} 23 | Entries []*DirEntry 24 | } 25 | 26 | func (f *jsonEntry) Open() fs.File { 27 | if f.Mode.IsDir() { 28 | entries := make([]fs.DirEntry, len(f.Entries)) 29 | for i, entry := range f.Entries { 30 | entries[i] = entry 31 | } 32 | return &openDir{&File{ 33 | Path: f.Path, 34 | Mode: f.Mode, 35 | ModTime: f.ModTime, 36 | Entries: entries, 37 | }, 0} 38 | } 39 | return &openFile{&File{ 40 | Path: f.Path, 41 | Data: f.Data, 42 | Mode: f.Mode, 43 | ModTime: f.ModTime, 44 | }, 0} 45 | } 46 | 47 | func UnmarshalJSON(file []byte) (fs.File, error) { 48 | var entry jsonEntry 49 | err := json.Unmarshal(file, &entry) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return entry.Open(), nil 54 | } 55 | -------------------------------------------------------------------------------- /package/virtual/map.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "path" 6 | ) 7 | 8 | type Map map[string]string 9 | 10 | // Map only implements fs.FS because we can't make directories 11 | // or store permission bits in a map. 12 | var _ fs.FS = (Map)(nil) 13 | var _ fs.SubFS = (Map)(nil) 14 | 15 | func (m Map) Open(name string) (fs.File, error) { 16 | if !fs.ValidPath(name) { 17 | return nil, &fs.PathError{ 18 | Op: "Open", 19 | Path: name, 20 | Err: fs.ErrInvalid, 21 | } 22 | } 23 | return toTree(m).Open(name) 24 | } 25 | 26 | func (m Map) Sub(dir string) (fs.FS, error) { 27 | if !fs.ValidPath(dir) { 28 | return nil, &fs.PathError{ 29 | Op: "Sub", 30 | Path: dir, 31 | Err: fs.ErrInvalid, 32 | } 33 | } 34 | return &subMap{dir, m}, nil 35 | } 36 | 37 | func toTree(m map[string]string) Tree { 38 | tree := Tree{} 39 | for path, data := range m { 40 | tree[path] = &File{Data: []byte(data)} 41 | } 42 | return tree 43 | } 44 | 45 | type subMap struct { 46 | dir string 47 | m Map 48 | } 49 | 50 | func (s *subMap) Open(name string) (fs.File, error) { 51 | return s.m.Open(path.Join(s.dir, name)) 52 | } 53 | -------------------------------------------------------------------------------- /package/virtual/map_test.go: -------------------------------------------------------------------------------- 1 | package virtual_test 2 | 3 | import ( 4 | "io/fs" 5 | "testing" 6 | 7 | "github.com/livebud/bud/internal/is" 8 | "github.com/livebud/bud/package/virtual" 9 | ) 10 | 11 | func TestMap(t *testing.T) { 12 | is := is.New(t) 13 | fsys := virtual.Map{ 14 | "bud/view/index.svelte": `

index

`, 15 | "bud/controller/controller.go": `package controller`, 16 | } 17 | 18 | // Read bud/view/index.svelte 19 | code, err := fs.ReadFile(fsys, "bud/view/index.svelte") 20 | is.NoErr(err) 21 | is.Equal(string(code), `

index

`) 22 | 23 | // stat bud/ 24 | stat, err := fs.Stat(fsys, "bud") 25 | is.NoErr(err) 26 | is.Equal(stat.Name(), "bud") 27 | is.Equal(stat.IsDir(), true) 28 | is.Equal(stat.Mode(), fs.FileMode(fs.ModeDir)) 29 | 30 | // Test reading the directory 31 | des, err := fs.ReadDir(fsys, "bud") 32 | is.NoErr(err) 33 | is.Equal(len(des), 2) 34 | is.Equal(des[0].Name(), "controller") 35 | is.Equal(des[1].Name(), "view") 36 | } 37 | -------------------------------------------------------------------------------- /package/virtual/os.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // OS creates a new OS filesystem rooted at the given directory. 10 | // TODO: create an os_windows for opening on multiple drives 11 | // with the same API: 12 | // https://github.com/golang/go/issues/44279#issuecomment-955766528 13 | type OS string 14 | 15 | var _ FS = (OS)("") 16 | 17 | func (dir OS) Open(name string) (fs.File, error) { 18 | if !fs.ValidPath(name) { 19 | return nil, &fs.PathError{Op: "Open", Path: name, Err: fs.ErrInvalid} 20 | } 21 | return os.Open(filepath.Join(string(dir), name)) 22 | } 23 | 24 | func (dir OS) MkdirAll(path string, perm fs.FileMode) error { 25 | if !fs.ValidPath(path) { 26 | return &fs.PathError{Op: "mkdirall", Path: path, Err: fs.ErrInvalid} 27 | } 28 | return os.MkdirAll(filepath.Join(string(dir), path), perm) 29 | } 30 | 31 | func (dir OS) WriteFile(name string, data []byte, perm fs.FileMode) error { 32 | if !fs.ValidPath(name) { 33 | return &fs.PathError{Op: "WriteFile", Path: name, Err: fs.ErrInvalid} 34 | } 35 | return os.WriteFile(filepath.Join(string(dir), name), data, perm) 36 | } 37 | 38 | func (dir OS) RemoveAll(path string) error { 39 | if !fs.ValidPath(path) { 40 | return &fs.PathError{Op: "RemoveAll", Path: path, Err: fs.ErrInvalid} 41 | } 42 | return os.RemoveAll(filepath.Join(string(dir), path)) 43 | } 44 | 45 | func (dir OS) Sub(subdir string) (FS, error) { 46 | if !fs.ValidPath(subdir) { 47 | return nil, &fs.PathError{Op: "Sub", Path: subdir, Err: fs.ErrInvalid} 48 | } 49 | return OS(filepath.Join(string(dir), subdir)), nil 50 | } 51 | -------------------------------------------------------------------------------- /package/virtual/os_test.go: -------------------------------------------------------------------------------- 1 | package virtual_test 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | "testing/fstest" 10 | 11 | "github.com/livebud/bud/internal/is" 12 | "github.com/livebud/bud/package/virtual" 13 | ) 14 | 15 | func TestOSRead(t *testing.T) { 16 | is := is.New(t) 17 | dir := t.TempDir() 18 | is.NoErr(os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)) 19 | is.NoErr(os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b"), 0644)) 20 | is.NoErr(os.MkdirAll(filepath.Join(dir, "c"), 0755)) 21 | is.NoErr(os.WriteFile(filepath.Join(dir, "c/c.txt"), []byte("d"), 0644)) 22 | // Try reading the directory 23 | fsys := virtual.OS(dir) 24 | err := fstest.TestFS(fsys, "a.txt", "b.txt", "c/c.txt") 25 | is.NoErr(err) 26 | } 27 | 28 | func TestOSRemoveAllOutsideFail(t *testing.T) { 29 | is := is.New(t) 30 | dir := t.TempDir() 31 | is.NoErr(os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)) 32 | is.NoErr(os.MkdirAll(filepath.Join(dir, "b"), 0755)) 33 | fsys := virtual.OS(filepath.Join(dir, "b")) 34 | err := fsys.RemoveAll("../a.txt") 35 | is.True(errors.Is(err, fs.ErrInvalid)) 36 | } 37 | 38 | func TestOSRemoveAll(t *testing.T) { 39 | is := is.New(t) 40 | dir := t.TempDir() 41 | is.NoErr(os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)) 42 | fsys := virtual.OS(dir) 43 | err := fsys.RemoveAll("a.txt") 44 | is.NoErr(err) 45 | } 46 | -------------------------------------------------------------------------------- /package/virtual/print.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | 6 | "github.com/livebud/bud/internal/printfs" 7 | ) 8 | 9 | // Print out a virtual filesystem. 10 | func Print(fsys fs.FS) (string, error) { 11 | tree, err := printfs.Walk(fsys) 12 | if err != nil { 13 | return "", err 14 | } 15 | return tree.String(), nil 16 | } 17 | -------------------------------------------------------------------------------- /package/virtual/virtual.go: -------------------------------------------------------------------------------- 1 | package virtual 2 | 3 | import ( 4 | "io/fs" 5 | "time" 6 | ) 7 | 8 | type FS interface { 9 | fs.FS 10 | MkdirAll(path string, perm fs.FileMode) error 11 | WriteFile(name string, data []byte, perm fs.FileMode) error 12 | RemoveAll(path string) error 13 | Sub(path string) (FS, error) 14 | } 15 | 16 | func Open(f *File) fs.File { 17 | if f.Mode.IsDir() { 18 | return &openDir{f, 0} 19 | } 20 | return &openFile{f, 0} 21 | } 22 | 23 | // Now may be overridden for testing purposes 24 | var Now = func() time.Time { 25 | return time.Now() 26 | } 27 | -------------------------------------------------------------------------------- /runtime/transpiler/transpiler_test.go: -------------------------------------------------------------------------------- 1 | package transpiler 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/livebud/bud/internal/is" 7 | ) 8 | 9 | func TestSplitRoot(t *testing.T) { 10 | is := is.New(t) 11 | root, path := splitRoot("foo/bar/baz.svelte") 12 | is.Equal(root, "foo") 13 | is.Equal(path, "bar/baz.svelte") 14 | } 15 | -------------------------------------------------------------------------------- /scripts/_test-v8-pipe/child/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/livebud/bud/internal/extrafile" 10 | "github.com/livebud/bud/package/exe" 11 | "github.com/livebud/bud/package/js/v8client" 12 | "github.com/livebud/bud/package/socket" 13 | ) 14 | 15 | func run(ctx context.Context) error { 16 | listener, err := socket.Listen(":4444") 17 | if err != nil { 18 | return err 19 | } 20 | defer listener.Close() 21 | fileListener, err := listener.File() 22 | if err != nil { 23 | return err 24 | } 25 | v8client, err := v8client.Load(ctx) 26 | if err != nil { 27 | return err 28 | } 29 | if err := v8client.Script("script.js", "const __svelte__ = 3+3"); err != nil { 30 | return err 31 | } 32 | fmt.Println("calling child") 33 | cmd := exe.Command(ctx, "go", "run", "scripts/test-v8-pipe/grandchild/main.go") 34 | cmd.Stdout = os.Stdout 35 | cmd.Stderr = os.Stderr 36 | cmd.Env = os.Environ() 37 | extrafile.Forward(&cmd.ExtraFiles, &cmd.Env, "V8") 38 | extrafile.Inject(&cmd.ExtraFiles, &cmd.Env, "APP", fileListener) 39 | if err := cmd.Run(); err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func main() { 46 | if err := run(context.Background()); err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /scripts/_test-v8-pipe/grandchild/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/livebud/bud/internal/extrafile" 9 | "github.com/livebud/bud/package/js/v8client" 10 | "github.com/livebud/bud/package/socket" 11 | ) 12 | 13 | func run(ctx context.Context) error { 14 | fmt.Println("calling grandchild") 15 | // files := extrafile.Load("V8") 16 | v8client, err := v8client.Load(ctx) 17 | if err != nil { 18 | return err 19 | } 20 | // v8client := v8client.New(files[0], files[1]) 21 | result, err := v8client.Eval("script.js", "__svelte__ + 2") 22 | if err != nil { 23 | return err 24 | } 25 | fmt.Println(result) 26 | appFile := extrafile.Load("APP") 27 | listener, err := socket.From(appFile[0]) 28 | if err != nil { 29 | return err 30 | } 31 | fmt.Println("got listener", listener) 32 | return nil 33 | } 34 | 35 | func main() { 36 | if err := run(context.Background()); err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/_test-v8-pipe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/livebud/bud/internal/extrafile" 10 | 11 | "github.com/livebud/bud/package/exe" 12 | 13 | "github.com/livebud/bud/package/js/v8server" 14 | ) 15 | 16 | func run(ctx context.Context) error { 17 | r1, w2, err := os.Pipe() 18 | if err != nil { 19 | return err 20 | } 21 | // defer w2.Close() 22 | r2, w1, err := os.Pipe() 23 | if err != nil { 24 | return err 25 | } 26 | // defer w1.Close() 27 | v8Server := v8server.New(r1, w1) 28 | go func() { 29 | fmt.Println("err serving", v8Server.Serve()) 30 | }() 31 | 32 | cmd := exe.Command(ctx, "go", "run", "scripts/test-v8-pipe/child/main.go") 33 | cmd.Stdout = os.Stdout 34 | cmd.Stderr = os.Stderr 35 | cmd.Env = os.Environ() 36 | extrafile.Inject(&cmd.ExtraFiles, &cmd.Env, "V8", r2, w2) 37 | if err := cmd.Run(); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func main() { 45 | if err := run(context.Background()); err != nil { 46 | log.Fatal(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/generate-changelog/changelog.gotext: -------------------------------------------------------------------------------- 1 | {{ .Notes }} 2 | -------------------------------------------------------------------------------- /scripts/generate-checksums/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/livebud/bud/package/commander" 12 | "github.com/livebud/bud/package/gomod" 13 | "github.com/livebud/bud/package/log/console" 14 | ) 15 | 16 | func main() { 17 | if err := run(); err != nil { 18 | console.Error(err.Error()) 19 | os.Exit(1) 20 | } 21 | os.Exit(0) 22 | } 23 | 24 | func run() error { 25 | cmd := new(Command) 26 | cli := commander.New("generate-checksums", "generate checksums") 27 | cli.Run(cmd.Run) 28 | return cli.Parse(context.Background(), os.Args...) 29 | } 30 | 31 | type Command struct { 32 | Version string 33 | } 34 | 35 | type State struct { 36 | Notes string 37 | } 38 | 39 | func (c *Command) Run(ctx context.Context) error { 40 | module, err := gomod.Find(".") 41 | if err != nil { 42 | return err 43 | } 44 | paths, err := filepath.Glob(module.Directory("release", "*.tar.gz")) 45 | if err != nil { 46 | return err 47 | } 48 | f, err := os.Create(filepath.Join("release", "checksums.txt")) 49 | if err != nil { 50 | return err 51 | } 52 | defer f.Close() 53 | for _, path := range paths { 54 | sha, err := computeSHA(path) 55 | if err != nil { 56 | return err 57 | } 58 | if _, err := f.Write([]byte(sha + " " + filepath.Base(path) + "\n")); err != nil { 59 | return err 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | func computeSHA(path string) (string, error) { 66 | h := sha256.New() 67 | f, err := os.Open(path) 68 | if err != nil { 69 | return "", err 70 | } 71 | defer f.Close() 72 | if _, err := io.Copy(h, f); err != nil { 73 | return "", err 74 | } 75 | return hex.EncodeToString(h.Sum(nil)), nil 76 | } 77 | -------------------------------------------------------------------------------- /scripts/set-package-json/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/livebud/bud/internal/npm" 8 | "github.com/livebud/bud/internal/versions" 9 | "github.com/livebud/bud/package/gomod" 10 | "github.com/livebud/bud/package/log/console" 11 | ) 12 | 13 | func main() { 14 | if err := run(); err != nil { 15 | console.Error(err.Error()) 16 | os.Exit(1) 17 | } 18 | os.Exit(0) 19 | } 20 | 21 | func run() error { 22 | dir, err := gomod.Absolute(".") 23 | if err != nil { 24 | return err 25 | } 26 | // Update the dependencies in ./livebud/package.json 27 | if err := npm.Set(filepath.Join(dir, "livebud"), map[string]string{ 28 | "dependencies.svelte": versions.Svelte, 29 | "dependencies.react": versions.React, 30 | "dependencies.react-dom": versions.React, 31 | "devDependencies.@types/react": versions.React, 32 | "devDependencies.@types/react-dom": versions.React, 33 | }); err != nil { 34 | return err 35 | } 36 | // Update the dependencies in . 37 | if err := npm.Set(dir, map[string]string{ 38 | "devDependencies.svelte": versions.Svelte, 39 | "devDependencies.react": versions.React, 40 | "devDependencies.react-dom": versions.React, 41 | "devDependencies.@types/react": versions.React, 42 | "devDependencies.@types/react-dom": versions.React, 43 | }); err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /scripts/svelte/error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Oh no! 7 | 8 | 9 |

Oh no!

10 | 11 |
{message}
12 | -------------------------------------------------------------------------------- /scripts/svelte/index.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Hello {planet}!

8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/svelte/layout.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/svelte/show.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Hello {planet} from {id}

8 | 9 | 10 | -------------------------------------------------------------------------------- /scripts/test-socket-passthrough/child/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | 11 | "golang.org/x/sync/errgroup" 12 | 13 | "github.com/livebud/bud/internal/sig" 14 | 15 | "github.com/livebud/bud/package/socket" 16 | 17 | "github.com/livebud/bud/internal/extrafile" 18 | ) 19 | 20 | func main() { 21 | if err := run(sig.Trap(context.Background(), os.Interrupt)); err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | func run(ctx context.Context) error { 27 | files := extrafile.Load("APP") 28 | if len(files) == 0 { 29 | return fmt.Errorf("no files passed through") 30 | } 31 | listener, err := socket.From(files[0]) 32 | if err != nil { 33 | return err 34 | } 35 | server := &http.Server{ 36 | Addr: ":0", 37 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | w.Write([]byte("hello world")) 39 | }), 40 | } 41 | eg := new(errgroup.Group) 42 | eg.Go(func() error { 43 | fmt.Println("listening on", listener.Addr()) 44 | return server.Serve(listener) 45 | }) 46 | <-ctx.Done() 47 | if err := server.Shutdown(context.Background()); err != nil { 48 | return err 49 | } 50 | if err := eg.Wait(); err != nil { 51 | if errors.Is(err, http.ErrServerClosed) { 52 | return nil 53 | } 54 | return err 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /scripts/test-subprocess-interrupt/child/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/livebud/bud/internal/sig" 10 | ) 11 | 12 | func main() { 13 | fmt.Println("child: started") 14 | ctx := sig.Trap(context.Background(), os.Interrupt) 15 | select { 16 | case <-ctx.Done(): 17 | fmt.Println("child: interrupted!") 18 | time.Sleep(time.Second) 19 | fmt.Println("child: exiting") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scripts/test-subprocess-interrupt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/livebud/bud/internal/current" 11 | "github.com/livebud/bud/internal/sig" 12 | ) 13 | 14 | func main() { 15 | if err := run(context.Background()); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | 20 | func run(ctx context.Context) error { 21 | ctx = sig.Trap(ctx, os.Interrupt) 22 | dirname, err := current.Directory() 23 | if err != nil { 24 | return err 25 | } 26 | // Build child 27 | cmd := exec.Command("go", "build", "-o", "child/main", "child/main.go") 28 | cmd.Stdout = os.Stdout 29 | cmd.Stderr = os.Stderr 30 | cmd.Dir = dirname 31 | cmd.Env = os.Environ() 32 | if err := cmd.Run(); err != nil { 33 | return err 34 | } 35 | // Run child 36 | cmd = exec.Command("child/main") 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | cmd.Env = os.Environ() 40 | cmd.Dir = dirname 41 | if err := cmd.Start(); err != nil { 42 | return err 43 | } 44 | select { 45 | case <-ctx.Done(): 46 | fmt.Println("parent: interrupted!") 47 | if err := cmd.Wait(); err != nil { 48 | return err 49 | } 50 | fmt.Println("parent: exiting") 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /scripts/test-watcher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/livebud/bud/internal/current" 10 | 11 | "github.com/livebud/bud/internal/sig" 12 | "github.com/livebud/bud/package/watcher" 13 | ) 14 | 15 | func run(ctx context.Context) error { 16 | dirname, err := current.Directory() 17 | if err != nil { 18 | return err 19 | } 20 | ctx = sig.Trap(ctx, os.Interrupt) 21 | return watcher.Watch(ctx, dirname, func(events []watcher.Event) error { 22 | fmt.Println("-> triggered", events) 23 | return nil 24 | }) 25 | } 26 | 27 | func main() { 28 | ctx := context.Background() 29 | if err := run(ctx); err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // Tools we depend on. This file is here to prevent `go mod tidy` from cleaning 5 | // up these dependencies 6 | package bud 7 | 8 | import ( 9 | _ "github.com/evanw/esbuild/cmd/esbuild" 10 | _ "github.com/hexops/valast" 11 | _ "github.com/livebud/bud-test-plugin" 12 | _ "github.com/pointlander/peg" 13 | _ "honnef.co/go/tools/cmd/staticcheck" 14 | _ "src.techknowlogick.com/xgo" 15 | ) 16 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.2.8 --------------------------------------------------------------------------------