├── .gitignore ├── LICENSE ├── README.md ├── actions └── actions.go ├── index.html ├── index.jsgo.html ├── main.go ├── main_test.go ├── models ├── modals.go └── share.go ├── stores ├── app.go ├── archive.go ├── builderjs │ └── builderjs.go ├── compile.go ├── connection.go ├── deploy.go ├── editor.go ├── empty.go ├── history.go ├── local.go ├── page.go ├── request.go ├── scanner.go ├── share.go └── source.go ├── testing ├── a │ └── a.go ├── b │ └── b.go └── main │ └── main.go └── views ├── add-file.go ├── add-package.go ├── build-tags.go ├── clash-warning.go ├── delete-file.go ├── deploy-done.go ├── editor.go ├── help.go ├── load-package.go ├── menu.go ├── modal.go ├── page.go └── remove-package.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .idea/ 4 | coverage.out 5 | .coverage/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 David Brophy 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 2020 Update 4 | 5 | * The compile.jsgo.io and play.jsgo.io services have been shut down. 6 | * Anything deployed to jsgo.io or pkg.jsgo.io will continue to work fine. 7 | 8 | I created the jsgo.io system several years ago, and it costs about $150/month to host which I pay 9 | personally. I'm tightening up my finances right now, so this outgoing had to stop. 10 | 11 | If anyone would like to host it for me (it runs on a single GKE `n1-standard-2` instance), please 12 | let me know and we can get it back online! 13 | 14 | I had a plan for a big rewrite that would make is possible to run on App Engine, thus reduce the 15 | cost to almost zero. Unfortunately this is something I'm hesitant to start, because it seems that 16 | Go on the client is moving away from GopherJS and towards WASM. 17 | 18 | # play.jsgo.io 19 | 20 | Edit and run Go in the browser, supporting arbitrary import paths! 21 | 22 | https://play.jsgo.io/ 23 | 24 | [title936803092](https://play.jsgo.io/) 25 | 26 | The jsgo playground is an extension of the jsgo compiler. The compiler allows you to easily compile Go 27 | to JS using GopherJS, and automatically host the results in an aggressively cached CDN. The playground 28 | adds an online editor and many other features (see below). 29 | 30 | The unique feature of the jsgo playground is that it supports arbitrary import paths. Other Go playgrounds 31 | are limited to just the Go standard library. 32 | 33 | For more for more info: 34 | 35 | * jsgo compiler: https://github.com/dave/jsgo 36 | * jsgo playground: https://github.com/dave/play 37 | 38 | ## Demos 39 | 40 | Here's the simplest demo - it just writes to the console and to the page: 41 | 42 | * https://play.jsgo.io/github.com/dave/jstest 43 | 44 | Here's a couple of simple demos that accept files by drag and drop. The first compresses dropped files to 45 | a zip. The second compresses images to jpg. They use the Go standard library zip / image libraries, which 46 | work flawlessly in the browser: 47 | 48 | * https://play.jsgo.io/github.com/dave/zip 49 | * https://play.jsgo.io/github.com/dave/img 50 | 51 | The amazing ebiten 2D games library is a perfect example of the power of Go in the browser. Here's some 52 | demos: 53 | 54 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/2048 55 | * https://play.jsgo.io/github.com/hajimehoshi/go-inovation 56 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/flappy 57 | 58 | ## Contact 59 | 60 | If you'd like to chat more about the project, feel free to [add an issue](https://github.com/dave/play/issues), 61 | mention [@dave](https://github.com/dave/) or post in the #gopherjs channel of the Gophers Slack. I'm 62 | happy to help! 63 | 64 | ## Features 65 | 66 | #### Initialise 67 | The URL can be used to initialise with code in several ways: 68 | 69 | * Load a Go package with `/{{ Package path }}` 70 | * Load a Github Gist with `/gist.github.com/{{ Gist ID }}` 71 | * Load a shared project with `/{{ Share ID }}` 72 | * Load a `play.golang.org` share with `/p/{{ Go playground ID }}` 73 | 74 | run 75 | 76 | #### Run 77 | Click the `Run` button to run your code in the right-hand panel. If the imports have been changed recently, 78 | the dependencies will be refreshed before running. 79 | 80 |
81 | 82 | format 83 | 84 | #### Format code 85 | Use the `Format code` option to run `gofmt` on your code. This is executed automatically when the `Run`, 86 | `Update`, `Share` or `Deploy` features are used. 87 | 88 |
89 | 90 | update 91 | 92 | #### Update 93 | If you update a dependency, use the `Update` option, which does the equivalent of `go get -u` and refreshes 94 | the changes in any import or dependency. 95 | 96 |
97 | 98 | share 99 | 100 | #### Share 101 | To share your project with others, use the `Share` option. Your project will be persisted to a json file 102 | on `src.jsgo.io` and the page will update to a sharable URL. 103 | 104 |
105 | 106 | deploy 107 | 108 | #### Deploy 109 | To deploy your code to [jsgo.io](https://jsgo.io), use the `Deploy` feature. A modal will be displayed with the 110 | link to the page on `jsgo.io`, and the Loader JS on `pkg.jsgo.io`. 111 | 112 | Use the `jsgo.io` link for testing and toy projects. Remember you're sharing the `jsgo.io` domain with 113 | everyone else, so the browser environment should be considered toxic. 114 | 115 | The Loader JS on `pkg.jsgo.io` can be used in production, and should be added to a script tag on your 116 | own website. See [github.com/dave/jsgo](https://github.com/dave/jsgo) for more information. 117 | 118 |
119 | 120 | console 121 | 122 | #### Console 123 | Writes to `os.Stdout` are redirected to a playground console, which can be toggled using the `Show console` 124 | option. The console will automatically appear the first time it's written to. 125 | 126 |
127 | 128 | minify 129 | 130 | #### Minify 131 | In normal usage, all JS is minified. For debugging, this can be toggled with the `Minify JS` option. 132 | 133 |
134 | 135 | tags 136 | 137 | #### Build tags 138 | The build tags used when compiling can be edited with the `Build tags...` option. The selected build 139 | tags are persisted when using the `Share` feature. 140 | 141 |
142 | 143 | download 144 | 145 | #### Download 146 | The `Download` option downloads the project. Single file projects are downloaded as a single file, while 147 | multi-file projects download as a zip. 148 | 149 |
150 | 151 | download 152 | 153 | #### Upload 154 | Files can be uploaded to the project simply by drag+drop. Zip files generated by the `Download` feature 155 | can be uploaded to restore a multi-file project. 156 | 157 |
158 | 159 | files 160 | 161 | #### File menu 162 | Change the selected file with the file menu. 163 | 164 |
165 | 166 | add-file 167 | 168 | #### Add file 169 | Add a file to the current package with the `Add file` option. Only `.go`, `.md` and `.inc.js` files are 170 | supported. If no extension is supplied, `.go` is added. 171 | 172 |
173 | 174 | delete-file 175 | 176 | #### Delete file 177 | Delete a file from the current package with the `Delete file` option. 178 | 179 |
180 | 181 | package 182 | 183 | #### Package menu 184 | Change the selected package with the package menu. 185 | 186 |
187 | 188 | add-package 189 | 190 | #### Add package 191 | Add an empty package with the `Add package` option. 192 | 193 |
194 | 195 | load-package 196 | 197 | #### Load package 198 | The source for an import or dependency can be loaded with the `Load package` option. By default, only 199 | the direct imports of your project are listed. Use the `Show all dependencies` option to show the entire 200 | dependency tree. 201 | 202 |
203 | 204 | remove-package 205 | 206 | #### Remove package 207 | A package can be removed with the `Remove package` option. 208 | 209 | ## Run locally? 210 | 211 | If you'd like to run `play.jsgo.io` locally, take a look at [these instructions](https://github.com/dave/jsgo/blob/master/LOCAL.md). -------------------------------------------------------------------------------- /actions/actions.go: -------------------------------------------------------------------------------- 1 | package actions 2 | 3 | import ( 4 | "github.com/dave/dropper" 5 | "github.com/dave/flux" 6 | "github.com/dave/play/models" 7 | "github.com/dave/services" 8 | ) 9 | 10 | type Load struct{} 11 | 12 | type ConsoleFirstWrite struct{} 13 | type ConsoleToggleClick struct{} 14 | type MinifyToggleClick struct{} 15 | 16 | type ShowAllDepsChange struct{ State bool } 17 | 18 | type ChangeSplit struct{ Sizes []float64 } 19 | type ChangeFile struct { 20 | Path string 21 | Name string 22 | } 23 | 24 | type LoadSource struct { 25 | Source map[string]map[string]string 26 | Tags []string 27 | CurrentPackage string 28 | CurrentFile string 29 | Save bool // Save directly after loading? false during initialising, true for load package. 30 | Update bool // Update directly after loading? 31 | } 32 | 33 | type UserChangedSplit struct{ Sizes []float64 } 34 | type UserChangedText struct { 35 | Text string 36 | Changed bool 37 | } 38 | type UserChangedFile struct{ Name string } 39 | type UserChangedPackage struct{ Path string } 40 | 41 | type ModalOpen struct{ Modal models.Modal } 42 | type ModalClose struct{ Modal models.Modal } 43 | 44 | type DownloadClick struct{} 45 | type BuildTags struct{ Tags []string } 46 | 47 | type AddFile struct{ Name string } 48 | type AddPackage struct{ Path string } 49 | type DeleteFile struct{ Name string } 50 | type RemovePackage struct{ Path string } 51 | 52 | type FormatCode struct{ Then flux.ActionInterface } 53 | 54 | // CompileStart compiles the app and injects the js into the iframe 55 | type CompileStart struct{} 56 | 57 | type DragEnter struct{} 58 | type DragLeave struct{} 59 | type DragDrop struct { 60 | Files []dropper.File 61 | Changed map[string]map[string]bool 62 | } 63 | 64 | type Send struct{ Message services.Message } 65 | type Dial struct { 66 | Url string 67 | Open func() flux.ActionInterface 68 | Message func(interface{}) flux.ActionInterface 69 | Close func() flux.ActionInterface 70 | } 71 | 72 | type ShareStart struct{} 73 | type ShareOpen struct{} 74 | type ShareMessage struct{ Message interface{} } 75 | type ShareClose struct{} 76 | 77 | type DeployStart struct{} 78 | type DeployOpen struct{} 79 | type DeployMessage struct{ Message interface{} } 80 | type DeployClose struct{} 81 | 82 | type RequestStart struct { 83 | Type models.RequestType 84 | Path string // Path to get (for GetRequest and InitialiseRequest) 85 | Run bool // Run after update? (for UpdateRequest) 86 | } 87 | type RequestOpen struct { 88 | *RequestStart 89 | } 90 | type RequestMessage struct { 91 | *RequestStart 92 | Message interface{} 93 | } 94 | type RequestClose struct { 95 | *RequestStart 96 | } 97 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /index.jsgo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dave/play/actions" 5 | "github.com/dave/play/stores" 6 | "github.com/dave/play/views" 7 | "github.com/gopherjs/vecty" 8 | "github.com/vincent-petithory/dataurl" 9 | "honnef.co/go/js/dom" 10 | ) 11 | 12 | var document = dom.GetWindow().Document().(dom.HTMLDocument) 13 | 14 | func main() { 15 | if document.ReadyState() == "loading" { 16 | document.AddEventListener("DOMContentLoaded", false, func(dom.Event) { 17 | go run() 18 | }) 19 | } else { 20 | go run() 21 | } 22 | } 23 | 24 | func run() { 25 | 26 | vecty.AddStylesheet(dataurl.New([]byte(views.Styles), "text/css").String()) 27 | 28 | app := &stores.App{} 29 | app.Init() 30 | p := views.NewPage(app) 31 | 32 | app.Watch(nil, func(done chan struct{}) { 33 | defer close(done) 34 | vecty.Rerender(p) 35 | }) 36 | 37 | app.Dispatch(&actions.Load{}) 38 | 39 | vecty.RenderBody(p) 40 | } 41 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestPlay(t *testing.T) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /models/modals.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Modal string 4 | 5 | const ( 6 | ClashWarningModal Modal = "clash-warning-modal" 7 | AddPackageModal Modal = "add-package-modal" 8 | AddFileModal Modal = "add-file-modal" 9 | DeleteFileModal Modal = "delete-file-modal" 10 | DeployDoneModal Modal = "deploy-done-modal" 11 | LoadPackageModal Modal = "load-package-modal" 12 | RemovePackageModal Modal = "remove-package-modal" 13 | BuildTagsModal Modal = "build-tags-modal" 14 | HelpModal Modal = "help-modal" 15 | ) 16 | 17 | type RequestType string 18 | 19 | const ( 20 | GetRequest RequestType = "get" 21 | UpdateRequest RequestType = "update" 22 | InitialiseRequest RequestType = "initialise" 23 | ) 24 | -------------------------------------------------------------------------------- /models/share.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // SharePack is the structure of the data persisted on src.jsgo.io as json, so best to use json tags 4 | // to lower-case the names. 5 | type SharePack struct { 6 | Version int `json:"version"` 7 | Source map[string]map[string]string `json:"source"` // Source packages for this build: map[]map[] 8 | Tags []string `json:"tags"` // Build tags 9 | } 10 | -------------------------------------------------------------------------------- /stores/app.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "honnef.co/go/js/dom" 8 | 9 | "strings" 10 | 11 | "time" 12 | 13 | "github.com/dave/flux" 14 | "github.com/gopherjs/gopherjs/js" 15 | ) 16 | 17 | type App struct { 18 | Dispatcher flux.DispatcherInterface 19 | Watcher flux.WatcherInterface 20 | Notifier flux.NotifierInterface 21 | 22 | Archive *ArchiveStore 23 | Editor *EditorStore 24 | Connection *ConnectionStore 25 | Local *LocalStore 26 | Scanner *ScannerStore 27 | Compile *CompileStore 28 | Share *ShareStore 29 | Request *RequestStore 30 | Deploy *DeployStore 31 | Page *PageStore 32 | Source *SourceStore 33 | History *HistoryStore 34 | } 35 | 36 | func (a *App) Init() { 37 | 38 | n := flux.NewNotifier() 39 | a.Notifier = n 40 | a.Watcher = n 41 | 42 | a.Archive = NewArchiveStore(a) 43 | a.Editor = NewEditorStore(a) 44 | a.Connection = NewConnectionStore(a) 45 | a.Local = NewLocalStore(a) 46 | a.Scanner = NewScannerStore(a) 47 | a.Compile = NewCompileStore(a) 48 | a.Share = NewShareStore(a) 49 | a.Request = NewRequestStore(a) 50 | a.Deploy = NewDeployStore(a) 51 | a.Page = NewPageStore(a) 52 | a.Source = NewSourceStore(a) 53 | a.History = NewHistoryStore(a) 54 | 55 | a.Dispatcher = flux.NewDispatcher( 56 | // Notifier: 57 | a.Notifier, 58 | // Stores: 59 | a.Archive, 60 | a.Editor, 61 | a.Connection, 62 | a.Local, 63 | a.Scanner, 64 | a.Compile, 65 | a.Share, 66 | a.Request, 67 | a.Deploy, 68 | a.Page, 69 | a.Source, 70 | a.History, 71 | ) 72 | } 73 | 74 | func (a *App) Dispatch(action flux.ActionInterface) chan struct{} { 75 | return a.Dispatcher.Dispatch(action) 76 | } 77 | 78 | func (a *App) Watch(key interface{}, f func(done chan struct{})) { 79 | a.Watcher.Watch(key, f) 80 | } 81 | 82 | func (a *App) Delete(key interface{}) { 83 | a.Watcher.Delete(key) 84 | } 85 | 86 | func (a *App) Fail(err error) { 87 | // TODO: improve this 88 | js.Global.Call("alert", err.Error()) 89 | } 90 | 91 | func (a *App) Debug(message ...interface{}) { 92 | js.Global.Get("console").Call("log", message...) 93 | } 94 | 95 | var lastLog *struct{} 96 | 97 | // LogHide hides the message after 2 seconds 98 | func (a *App) LogHide(args ...interface{}) { 99 | a.Log(args...) 100 | if len(args) > 0 { 101 | // clear message after 2 sec if not changed 102 | before := lastLog 103 | go func() { 104 | <-time.After(time.Second * 2) 105 | if before == lastLog { 106 | a.Log() 107 | } 108 | }() 109 | } 110 | } 111 | 112 | func (a *App) Log(args ...interface{}) { 113 | m := dom.GetWindow().Document().GetElementByID("message") 114 | var message string 115 | if len(args) > 0 { 116 | message = strings.TrimSuffix(fmt.Sprintln(args...), "\n") 117 | } 118 | if m.InnerHTML() != message { 119 | if message != "" { 120 | js.Global.Get("console").Call("log", "Status", strconv.Quote(message)) 121 | } 122 | requestAnimationFrame() 123 | m.SetInnerHTML(message) 124 | requestAnimationFrame() 125 | lastLog = &struct{}{} 126 | } 127 | } 128 | 129 | func (a *App) Logf(format string, args ...interface{}) { 130 | a.Log(fmt.Sprintf(format, args...)) 131 | } 132 | 133 | func (a *App) LogHidef(format string, args ...interface{}) { 134 | a.LogHide(fmt.Sprintf(format, args...)) 135 | } 136 | 137 | func requestAnimationFrame() { 138 | c := make(chan struct{}) 139 | js.Global.Call("requestAnimationFrame", func() { close(c) }) 140 | <-c 141 | } 142 | -------------------------------------------------------------------------------- /stores/archive.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go/types" 7 | 8 | "encoding/gob" 9 | 10 | "errors" 11 | 12 | "net/http" 13 | 14 | "sync" 15 | 16 | "io/ioutil" 17 | 18 | "github.com/dave/flux" 19 | "github.com/dave/jsgo/config" 20 | "github.com/dave/play/actions" 21 | "github.com/dave/play/models" 22 | "github.com/dave/play/stores/builderjs" 23 | "github.com/dave/services/deployer/deployermsg" 24 | "github.com/gopherjs/gopherjs/compiler" 25 | ) 26 | 27 | type ArchiveStore struct { 28 | app *App 29 | 30 | // cache (path -> item) of archives 31 | cache map[string]CacheItem 32 | 33 | // index (path -> item) of the previously received update 34 | index deployermsg.ArchiveIndex 35 | 36 | wait sync.WaitGroup 37 | } 38 | 39 | type CacheItem struct { 40 | Hash string 41 | Archive *compiler.Archive // This archive is stripped of JS 42 | Js []byte 43 | } 44 | 45 | func NewArchiveStore(app *App) *ArchiveStore { 46 | s := &ArchiveStore{ 47 | app: app, 48 | cache: map[string]CacheItem{}, 49 | } 50 | return s 51 | } 52 | 53 | type Dep struct { 54 | Path string 55 | Js []byte 56 | } 57 | 58 | func (s *ArchiveStore) Compile(path string, tags []string) ([]Dep, error) { 59 | done := make(map[string]bool) 60 | archives := map[string]*compiler.Archive{} 61 | packages := map[string]*types.Package{} 62 | jsdeps := []Dep{ 63 | // Always start with the prelude 64 | {Path: "prelude", Js: s.cache["prelude"].Js}, 65 | } 66 | var deps []*compiler.Archive 67 | var compile func(path string) error 68 | compile = func(path string) error { 69 | if done[path] { 70 | return nil 71 | } 72 | if s.app.Source.HasPackage(path) { 73 | for _, imp := range s.app.Scanner.Imports(path) { 74 | if err := compile(imp); err != nil { 75 | return err 76 | } 77 | } 78 | archive, err := builderjs.BuildPackage( 79 | path, 80 | s.app.Source.Source(), 81 | tags, 82 | deps, 83 | s.app.Page.Minify(), 84 | archives, 85 | packages, 86 | ) 87 | if err != nil { 88 | return err 89 | } 90 | js, _, err := builderjs.GetPackageCode(context.Background(), archive, false, true) 91 | if err != nil { 92 | return err 93 | } 94 | jsdeps = append(jsdeps, Dep{path, js}) 95 | deps = append(deps, archive) 96 | done[path] = true 97 | return nil 98 | } 99 | item, ok := s.cache[path] 100 | if !ok { 101 | return fmt.Errorf("%s not found", path) 102 | } 103 | for _, imp := range item.Archive.Imports { 104 | if err := compile(imp); err != nil { 105 | return err 106 | } 107 | } 108 | jsdeps = append(jsdeps, Dep{path, item.Js}) 109 | deps = append(deps, item.Archive) 110 | done[path] = true 111 | return nil 112 | } 113 | if err := compile("runtime"); err != nil { 114 | return nil, err 115 | } 116 | if err := compile(path); err != nil { 117 | return nil, err 118 | } 119 | return jsdeps, nil 120 | } 121 | 122 | func (s *ArchiveStore) AllFresh() bool { 123 | for path := range s.app.Scanner.MainPackages() { 124 | if !s.Fresh(path) { 125 | return false 126 | } 127 | } 128 | return true 129 | } 130 | 131 | // Fresh is true if current cache matches the previously downloaded archives 132 | func (s *ArchiveStore) Fresh(mainPath string) bool { 133 | // if index is nil, either the page has just loaded or we're in the middle of an update 134 | if s.index == nil { 135 | return false 136 | } 137 | 138 | // first check that all indexed packages are in the cache at the right versions. This would fail 139 | // if there was an error while downloading one of the archive files. 140 | for path, item := range s.index { 141 | cached, ok := s.cache[path] 142 | if !ok { 143 | return false 144 | } 145 | if cached.Hash != item.Hash { 146 | return false 147 | } 148 | } 149 | 150 | // then check that all the imports in all packages are found in the index, or in the source 151 | for _, path := range s.app.Scanner.Imports(mainPath) { 152 | _, inIndex := s.index[path] 153 | _, inSource := s.app.Source.Source()[path] 154 | if !inIndex && !inSource { 155 | return false 156 | } 157 | } 158 | 159 | return true 160 | } 161 | 162 | func (s *ArchiveStore) Cache() map[string]CacheItem { 163 | return s.cache 164 | } 165 | 166 | func (s *ArchiveStore) CacheStrings() map[string]string { 167 | hashes := map[string]string{} 168 | for path, item := range s.app.Archive.Cache() { 169 | hashes[path] = item.Hash 170 | } 171 | return hashes 172 | } 173 | 174 | func (s *ArchiveStore) Handle(payload *flux.Payload) bool { 175 | switch a := payload.Action.(type) { 176 | case *actions.MinifyToggleClick: 177 | payload.Wait(s.app.Page) 178 | s.index = nil 179 | s.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest, Run: false}) 180 | case *actions.LoadSource: 181 | payload.Wait(s.app.Scanner) 182 | if a.Update && !s.AllFresh() { 183 | s.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest, Run: false}) 184 | } 185 | case *actions.RequestMessage: 186 | switch message := a.Message.(type) { 187 | case deployermsg.Archive: 188 | s.wait.Add(1) 189 | go func() { 190 | defer s.wait.Done() 191 | c := CacheItem{ 192 | Hash: message.Hash, 193 | } 194 | var getwait sync.WaitGroup 195 | getwait.Add(2) 196 | go func() { 197 | defer getwait.Done() 198 | if message.Path == "prelude" { 199 | // prelude doesn't have an archive file 200 | return 201 | } 202 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.%s.ax", config.Protocol[config.Pkg], config.Host[config.Pkg], message.Path, message.Hash)) 203 | if err != nil { 204 | s.app.Fail(err) 205 | return 206 | } 207 | var a compiler.Archive 208 | if err := gob.NewDecoder(resp.Body).Decode(&a); err != nil { 209 | s.app.Fail(err) 210 | return 211 | } 212 | c.Archive = &a 213 | }() 214 | go func() { 215 | defer getwait.Done() 216 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.%s.js", config.Protocol[config.Pkg], config.Host[config.Pkg], message.Path, message.Hash)) 217 | if err != nil { 218 | s.app.Fail(err) 219 | return 220 | } 221 | js, err := ioutil.ReadAll(resp.Body) 222 | if err != nil { 223 | s.app.Fail(err) 224 | return 225 | } 226 | c.Js = js 227 | }() 228 | getwait.Wait() 229 | s.cache[message.Path] = c 230 | if message.Path == "prelude" { 231 | // prelude doesn't have an archive file 232 | s.app.Log("prelude") 233 | } else { 234 | s.app.Log(c.Archive.Name) 235 | } 236 | }() 237 | return true 238 | case deployermsg.ArchiveIndex: 239 | s.index = message 240 | } 241 | case *actions.RequestClose: 242 | 243 | if a.Type == models.GetRequest { 244 | // get request doesn't do an update - just gets files 245 | return true 246 | } 247 | 248 | s.wait.Wait() 249 | 250 | if !s.AllFresh() { 251 | s.app.Fail(errors.New("websocket closed but archives not updated")) 252 | return true 253 | } 254 | 255 | if a.Run { 256 | s.app.Dispatch(&actions.CompileStart{}) 257 | } else { 258 | var downloaded, unchanged int 259 | for _, v := range s.index { 260 | if v.Unchanged { 261 | unchanged++ 262 | } else { 263 | downloaded++ 264 | } 265 | } 266 | if downloaded == 0 && unchanged == 0 { 267 | s.app.Log() 268 | } else if downloaded > 0 && unchanged > 0 { 269 | s.app.LogHidef("%d downloaded, %d unchanged", downloaded, unchanged) 270 | } else if downloaded > 0 { 271 | s.app.LogHidef("%d downloaded", downloaded) 272 | } else if unchanged > 0 { 273 | s.app.LogHidef("%d unchanged", unchanged) 274 | } 275 | } 276 | payload.Notify() 277 | } 278 | 279 | return true 280 | } 281 | -------------------------------------------------------------------------------- /stores/builderjs/builderjs.go: -------------------------------------------------------------------------------- 1 | package builderjs 2 | 3 | import ( 4 | "context" 5 | "go/token" 6 | "go/types" 7 | "sort" 8 | 9 | "go/ast" 10 | "go/parser" 11 | 12 | "fmt" 13 | 14 | "strings" 15 | 16 | "bytes" 17 | "crypto/sha1" 18 | 19 | "github.com/dave/services/includer" 20 | "github.com/gopherjs/gopherjs/compiler" 21 | "golang.org/x/tools/go/gcexportdata" 22 | ) 23 | 24 | func BuildPackage(path string, source map[string]map[string]string, tags []string, deps []*compiler.Archive, minify bool, archives map[string]*compiler.Archive, packages map[string]*types.Package) (*compiler.Archive, error) { 25 | 26 | for _, a := range deps { 27 | if archives[a.ImportPath] == nil { 28 | archives[a.ImportPath] = a 29 | } 30 | if packages[a.ImportPath] == nil { 31 | p, err := gcexportdata.Read(bytes.NewReader(a.ExportData), token.NewFileSet(), packages, a.ImportPath) 32 | if err != nil { 33 | return nil, err 34 | } 35 | packages[a.ImportPath] = p 36 | } 37 | } 38 | 39 | fset := token.NewFileSet() 40 | 41 | var importContext *compiler.ImportContext 42 | importContext = &compiler.ImportContext{ 43 | Packages: packages, 44 | Import: func(imp string) (*compiler.Archive, error) { 45 | a, ok := archives[imp] 46 | if ok { 47 | return a, nil 48 | } 49 | sourceFiles, ok := source[imp] 50 | if ok { 51 | // We have the source for this dep 52 | archive, err := compileFiles(fset, imp, tags, sourceFiles, importContext, minify) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return archive, nil 57 | } 58 | return nil, fmt.Errorf("%s not found", imp) 59 | }, 60 | } 61 | 62 | archive, err := importContext.Import(path) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return archive, nil 68 | } 69 | 70 | func compileFiles(fset *token.FileSet, path string, tags []string, sourceFiles map[string]string, importContext *compiler.ImportContext, minify bool) (*compiler.Archive, error) { 71 | var files []*ast.File 72 | inc := includer.New(sourceFiles, tags) 73 | for name, contents := range sourceFiles { 74 | include, err := inc.Include(name) 75 | if err != nil { 76 | return nil, err 77 | } 78 | if !include { 79 | continue 80 | } 81 | f, err := parser.ParseFile(fset, name, contents, parser.ParseComments) 82 | if err != nil { 83 | return nil, err 84 | } 85 | files = append(files, f) 86 | } 87 | 88 | if len(files) == 0 { 89 | return nil, fmt.Errorf("no buildable Go source files in %s", path) 90 | } 91 | 92 | // TODO: Remove this when https://github.com/gopherjs/gopherjs/pull/742 is merged 93 | // Files must be in the same order to get reproducible JS 94 | sort.Slice(files, func(i, j int) bool { 95 | return fset.File(files[i].Pos()).Name() > fset.File(files[j].Pos()).Name() 96 | }) 97 | 98 | archive, err := compiler.Compile(path, files, fset, importContext, minify) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | for name, contents := range sourceFiles { 104 | if !strings.HasSuffix(name, ".inc.js") { 105 | continue 106 | } 107 | archive.IncJSCode = append(archive.IncJSCode, []byte("\t(function() {\n")...) 108 | archive.IncJSCode = append(archive.IncJSCode, []byte(contents)...) 109 | archive.IncJSCode = append(archive.IncJSCode, []byte("\n\t}).call($global);\n")...) 110 | } 111 | 112 | return archive, nil 113 | } 114 | 115 | func GetPackageCode(ctx context.Context, archive *compiler.Archive, minify, initializer bool) (contents []byte, hash []byte, err error) { 116 | dceSelection := make(map[*compiler.Decl]struct{}) 117 | for _, d := range archive.Declarations { 118 | dceSelection[d] = struct{}{} 119 | } 120 | buf := new(bytes.Buffer) 121 | 122 | if initializer { 123 | var s string 124 | if minify { 125 | s = `$load["%s"]=function(){` 126 | } else { 127 | s = `$load["%s"] = function () {` + "\n" 128 | } 129 | if _, err := fmt.Fprintf(buf, s, archive.ImportPath); err != nil { 130 | return nil, nil, err 131 | } 132 | } 133 | 134 | if err := compiler.WritePkgCode(archive, dceSelection, minify, &compiler.SourceMapFilter{Writer: buf}); err != nil { 135 | return nil, nil, err 136 | } 137 | 138 | if minify { 139 | // compiler.WritePkgCode always finishes with a "\n". In minified mode we should remove this. 140 | buf.Truncate(buf.Len() - 1) 141 | } 142 | 143 | if initializer { 144 | /* 145 | var s string 146 | if minify { 147 | s = "};$done();" 148 | } else { 149 | s = "};\n$done();" 150 | } 151 | if _, err := fmt.Fprint(buf, s); err != nil { 152 | return nil, nil, err 153 | } 154 | */ 155 | if _, err := fmt.Fprint(buf, "};"); err != nil { 156 | return nil, nil, err 157 | } 158 | } 159 | 160 | sha := sha1.New() 161 | if _, err := sha.Write(buf.Bytes()); err != nil { 162 | return nil, nil, err 163 | } 164 | return buf.Bytes(), sha.Sum(nil), nil 165 | } 166 | -------------------------------------------------------------------------------- /stores/compile.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "errors" 5 | 6 | "bytes" 7 | "text/template" 8 | 9 | "strconv" 10 | 11 | "fmt" 12 | 13 | "github.com/dave/flux" 14 | "github.com/dave/play/actions" 15 | "github.com/dave/play/models" 16 | "github.com/gopherjs/gopherjs/js" 17 | "honnef.co/go/js/dom" 18 | ) 19 | 20 | func NewCompileStore(app *App) *CompileStore { 21 | s := &CompileStore{ 22 | app: app, 23 | } 24 | return s 25 | } 26 | 27 | type CompileStore struct { 28 | app *App 29 | compiling bool 30 | compiled bool 31 | consoleWritten bool 32 | tags []string 33 | } 34 | 35 | func (s *CompileStore) Tags() []string { 36 | return s.tags 37 | } 38 | 39 | func (s *CompileStore) Compiling() bool { 40 | return s.compiling 41 | } 42 | 43 | func (s *CompileStore) Compiled() bool { 44 | return s.compiled 45 | } 46 | 47 | func (s *CompileStore) Handle(payload *flux.Payload) bool { 48 | switch a := payload.Action.(type) { 49 | case *actions.LoadSource: 50 | s.tags = append(s.tags, a.Tags...) 51 | payload.Notify() 52 | case *actions.CompileStart: 53 | if err := s.compile(); err != nil { 54 | s.app.Fail(err) 55 | return true 56 | } 57 | payload.Notify() 58 | case *actions.BuildTags: 59 | s.tags = a.Tags 60 | payload.Notify() 61 | } 62 | return true 63 | } 64 | 65 | func (s *CompileStore) compile() error { 66 | path, count := s.app.Scanner.Main() 67 | if path == "" { 68 | if count == 0 { 69 | return errors.New("project has no main package") 70 | } else { 71 | return fmt.Errorf("project has %d main packages - select one and retry", count) 72 | } 73 | } 74 | 75 | if !s.app.Archive.Fresh(path) { 76 | s.app.Dispatch( 77 | &actions.RequestStart{Type: models.UpdateRequest, Run: true}, 78 | ) 79 | return nil 80 | } 81 | 82 | s.compiling = true 83 | defer func() { 84 | s.compiling = false 85 | }() 86 | 87 | s.app.Log("compiling") 88 | 89 | deps, err := s.app.Archive.Compile(path, s.Tags()) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | s.app.Log("running") 95 | 96 | doc := dom.GetWindow().Document() 97 | holder := doc.GetElementByID("iframe-holder") 98 | for _, v := range holder.ChildNodes() { 99 | v.Underlying().Call("remove") 100 | } 101 | frame := doc.CreateElement("iframe").(*dom.HTMLIFrameElement) 102 | frame.SetID("iframe") 103 | frame.Style().Set("width", "100%") 104 | frame.Style().Set("height", "100%") 105 | frame.Style().Set("border", "0") 106 | 107 | // We need to wait for the iframe to load before adding contents or Firefox will clear the iframe 108 | // after momentarily flashing up the contents. 109 | c := make(chan struct{}) 110 | listener := frame.AddEventListener("load", false, func(event dom.Event) { 111 | close(c) 112 | }) 113 | 114 | holder.AppendChild(frame) 115 | <-c 116 | 117 | // remove the listener so if it's triggered again we don't close the closed channel 118 | frame.RemoveEventListener("load", false, listener) 119 | 120 | console := dom.GetWindow().Document().GetElementByID("console") 121 | console.SetInnerHTML("") 122 | frame.Get("contentWindow").Set("goPrintToConsole", js.InternalObject(func(b []byte) { 123 | console.SetInnerHTML(console.InnerHTML() + string(b)) 124 | if !s.consoleWritten { 125 | s.consoleWritten = true 126 | s.app.Dispatch(&actions.ConsoleFirstWrite{}) 127 | } 128 | })) 129 | 130 | frameDoc := frame.ContentDocument() 131 | 132 | if index, ok := s.app.Source.Files(path)["index.jsgo.html"]; ok { 133 | // has index 134 | 135 | indexTemplate, err := template.New("index").Parse(index) 136 | if err != nil { 137 | return err 138 | } 139 | data := struct{ Script string }{Script: ""} 140 | buf := &bytes.Buffer{} 141 | if err := indexTemplate.Execute(buf, data); err != nil { 142 | return err 143 | } 144 | 145 | frameDoc.Underlying().Call("open") 146 | frameDoc.Underlying().Call("write", buf.String()) 147 | frameDoc.Underlying().Call("close") 148 | } 149 | 150 | head := frameDoc.GetElementsByTagName("head")[0].(*dom.BasicHTMLElement) 151 | 152 | loaderJs := "" 153 | for _, dep := range deps { 154 | loaderJs += "$load[" + strconv.Quote(dep.Path) + "]();\n" 155 | } 156 | scriptLoad := frameDoc.CreateElement("script") 157 | scriptLoad.SetID("loader") 158 | scriptLoad.SetInnerHTML(` 159 | var $load = {}; 160 | var $count = 0; 161 | var $total = ` + fmt.Sprint(len(deps)) + `; 162 | var $finished = function() { 163 | ` + loaderJs + ` 164 | $mainPkg = $packages[` + strconv.Quote(path) + `]; 165 | $synthesizeMethods(); 166 | $packages["runtime"].$init(); 167 | $go($mainPkg.$init, []); 168 | $flushConsole(); 169 | }; 170 | var $done = function() { 171 | $count++; 172 | if ($count == $total) { 173 | $finished(); 174 | } 175 | }; 176 | `) 177 | head.AppendChild(scriptLoad) 178 | 179 | for _, dep := range deps { 180 | scriptDep := frameDoc.CreateElement("script") 181 | scriptDep.SetID(dep.Path) 182 | scriptDep.SetInnerHTML(string(dep.Js) + "$done();") 183 | //scriptDep.AppendChild(doc.CreateTextNode(string(dep.Js) + "$done();")) 184 | head.AppendChild(scriptDep) 185 | } 186 | 187 | s.compiled = true 188 | s.app.Log() 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /stores/connection.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "errors" 5 | 6 | "honnef.co/go/js/dom" 7 | 8 | "fmt" 9 | 10 | "strings" 11 | 12 | "github.com/dave/flux" 13 | "github.com/dave/jsgo/server/play/messages" 14 | "github.com/dave/jsgo/server/servermsg" 15 | "github.com/dave/play/actions" 16 | "github.com/gopherjs/gopherjs/js" 17 | "github.com/gopherjs/websocket/websocketjs" 18 | ) 19 | 20 | type ConnectionStore struct { 21 | app *App 22 | 23 | open bool 24 | ws *websocketjs.WebSocket 25 | } 26 | 27 | func NewConnectionStore(app *App) *ConnectionStore { 28 | s := &ConnectionStore{ 29 | app: app, 30 | } 31 | return s 32 | } 33 | 34 | func (s *ConnectionStore) Open() bool { 35 | return s.open 36 | } 37 | 38 | func (s *ConnectionStore) Handle(payload *flux.Payload) bool { 39 | switch action := payload.Action.(type) { 40 | case *actions.Send: 41 | s.app.Debug(fmt.Sprintf("Sending %T", action.Message), action.Message) 42 | if !s.open { 43 | s.app.Fail(errors.New("connection closed")) 44 | return true 45 | } 46 | b, _, err := messages.Marshal(action.Message) 47 | if err != nil { 48 | s.app.Fail(err) 49 | return true 50 | } 51 | if err := s.ws.Send(string(b)); err != nil { 52 | s.app.Fail(err) 53 | return true 54 | } 55 | case *actions.Dial: 56 | if s.open { 57 | s.app.Fail(errors.New("connection already open")) 58 | return true 59 | } 60 | s.app.Debug("Web socket dialing", action.Url) 61 | var err error 62 | if s.ws, err = websocketjs.New(action.Url); err != nil { 63 | s.app.Fail(err) 64 | return true 65 | } 66 | s.open = true 67 | s.ws.AddEventListener("open", false, func(ev *js.Object) { 68 | go func() { 69 | s.app.Debug("Web socket open") 70 | s.app.Dispatch(action.Open()) 71 | }() 72 | }) 73 | s.ws.AddEventListener("message", false, func(ev *js.Object) { 74 | go func() { 75 | m, err := messages.Unmarshal([]byte(ev.Get("data").String())) 76 | if err != nil { 77 | s.app.Fail(err) 78 | return 79 | } 80 | s.app.Debug(fmt.Sprintf("Received %T", m), m) 81 | if e, ok := m.(servermsg.Error); ok { 82 | s.app.Fail(errors.New(e.Message)) 83 | return 84 | } 85 | s.app.Dispatch(action.Message(m)) 86 | }() 87 | }) 88 | s.ws.AddEventListener("close", false, func(ev *js.Object) { 89 | go func() { 90 | s.app.Debug("Web socket closed") 91 | s.app.Dispatch(action.Close()) 92 | s.ws.Close() 93 | s.open = false 94 | }() 95 | }) 96 | s.ws.AddEventListener("error", false, func(ev *js.Object) { 97 | go func() { 98 | s.app.Debug("Web socket error") 99 | s.app.Fail(errors.New("error from server")) 100 | s.ws.Close() 101 | s.open = false 102 | }() 103 | }) 104 | } 105 | return true 106 | } 107 | 108 | func defaultUrl() string { 109 | var url string 110 | if strings.HasPrefix(dom.GetWindow().Document().DocumentURI(), "https://") { 111 | url = "wss://compile.jsgo.io/_play/" 112 | } else { 113 | url = "ws://localhost:8081/_play/" 114 | } 115 | return url 116 | } 117 | -------------------------------------------------------------------------------- /stores/deploy.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/dave/flux" 8 | "github.com/dave/jsgo/config" 9 | "github.com/dave/jsgo/server/play/messages" 10 | "github.com/dave/play/actions" 11 | "github.com/dave/play/models" 12 | "github.com/dave/services/builder/buildermsg" 13 | "github.com/dave/services/constor/constormsg" 14 | "github.com/dave/services/getter/gettermsg" 15 | ) 16 | 17 | func NewDeployStore(app *App) *DeployStore { 18 | s := &DeployStore{ 19 | app: app, 20 | } 21 | return s 22 | } 23 | 24 | type DeployStore struct { 25 | app *App 26 | mainHash, indexHash, mainPath string 27 | } 28 | 29 | func (s *DeployStore) LoaderJs() string { 30 | return fmt.Sprintf("%s://%s/%s.%s.js", config.Protocol[config.Pkg], config.Host[config.Pkg], s.mainPath, s.mainHash) 31 | } 32 | 33 | func (s *DeployStore) Index() string { 34 | return fmt.Sprintf("%s://%s/%s", config.Protocol[config.Index], config.Host[config.Index], s.indexHash) 35 | } 36 | 37 | func (s *DeployStore) Handle(payload *flux.Payload) bool { 38 | switch action := payload.Action.(type) { 39 | case *actions.DeployStart: 40 | path, count := s.app.Scanner.Main() 41 | if path == "" { 42 | if count == 0 { 43 | s.app.Fail(errors.New("project has no main package")) 44 | return true 45 | } else { 46 | s.app.Fail(fmt.Errorf("project has %d main packages - select one and retry", count)) 47 | return true 48 | } 49 | } 50 | s.app.Log("deploying") 51 | s.mainHash = "" 52 | s.indexHash = "" 53 | s.mainPath = path 54 | s.app.Dispatch(&actions.Dial{ 55 | Url: defaultUrl(), 56 | Open: func() flux.ActionInterface { return &actions.DeployOpen{} }, 57 | Message: func(m interface{}) flux.ActionInterface { return &actions.DeployMessage{Message: m} }, 58 | Close: func() flux.ActionInterface { return &actions.DeployClose{} }, 59 | }) 60 | payload.Notify() 61 | case *actions.DeployOpen: 62 | message := messages.Deploy{ 63 | Main: s.mainPath, 64 | Imports: s.app.Scanner.Imports(s.mainPath), 65 | Source: s.app.Source.Source(), 66 | Tags: s.app.Compile.Tags(), 67 | } 68 | s.app.Dispatch(&actions.Send{ 69 | Message: message, 70 | }) 71 | case *actions.DeployMessage: 72 | switch message := action.Message.(type) { 73 | case gettermsg.Downloading: 74 | if message.Message != "" { 75 | s.app.Log(message.Message) 76 | } 77 | case buildermsg.Building: 78 | if message.Message != "" { 79 | s.app.Log(message.Message) 80 | } 81 | case constormsg.Storing: 82 | s.app.Log("storing") 83 | case messages.DeployComplete: 84 | s.mainHash = message.Main 85 | s.indexHash = message.Index 86 | s.app.Dispatch(&actions.ModalOpen{Modal: models.DeployDoneModal}) 87 | s.app.LogHide("deployed") 88 | payload.Notify() 89 | } 90 | case *actions.DeployClose: 91 | // nothing 92 | } 93 | return true 94 | } 95 | -------------------------------------------------------------------------------- /stores/editor.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "github.com/dave/flux" 5 | "github.com/dave/play/actions" 6 | ) 7 | 8 | func NewEditorStore(app *App) *EditorStore { 9 | s := &EditorStore{ 10 | app: app, 11 | currentFiles: map[string]string{}, 12 | } 13 | return s 14 | } 15 | 16 | type EditorStore struct { 17 | app *App 18 | 19 | sizes []float64 20 | currentPackage string 21 | currentFiles map[string]string // tracks the currently selected file in each package 22 | loaded bool 23 | } 24 | 25 | func (s *EditorStore) Loaded() bool { 26 | return s.loaded 27 | } 28 | 29 | func (s *EditorStore) Sizes() []float64 { 30 | return s.sizes 31 | } 32 | 33 | func (s *EditorStore) CurrentPackage() string { 34 | return s.currentPackage 35 | } 36 | 37 | func (s *EditorStore) CurrentFile() string { 38 | return s.currentFiles[s.currentPackage] 39 | } 40 | 41 | func (s *EditorStore) defaultPackage() string { 42 | if len(s.app.Source.Packages()) == 0 { 43 | return "" 44 | } 45 | var path string 46 | 47 | // default to the first main package 48 | for p, n := range s.app.Scanner.Names() { 49 | if n == "main" { 50 | path = p 51 | break 52 | } 53 | } 54 | 55 | if path == "" { 56 | // if no main package, choose first package ordered by name 57 | path = s.app.Source.Packages()[0] 58 | } 59 | 60 | return path 61 | } 62 | 63 | func (s *EditorStore) defaultFile(path string) string { 64 | if len(s.app.Source.Files(path)) == 0 { 65 | return "" 66 | } 67 | if _, ok := s.app.Source.Files(path)["README.md"]; ok { 68 | return "README.md" 69 | } 70 | if _, ok := s.app.Source.Files(path)["readme.md"]; ok { 71 | return "readme.md" 72 | } 73 | if _, ok := s.app.Source.Files(path)["main.go"]; ok { 74 | return "main.go" 75 | } 76 | return s.app.Source.Filenames(path)[0] 77 | } 78 | 79 | func (s *EditorStore) Handle(payload *flux.Payload) bool { 80 | switch a := payload.Action.(type) { 81 | case *actions.DragDrop: 82 | payload.Wait(s.app.Source) 83 | s.currentPackage = s.defaultPackage() 84 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage) 85 | payload.Notify() 86 | case *actions.LoadSource: 87 | payload.Wait(s.app.Scanner) 88 | 89 | defer func() { 90 | s.loaded = true 91 | }() 92 | 93 | var switchPackage string 94 | for path := range a.Source { 95 | switchPackage = path 96 | if s.app.Scanner.Name(path) == "main" { 97 | // break if we find a main package 98 | break 99 | } 100 | } 101 | 102 | // Switch to the right package. 103 | if a.CurrentPackage != "" && s.app.Source.HasPackage(a.CurrentPackage) { 104 | s.currentPackage = a.CurrentPackage 105 | } else { 106 | s.currentPackage = switchPackage 107 | } 108 | 109 | // Switch to the right file. 110 | if a.CurrentFile != "" && s.app.Source.HasFile(s.currentPackage, a.CurrentFile) { 111 | s.currentFiles[s.currentPackage] = a.CurrentFile 112 | } else { 113 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage) 114 | } 115 | 116 | payload.Notify() 117 | 118 | case *actions.ChangeSplit: 119 | s.sizes = a.Sizes 120 | payload.Notify() 121 | case *actions.UserChangedSplit: 122 | s.sizes = a.Sizes 123 | case *actions.UserChangedFile: 124 | s.currentFiles[s.currentPackage] = a.Name 125 | payload.Notify() 126 | case *actions.ChangeFile: 127 | s.currentPackage = a.Path 128 | s.currentFiles[a.Path] = a.Name 129 | payload.Notify() 130 | case *actions.UserChangedPackage: 131 | s.currentPackage = a.Path 132 | if s.currentFiles[a.Path] == "" { 133 | s.currentFiles[a.Path] = s.defaultFile(a.Path) 134 | } 135 | payload.Notify() 136 | case *actions.AddFile: 137 | payload.Wait(s.app.Source) 138 | s.currentFiles[s.currentPackage] = a.Name 139 | payload.Notify() 140 | case *actions.DeleteFile: 141 | payload.Wait(s.app.Source) 142 | if s.CurrentFile() == a.Name { 143 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage) 144 | payload.Notify() 145 | } 146 | case *actions.AddPackage: 147 | payload.Wait(s.app.Source) 148 | s.currentPackage = a.Path 149 | payload.Notify() 150 | case *actions.RemovePackage: 151 | payload.Wait(s.app.Scanner) 152 | if s.currentPackage == a.Path { 153 | s.currentPackage = s.defaultPackage() 154 | if s.currentFiles[s.currentPackage] == "" { 155 | s.currentFiles[s.currentPackage] = s.defaultFile(s.currentPackage) 156 | } 157 | payload.Notify() 158 | } 159 | } 160 | return true 161 | } 162 | -------------------------------------------------------------------------------- /stores/empty.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "github.com/dave/flux" 5 | ) 6 | 7 | func NewEmptyStore(app *App) *EmptyStore { 8 | s := &EmptyStore{ 9 | app: app, 10 | } 11 | return s 12 | } 13 | 14 | type EmptyStore struct { 15 | app *App 16 | } 17 | 18 | func (s *EmptyStore) Handle(payload *flux.Payload) bool { 19 | switch action := payload.Action.(type) { 20 | default: 21 | _ = action 22 | } 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /stores/history.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "github.com/dave/flux" 5 | "github.com/dave/play/actions" 6 | "github.com/gopherjs/gopherjs/js" 7 | ) 8 | 9 | func NewHistoryStore(app *App) *HistoryStore { 10 | s := &HistoryStore{ 11 | app: app, 12 | } 13 | return s 14 | } 15 | 16 | type HistoryStore struct { 17 | app *App 18 | } 19 | 20 | func (s *HistoryStore) Handle(payload *flux.Payload) bool { 21 | switch a := payload.Action.(type) { 22 | case *actions.UserChangedText, 23 | *actions.AddFile, 24 | *actions.DeleteFile, 25 | *actions.AddPackage, 26 | *actions.RemovePackage, 27 | *actions.DragDrop, 28 | *actions.BuildTags: 29 | js.Global.Get("history").Call("replaceState", js.M{}, "", "/") 30 | case *actions.LoadSource: 31 | if a.Save { 32 | js.Global.Get("history").Call("replaceState", js.M{}, "", "/") 33 | } 34 | } 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /stores/local.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "encoding/json" 9 | 10 | "regexp" 11 | 12 | "github.com/dave/flux" 13 | "github.com/dave/jsgo/config" 14 | "github.com/dave/locstor" 15 | "github.com/dave/play/actions" 16 | "github.com/dave/play/models" 17 | "honnef.co/go/js/dom" 18 | ) 19 | 20 | type LocalStore struct { 21 | app *App 22 | 23 | local *locstor.DataStore 24 | initialized bool 25 | } 26 | 27 | func NewLocalStore(app *App) *LocalStore { 28 | s := &LocalStore{ 29 | app: app, 30 | local: locstor.NewDataStore(locstor.JSONEncoding), 31 | } 32 | return s 33 | } 34 | 35 | func (s *LocalStore) Initialized() bool { 36 | return s.initialized 37 | } 38 | 39 | func (s *LocalStore) Handle(payload *flux.Payload) bool { 40 | switch action := payload.Action.(type) { 41 | case *actions.Load: 42 | var sizes []float64 43 | found, err := s.local.Find("split-sizes", &sizes) 44 | if err != nil { 45 | s.app.Fail(err) 46 | return true 47 | } 48 | if !found { 49 | sizes = defaultSizes 50 | } 51 | s.app.Dispatch(&actions.ChangeSplit{Sizes: sizes}) 52 | 53 | location := strings.Trim(dom.GetWindow().Location().Pathname, "/") 54 | 55 | var seenHelp bool 56 | if _, err := s.local.Find("seen-help", &seenHelp); err != nil { 57 | s.app.Fail(err) 58 | return true 59 | } 60 | if !seenHelp { 61 | s.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal}) 62 | if err := s.local.Save("seen-help", true); err != nil { 63 | s.app.Fail(err) 64 | return true 65 | } 66 | } 67 | 68 | // No page path -> load files from local storage or use default files 69 | if location == "" { 70 | var currentPackage, currentFile string 71 | var buildTags []string 72 | var source map[string]map[string]string 73 | found, err = s.local.Find("source", &source) 74 | if err != nil { 75 | s.app.Fail(err) 76 | return true 77 | } 78 | if !found { 79 | // old format for storing files 80 | var files map[string]string 81 | found, err = s.local.Find("files", &files) 82 | if err != nil { 83 | s.app.Fail(err) 84 | return true 85 | } 86 | if found { 87 | source = map[string]map[string]string{"main": files} 88 | } 89 | } 90 | if found { 91 | // if we found files in local storage, also load the current file and package 92 | if _, err := s.local.Find("current-file", ¤tFile); err != nil { 93 | s.app.Fail(err) 94 | return true 95 | } 96 | if _, err := s.local.Find("current-package", ¤tPackage); err != nil { 97 | s.app.Fail(err) 98 | return true 99 | } 100 | if _, err := s.local.Find("build-tags", &buildTags); err != nil { 101 | s.app.Fail(err) 102 | return true 103 | } 104 | } else { 105 | // if we didn't find source in the local storage, add the default file 106 | source = map[string]map[string]string{"main": {"main.go": defaultFile}} 107 | currentFile = "main.go" 108 | currentPackage = "main" 109 | } 110 | s.app.Dispatch(&actions.LoadSource{ 111 | Source: source, 112 | CurrentFile: currentFile, 113 | CurrentPackage: currentPackage, 114 | Tags: buildTags, 115 | Update: true, 116 | }) 117 | break 118 | } 119 | 120 | // Hash in page path -> load files from src.jsgo.io json blob 121 | if shaRegex.MatchString(location) { 122 | resp, err := http.Get(fmt.Sprintf("%s://%s/%s.json", config.Protocol[config.Src], config.Host[config.Src], location)) 123 | if err != nil { 124 | s.app.Fail(err) 125 | return true 126 | } 127 | if resp.StatusCode != 200 { 128 | s.app.Fail(fmt.Errorf("error %d loading source", resp.StatusCode)) 129 | return true 130 | } 131 | var sp models.SharePack 132 | if err := json.NewDecoder(resp.Body).Decode(&sp); err != nil { 133 | s.app.Fail(err) 134 | return true 135 | } 136 | s.app.Dispatch(&actions.LoadSource{ 137 | Source: sp.Source, 138 | Tags: sp.Tags, 139 | Update: true, 140 | }) 141 | break 142 | } 143 | 144 | // Package path in page path -> open websocket and load files 145 | s.app.Dispatch(&actions.RequestStart{Type: models.InitialiseRequest, Path: location}) 146 | 147 | case *actions.UserChangedSplit: 148 | if err := s.saveSplitSizes(action.Sizes); err != nil { 149 | s.app.Fail(err) 150 | return true 151 | } 152 | case *actions.UserChangedText, *actions.FormatCode: 153 | payload.Wait(s.app.Editor) 154 | if err := s.saveSource(); err != nil { 155 | s.app.Fail(err) 156 | return true 157 | } 158 | case *actions.BuildTags: 159 | payload.Wait(s.app.Compile) 160 | if err := s.saveSource(); err != nil { 161 | s.app.Fail(err) 162 | return true 163 | } 164 | case *actions.UserChangedFile: 165 | payload.Wait(s.app.Editor) 166 | if err := s.saveSource(); err != nil { 167 | s.app.Fail(err) 168 | return true 169 | } 170 | case *actions.UserChangedPackage: 171 | payload.Wait(s.app.Editor) 172 | if err := s.saveSource(); err != nil { 173 | s.app.Fail(err) 174 | return true 175 | } 176 | case *actions.AddFile, *actions.DeleteFile: 177 | payload.Wait(s.app.Source) 178 | if err := s.saveSource(); err != nil { 179 | s.app.Fail(err) 180 | return true 181 | } 182 | case *actions.AddPackage, *actions.RemovePackage, *actions.DragDrop: 183 | payload.Wait(s.app.Editor) 184 | if err := s.saveSource(); err != nil { 185 | s.app.Fail(err) 186 | return true 187 | } 188 | case *actions.LoadSource: 189 | if action.Save { 190 | payload.Wait(s.app.Editor) 191 | if err := s.saveSource(); err != nil { 192 | s.app.Fail(err) 193 | return true 194 | } 195 | } 196 | } 197 | return true 198 | } 199 | 200 | func (s *LocalStore) saveSource() error { 201 | s.local.Delete("files") // delete old format file storage location 202 | if err := s.local.Save("source", s.app.Source.Source()); err != nil { 203 | return err 204 | } 205 | if err := s.local.Save("current-package", s.app.Editor.CurrentPackage()); err != nil { 206 | return err 207 | } 208 | if err := s.local.Save("current-file", s.app.Editor.CurrentFile()); err != nil { 209 | return err 210 | } 211 | if err := s.local.Save("build-tags", s.app.Compile.Tags()); err != nil { 212 | return err 213 | } 214 | return nil 215 | } 216 | 217 | func (s *LocalStore) saveSplitSizes(sizes []float64) error { 218 | return s.local.Save("split-sizes", sizes) 219 | } 220 | 221 | var ( 222 | defaultSizes = []float64{50, 50} 223 | defaultFile = `package main 224 | 225 | import ( 226 | "fmt" 227 | "honnef.co/go/js/dom" 228 | ) 229 | 230 | func main() { 231 | body := dom.GetWindow().Document().GetElementsByTagName("body")[0] 232 | body.SetInnerHTML("Hello, HTML!") 233 | fmt.Println("Hello, console!") 234 | }` 235 | ) 236 | 237 | var shaRegex = regexp.MustCompile("^[0-9a-f]{40}$") 238 | -------------------------------------------------------------------------------- /stores/page.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "github.com/dave/flux" 5 | "github.com/dave/play/actions" 6 | "github.com/dave/play/models" 7 | ) 8 | 9 | func NewPageStore(app *App) *PageStore { 10 | s := &PageStore{ 11 | app: app, 12 | autoOpen: true, 13 | modals: map[models.Modal]bool{}, 14 | minify: true, 15 | } 16 | return s 17 | } 18 | 19 | type PageStore struct { 20 | app *App 21 | console bool 22 | minify bool 23 | autoOpen bool 24 | modals map[models.Modal]bool 25 | showAllDeps bool // show all dependencies in the load package modal 26 | } 27 | 28 | func (s *PageStore) ShowAllDeps() bool { 29 | return s.showAllDeps 30 | } 31 | 32 | func (s *PageStore) ModalOpen(modal models.Modal) bool { 33 | return s.modals[modal] 34 | } 35 | 36 | func (s *PageStore) Console() bool { 37 | return s.console 38 | } 39 | 40 | func (s *PageStore) Minify() bool { 41 | return s.minify 42 | } 43 | 44 | func (s *PageStore) Handle(payload *flux.Payload) bool { 45 | switch a := payload.Action.(type) { 46 | case *actions.ModalOpen: 47 | s.modals[a.Modal] = true 48 | payload.Notify() 49 | case *actions.ModalClose: 50 | s.modals[a.Modal] = false 51 | payload.Notify() 52 | case *actions.ConsoleToggleClick: 53 | s.console = !s.console 54 | payload.Notify() 55 | case *actions.MinifyToggleClick: 56 | s.minify = !s.minify 57 | payload.Notify() 58 | case *actions.ConsoleFirstWrite: 59 | if s.autoOpen { 60 | s.console = true 61 | s.autoOpen = false 62 | payload.Notify() 63 | } 64 | case *actions.ShowAllDepsChange: 65 | s.showAllDeps = a.State 66 | payload.Notify() 67 | } 68 | return true 69 | } 70 | -------------------------------------------------------------------------------- /stores/request.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "github.com/dave/flux" 5 | "github.com/dave/jsgo/server/play/messages" 6 | "github.com/dave/jsgo/server/servermsg" 7 | "github.com/dave/play/actions" 8 | "github.com/dave/play/models" 9 | "github.com/dave/services" 10 | "github.com/dave/services/getter/gettermsg" 11 | ) 12 | 13 | func NewRequestStore(app *App) *RequestStore { 14 | s := &RequestStore{ 15 | app: app, 16 | } 17 | return s 18 | } 19 | 20 | type RequestStore struct { 21 | app *App 22 | } 23 | 24 | func (s *RequestStore) Handle(payload *flux.Payload) bool { 25 | switch action := payload.Action.(type) { 26 | case *actions.RequestStart: 27 | s.app.Log("downloading") 28 | s.app.Dispatch(&actions.Dial{ 29 | Url: defaultUrl(), 30 | Open: func() flux.ActionInterface { return &actions.RequestOpen{RequestStart: action} }, 31 | Message: func(m interface{}) flux.ActionInterface { 32 | return &actions.RequestMessage{RequestStart: action, Message: m} 33 | }, 34 | Close: func() flux.ActionInterface { return &actions.RequestClose{RequestStart: action} }, 35 | }) 36 | payload.Notify() 37 | case *actions.RequestOpen: 38 | var message services.Message 39 | switch action.Type { 40 | case models.GetRequest: 41 | message = messages.Get{ 42 | Path: action.Path, 43 | } 44 | case models.UpdateRequest: 45 | message = messages.Update{ 46 | Source: s.app.Source.Source(), 47 | Cache: s.app.Archive.CacheStrings(), 48 | Minify: s.app.Page.Minify(), 49 | Tags: s.app.Compile.Tags(), 50 | } 51 | case models.InitialiseRequest: 52 | message = messages.Initialise{ 53 | Path: action.Path, 54 | Minify: s.app.Page.Minify(), 55 | } 56 | } 57 | s.app.Dispatch(&actions.Send{ 58 | Message: message, 59 | }) 60 | case *actions.RequestMessage: 61 | switch message := action.Message.(type) { 62 | case servermsg.Queueing: 63 | if message.Position > 1 { 64 | s.app.Logf("queued position %d", message.Position) 65 | } 66 | case gettermsg.Downloading: 67 | if len(message.Message) > 0 { 68 | s.app.Log(message.Message) 69 | } 70 | case messages.GetComplete: 71 | s.app.Dispatch(&actions.LoadSource{ 72 | Source: message.Source, 73 | Save: action.Type == models.GetRequest, 74 | Update: action.Type != models.InitialiseRequest, // never update after initialise (will be updated by initialise) 75 | }) 76 | var count int 77 | for _, files := range message.Source { 78 | count += len(files) 79 | } 80 | if count == 1 { 81 | s.app.LogHide("got 1 file") 82 | } else { 83 | s.app.LogHidef("got %d files", count) 84 | } 85 | } 86 | case *actions.RequestClose: 87 | // nothing 88 | } 89 | return true 90 | } 91 | -------------------------------------------------------------------------------- /stores/scanner.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "go/parser" 5 | "go/token" 6 | 7 | "strconv" 8 | 9 | "sort" 10 | 11 | "strings" 12 | 13 | "github.com/dave/flux" 14 | "github.com/dave/play/actions" 15 | "github.com/dave/services/includer" 16 | ) 17 | 18 | func NewScannerStore(app *App) *ScannerStore { 19 | s := &ScannerStore{ 20 | app: app, 21 | imports: map[string]map[string][]string{}, 22 | names: map[string]string{}, 23 | clashes: map[string]map[string]bool{}, 24 | } 25 | return s 26 | } 27 | 28 | type ScannerStore struct { 29 | app *App 30 | imports map[string]map[string][]string 31 | names map[string]string 32 | clashes map[string]map[string]bool 33 | } 34 | 35 | func (s *ScannerStore) Clashes() map[string]map[string]bool { 36 | return s.clashes 37 | } 38 | 39 | // Main is the path of the main package 40 | func (s *ScannerStore) Main() (string, int) { 41 | if s.names[s.app.Editor.CurrentPackage()] == "main" { 42 | return s.app.Editor.CurrentPackage(), 0 43 | } 44 | // count the main packages 45 | var count int 46 | var path string 47 | for p, n := range s.names { 48 | if n == "main" { 49 | count++ 50 | path = p 51 | } 52 | } 53 | if count == 1 { 54 | return path, 1 55 | } 56 | return "", count 57 | } 58 | 59 | func (s *ScannerStore) MainPackages() map[string]bool { 60 | m := map[string]bool{} 61 | for p, n := range s.names { 62 | if n == "main" { 63 | m[p] = true 64 | } 65 | } 66 | return m 67 | } 68 | 69 | func (s *ScannerStore) DisplayPath(path string) string { 70 | parts := strings.Split(path, "/") 71 | guessed := parts[len(parts)-1] 72 | name := s.names[path] 73 | suffix := "" 74 | if guessed != name && name != "" { 75 | suffix = " (" + name + ")" 76 | } 77 | return path + suffix 78 | } 79 | 80 | func (s *ScannerStore) DisplayName(path string) string { 81 | if s.names[path] != "" { 82 | return s.names[path] 83 | } 84 | parts := strings.Split(path, "/") 85 | return parts[len(parts)-1] 86 | } 87 | 88 | func (s *ScannerStore) Name(path string) string { 89 | return s.names[path] 90 | } 91 | 92 | func (s *ScannerStore) Names() map[string]string { 93 | return s.names 94 | } 95 | 96 | // Imports returns all the imports from all files in a package 97 | func (s *ScannerStore) Imports(path string) []string { 98 | var a []string 99 | for _, f := range s.imports[path] { 100 | for _, i := range f { 101 | a = append(a, i) 102 | } 103 | } 104 | return a 105 | } 106 | 107 | func (s *ScannerStore) AllImports() map[string]bool { 108 | m := map[string]bool{} 109 | for _, imps := range s.imports { 110 | for _, file := range imps { 111 | for _, imp := range file { 112 | m[imp] = true 113 | } 114 | } 115 | } 116 | return m 117 | } 118 | 119 | func (s *ScannerStore) AllImportsOrdered() []string { 120 | var a []string 121 | m := map[string]bool{} 122 | for _, imps := range s.imports { 123 | for _, file := range imps { 124 | for _, imp := range file { 125 | if !m[imp] { 126 | m[imp] = true 127 | a = append(a, imp) 128 | } 129 | } 130 | } 131 | } 132 | sort.Strings(a) 133 | return a 134 | } 135 | 136 | func (s *ScannerStore) Handle(payload *flux.Payload) bool { 137 | switch action := payload.Action.(type) { 138 | case *actions.BuildTags: 139 | payload.Wait(s.app.Compile) 140 | 141 | s.imports = map[string]map[string][]string{} 142 | s.names = map[string]string{} 143 | 144 | var changed bool 145 | for path, files := range s.app.Source.Source() { 146 | for name, contents := range files { 147 | if s.refresh(path, name, contents) { 148 | changed = true 149 | } 150 | } 151 | } 152 | 153 | if s.checkForClash() { 154 | changed = true 155 | } 156 | 157 | if changed { 158 | payload.Notify() 159 | } 160 | 161 | case *actions.DeleteFile: 162 | delete(s.imports[s.app.Editor.CurrentPackage()], action.Name) 163 | payload.Notify() 164 | case *actions.RemovePackage: 165 | payload.Wait(s.app.Source) 166 | delete(s.imports, action.Path) 167 | delete(s.names, action.Path) 168 | s.checkForClash() 169 | payload.Notify() 170 | case *actions.AddPackage: 171 | payload.Wait(s.app.Source) 172 | if s.checkForClash() { 173 | payload.Notify() 174 | } 175 | case *actions.LoadSource: 176 | payload.Wait(s.app.Source) 177 | payload.Wait(s.app.Compile) 178 | 179 | for path := range action.Source { 180 | delete(s.imports, path) 181 | delete(s.names, path) 182 | } 183 | 184 | var changed bool 185 | for path, files := range action.Source { 186 | for name, contents := range files { 187 | if s.refresh(path, name, contents) { 188 | changed = true 189 | } 190 | } 191 | } 192 | 193 | if s.checkForClash() { 194 | changed = true 195 | } 196 | 197 | if changed { 198 | payload.Notify() 199 | } 200 | case *actions.RequestClose: 201 | payload.Wait(s.app.Archive) 202 | if s.checkForClash() { 203 | payload.Notify() 204 | } 205 | case *actions.DragDrop: 206 | payload.Wait(s.app.Source) 207 | var changed bool 208 | for path, files := range action.Changed { 209 | for name := range files { 210 | if s.refresh(path, name, s.app.Source.Contents(path, name)) { 211 | changed = true 212 | } 213 | } 214 | } 215 | if changed { 216 | payload.Notify() 217 | } 218 | case *actions.UserChangedText: 219 | payload.Wait(s.app.Source) 220 | if action.Changed { 221 | if s.refresh(s.app.Editor.CurrentPackage(), s.app.Editor.CurrentFile(), s.app.Source.Current()) { 222 | payload.Notify() 223 | } 224 | } 225 | } 226 | return true 227 | } 228 | 229 | func (s *ScannerStore) checkForClash() bool { 230 | 231 | clashes := map[string]map[string]bool{} 232 | 233 | var check func(path string) 234 | check = func(path string) { 235 | imports := map[string]bool{} 236 | if s.app.Source.HasPackage(path) { 237 | for _, imp := range s.app.Scanner.Imports(path) { 238 | imports[imp] = true 239 | } 240 | } else if ci, ok := s.app.Archive.Cache()[path]; ok { 241 | for _, imp := range ci.Archive.Imports { 242 | imports[imp] = true 243 | } 244 | } 245 | for imp := range imports { 246 | check(imp) 247 | if !s.app.Source.HasPackage(path) && s.app.Source.HasPackage(imp) { 248 | if clashes[imp] == nil { 249 | clashes[imp] = map[string]bool{} 250 | } 251 | clashes[imp][path] = true 252 | } 253 | } 254 | } 255 | for path := range s.MainPackages() { 256 | check(path) 257 | } 258 | 259 | hadClashesBefore := len(s.clashes) > 0 260 | hasClashesAfter := len(clashes) > 0 261 | 262 | s.clashes = clashes 263 | 264 | return hadClashesBefore || hasClashesAfter 265 | } 266 | 267 | func (s *ScannerStore) refresh(path, filename, contents string) bool { 268 | 269 | include, err := includer.New(map[string]string{filename: contents}, s.app.Compile.Tags()).Include(filename) 270 | if err != nil { 271 | // ignore errors (we never want to throw an error while we're scanning the source) 272 | return false 273 | } 274 | if !include { 275 | return false 276 | } 277 | 278 | fset := token.NewFileSet() 279 | 280 | f, err := parser.ParseFile(fset, filename, contents, parser.ImportsOnly) 281 | if err != nil { 282 | // ignore errors (we never want to throw an error while we're scanning the source) 283 | return false 284 | } 285 | 286 | name := f.Name.Name 287 | 288 | var nameChanged bool 289 | if s.names[path] != name { 290 | nameChanged = true 291 | s.names[path] = name 292 | } 293 | 294 | var imports []string 295 | for _, v := range f.Imports { 296 | unquoted, err := strconv.Unquote(v.Path.Value) 297 | if err != nil { 298 | // ignore errors (we never want to throw an error while we're scanning the source) 299 | continue 300 | } 301 | imports = append(imports, unquoted) 302 | } 303 | sort.Strings(imports) 304 | 305 | var importsChanged bool 306 | if s.imports[path] == nil { 307 | s.imports[path] = map[string][]string{} 308 | } 309 | if s.changed(s.imports[path][filename], imports) { 310 | importsChanged = true 311 | s.imports[path][filename] = imports 312 | } 313 | 314 | return nameChanged || importsChanged 315 | } 316 | 317 | func (s *ScannerStore) changed(imports, compare []string) bool { 318 | if len(compare) != len(imports) { 319 | return true 320 | } 321 | for i := range compare { 322 | if imports[i] != compare[i] { 323 | return true 324 | } 325 | } 326 | return false 327 | } 328 | -------------------------------------------------------------------------------- /stores/share.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dave/flux" 7 | "github.com/dave/jsgo/server/play/messages" 8 | "github.com/dave/play/actions" 9 | "github.com/dave/services/constor/constormsg" 10 | "github.com/gopherjs/gopherjs/js" 11 | ) 12 | 13 | func NewShareStore(app *App) *ShareStore { 14 | s := &ShareStore{ 15 | app: app, 16 | } 17 | return s 18 | } 19 | 20 | type ShareStore struct { 21 | app *App 22 | } 23 | 24 | func (s *ShareStore) Handle(payload *flux.Payload) bool { 25 | switch action := payload.Action.(type) { 26 | case *actions.ShareStart: 27 | s.app.Log("sharing") 28 | s.app.Dispatch(&actions.Dial{ 29 | Url: defaultUrl(), 30 | Open: func() flux.ActionInterface { return &actions.ShareOpen{} }, 31 | Message: func(m interface{}) flux.ActionInterface { return &actions.ShareMessage{Message: m} }, 32 | Close: func() flux.ActionInterface { return &actions.ShareClose{} }, 33 | }) 34 | payload.Notify() 35 | case *actions.ShareOpen: 36 | message := messages.Share{ 37 | Source: s.app.Source.Source(), 38 | Tags: s.app.Compile.Tags(), 39 | } 40 | s.app.Dispatch(&actions.Send{ 41 | Message: message, 42 | }) 43 | case *actions.ShareMessage: 44 | switch message := action.Message.(type) { 45 | case constormsg.Storing: 46 | s.app.Log("storing") 47 | case messages.ShareComplete: 48 | js.Global.Get("history").Call("replaceState", js.M{}, "", fmt.Sprintf("/%s", message.Hash)) 49 | s.app.LogHide("shared") 50 | } 51 | case *actions.ShareClose: 52 | // nothing 53 | } 54 | return true 55 | } 56 | -------------------------------------------------------------------------------- /stores/source.go: -------------------------------------------------------------------------------- 1 | package stores 2 | 3 | import ( 4 | "archive/zip" 5 | "sort" 6 | 7 | "go/format" 8 | 9 | "strings" 10 | 11 | "bytes" 12 | "io" 13 | 14 | "path/filepath" 15 | 16 | "io/ioutil" 17 | 18 | "errors" 19 | 20 | "fmt" 21 | 22 | "github.com/dave/flux" 23 | "github.com/dave/jsgo/config" 24 | "github.com/dave/play/actions" 25 | "github.com/dave/saver" 26 | ) 27 | 28 | func NewSourceStore(app *App) *SourceStore { 29 | s := &SourceStore{ 30 | app: app, 31 | source: map[string]map[string]string{}, 32 | } 33 | return s 34 | } 35 | 36 | type SourceStore struct { 37 | app *App 38 | 39 | source map[string]map[string]string 40 | } 41 | 42 | func (s *SourceStore) Current() string { 43 | return s.source[s.app.Editor.CurrentPackage()][s.app.Editor.CurrentFile()] 44 | } 45 | 46 | func (s *SourceStore) Contents(path, name string) string { 47 | return s.source[path][name] 48 | } 49 | 50 | func (s *SourceStore) Files(path string) map[string]string { 51 | return s.source[path] 52 | } 53 | 54 | func (s *SourceStore) Source() map[string]map[string]string { 55 | return s.source 56 | } 57 | 58 | func (s *SourceStore) HasPackage(path string) bool { 59 | return s.source[path] != nil 60 | } 61 | 62 | func (s *SourceStore) HasFile(path, name string) bool { 63 | if !s.HasPackage(path) { 64 | return false 65 | } 66 | _, ok := s.source[path][name] 67 | return ok 68 | } 69 | 70 | func (s *SourceStore) Count() int { 71 | var count int 72 | for _, pkg := range s.source { 73 | count += len(pkg) 74 | } 75 | return count 76 | } 77 | 78 | // SinglePackage returns a random package. Use for when len(s.source) == 1 79 | func (s *SourceStore) SinglePackage() (path string, files map[string]string) { 80 | for path, files := range s.source { 81 | return path, files 82 | } 83 | return "", nil 84 | } 85 | 86 | // SingleFile returns a random file in a random package. Use for when Count() == 1 87 | func (s *SourceStore) SingleFile() (path, name, contents string) { 88 | for path, files := range s.source { 89 | for name, contents := range files { 90 | return path, name, contents 91 | } 92 | } 93 | return "", "", "" 94 | } 95 | 96 | func (s *SourceStore) Packages() []string { 97 | var paths []string 98 | for p := range s.source { 99 | paths = append(paths, p) 100 | } 101 | sort.Strings(paths) 102 | return paths 103 | } 104 | 105 | func (s *SourceStore) Filenames(path string) []string { 106 | var f []string 107 | for k := range s.source[path] { 108 | f = append(f, k) 109 | } 110 | sort.Strings(f) 111 | return f 112 | } 113 | 114 | func (s *SourceStore) Handle(payload *flux.Payload) bool { 115 | switch a := payload.Action.(type) { 116 | case *actions.DragEnter: 117 | s.app.Log("Drop to upload") 118 | case *actions.DragLeave: 119 | s.app.Log() 120 | case *actions.DragDrop: 121 | s.app.Log() 122 | packages := map[string]map[string][]byte{} 123 | if len(a.Files) == 1 && strings.HasSuffix(a.Files[0].Name(), ".zip") { 124 | b, err := ioutil.ReadAll(a.Files[0].Reader()) 125 | if err != nil { 126 | s.app.Fail(err) 127 | return true 128 | } 129 | zr, err := zip.NewReader(bytes.NewReader(b), int64(a.Files[0].Len())) 130 | if err != nil { 131 | s.app.Fail(err) 132 | return true 133 | } 134 | for _, file := range zr.File { 135 | path, name := filepath.Split(file.Name) 136 | if !isValidFile(name) { 137 | continue 138 | } 139 | path = strings.Trim(path, "/") 140 | if path == "" { 141 | path = s.app.Editor.CurrentPackage() 142 | } 143 | if path == "" { 144 | path = "main" 145 | } 146 | fr, err := file.Open() 147 | if err != nil { 148 | s.app.Fail(err) 149 | return true 150 | } 151 | b, err := ioutil.ReadAll(fr) 152 | if err != nil { 153 | fr.Close() 154 | s.app.Fail(err) 155 | return true 156 | } 157 | fr.Close() 158 | if packages[path] == nil { 159 | packages[path] = map[string][]byte{} 160 | } 161 | packages[path][name] = b 162 | } 163 | } else { 164 | for _, f := range a.Files { 165 | if !isValidFile(f.Name()) { 166 | continue 167 | } 168 | path := strings.Trim(f.Dir(), "/") 169 | if path == "" { 170 | path = s.app.Editor.CurrentPackage() 171 | } 172 | if path == "" { 173 | path = "main" 174 | } 175 | b, err := ioutil.ReadAll(f.Reader()) 176 | if err != nil { 177 | s.app.Fail(err) 178 | return true 179 | } 180 | if packages[path] == nil { 181 | packages[path] = map[string][]byte{} 182 | } 183 | packages[path][f.Name()] = b 184 | } 185 | } 186 | 187 | changed := map[string]map[string]bool{} 188 | for path, files := range packages { 189 | for name, contents := range files { 190 | if s.source[path] == nil { 191 | s.source[path] = map[string]string{} 192 | } 193 | if s.source[path][name] != string(contents) { 194 | s.source[path][name] = string(contents) 195 | 196 | // track changed files to pass to the scanner 197 | if changed[path] == nil { 198 | changed[path] = map[string]bool{} 199 | } 200 | changed[path][name] = true 201 | } 202 | } 203 | } 204 | 205 | a.Changed = changed 206 | 207 | payload.Notify() 208 | 209 | case *actions.DownloadClick: 210 | if s.Count() == 1 { 211 | _, name, contents := s.SingleFile() 212 | saver.Save(name, "text/plain", []byte(contents)) 213 | break 214 | } 215 | buf := &bytes.Buffer{} 216 | zw := zip.NewWriter(buf) 217 | if len(s.source) == 1 { 218 | _, files := s.SinglePackage() 219 | for name, contents := range files { 220 | w, err := zw.Create(name) 221 | if err != nil { 222 | s.app.Fail(err) 223 | return true 224 | } 225 | if _, err := io.Copy(w, strings.NewReader(contents)); err != nil { 226 | s.app.Fail(err) 227 | return true 228 | } 229 | } 230 | } else { 231 | for path, files := range s.source { 232 | for name, contents := range files { 233 | w, err := zw.Create(filepath.Join(path, name)) 234 | if err != nil { 235 | s.app.Fail(err) 236 | return true 237 | } 238 | if _, err := io.Copy(w, strings.NewReader(contents)); err != nil { 239 | s.app.Fail(err) 240 | return true 241 | } 242 | } 243 | } 244 | } 245 | zw.Close() 246 | saver.Save("src.zip", "application/zip", buf.Bytes()) 247 | case *actions.UserChangedText: 248 | p := s.app.Editor.CurrentPackage() 249 | f := s.app.Editor.CurrentFile() 250 | if p == "" { 251 | s.app.Fail(errors.New("no package selected")) 252 | return true 253 | } 254 | if f == "" { 255 | s.app.Fail(errors.New("no file selected")) 256 | return true 257 | } 258 | if s.source[p] == nil { 259 | s.source[p] = map[string]string{} 260 | } 261 | if s.source[p][f] != a.Text { 262 | s.source[p][f] = a.Text 263 | a.Changed = true 264 | } 265 | case *actions.AddFile: 266 | p := s.app.Editor.CurrentPackage() 267 | if p == "" { 268 | s.app.Fail(errors.New("no package selected")) 269 | return true 270 | } 271 | if s.source[p] == nil { 272 | s.source[p] = map[string]string{} 273 | } 274 | if s.app.Scanner.Name(p) != "" && strings.HasSuffix(a.Name, ".go") { 275 | s.source[p][a.Name] = "package " + s.app.Scanner.Name(p) + "\n\n" 276 | } else { 277 | s.source[p][a.Name] = "" 278 | } 279 | payload.Notify() 280 | case *actions.AddPackage: 281 | if s.source[a.Path] == nil { 282 | s.source[a.Path] = map[string]string{} 283 | } 284 | payload.Notify() 285 | case *actions.DeleteFile: 286 | p := s.app.Editor.CurrentPackage() 287 | if p == "" { 288 | s.app.Fail(errors.New("no package selected")) 289 | return true 290 | } 291 | if !s.HasPackage(p) { 292 | s.app.Fail(fmt.Errorf("package %s not found", p)) 293 | return true 294 | } 295 | if !s.HasFile(p, a.Name) { 296 | s.app.Fail(fmt.Errorf("%s not found", a.Name)) 297 | return true 298 | } 299 | delete(s.source[p], a.Name) 300 | payload.Notify() 301 | case *actions.RemovePackage: 302 | if !s.HasPackage(a.Path) { 303 | s.app.Fail(fmt.Errorf("%s not found", a.Path)) 304 | return true 305 | } 306 | delete(s.source, a.Path) 307 | payload.Notify() 308 | case *actions.LoadSource: 309 | for path, files := range a.Source { 310 | if s.source[path] == nil { 311 | s.source[path] = files 312 | } 313 | } 314 | payload.Notify() 315 | case *actions.FormatCode: 316 | p := s.app.Editor.CurrentPackage() 317 | f := s.app.Editor.CurrentFile() 318 | if strings.HasSuffix(f, ".go") { 319 | b, err := format.Source([]byte(s.Contents(p, f))) 320 | if err != nil { 321 | s.app.Fail(err) 322 | return true 323 | } 324 | s.source[p][f] = string(b) 325 | payload.Notify() 326 | } 327 | if a.Then != nil { 328 | s.app.Dispatch(a.Then) 329 | } 330 | } 331 | return true 332 | } 333 | 334 | func isValidFile(name string) bool { 335 | for _, ext := range config.ValidExtensions { 336 | if strings.HasSuffix(name, ext) { 337 | return true 338 | } 339 | } 340 | return false 341 | } 342 | -------------------------------------------------------------------------------- /testing/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type A struct { 4 | A int 5 | } 6 | -------------------------------------------------------------------------------- /testing/b/b.go: -------------------------------------------------------------------------------- 1 | package b 2 | 3 | import "github.com/dave/play/testing/a" 4 | 5 | func B(in a.A) a.A { 6 | return in 7 | } 8 | -------------------------------------------------------------------------------- /testing/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dave/play/testing/a" 7 | "github.com/dave/play/testing/b" 8 | ) 9 | 10 | func main() { 11 | fmt.Println(b.B(a.A{A: 1})) 12 | } 13 | -------------------------------------------------------------------------------- /views/add-file.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "strings" 7 | 8 | "github.com/dave/play/actions" 9 | "github.com/dave/play/models" 10 | "github.com/dave/play/stores" 11 | "github.com/gopherjs/gopherjs/js" 12 | "github.com/gopherjs/vecty" 13 | "github.com/gopherjs/vecty/elem" 14 | "github.com/gopherjs/vecty/event" 15 | "github.com/gopherjs/vecty/prop" 16 | ) 17 | 18 | type AddFileModal struct { 19 | *Modal 20 | input *vecty.HTML 21 | } 22 | 23 | func NewAddFileModal(app *stores.App) *AddFileModal { 24 | v := &AddFileModal{} 25 | v.Modal = &Modal{ 26 | app: app, 27 | id: models.AddFileModal, 28 | title: "Add file", 29 | action: v.save, 30 | shown: func() { 31 | js.Global.Call("$", "#add-file-input").Call("focus") 32 | js.Global.Call("$", "#add-file-input").Call("val", "") 33 | }, 34 | } 35 | return v 36 | } 37 | 38 | func (v *AddFileModal) Render() vecty.ComponentOrHTML { 39 | v.input = elem.Input( 40 | vecty.Markup( 41 | prop.Type(prop.TypeText), 42 | vecty.Class("form-control"), 43 | prop.ID("add-file-input"), 44 | event.KeyPress(func(ev *vecty.Event) { 45 | if ev.Get("keyCode").Int() == 13 { 46 | ev.Call("preventDefault") 47 | v.save(ev) 48 | } 49 | }), 50 | ), 51 | ) 52 | return v.Body( 53 | elem.Form( 54 | elem.Div( 55 | vecty.Markup(vecty.Class("form-group")), 56 | elem.Label( 57 | vecty.Markup( 58 | vecty.Property("for", "add-file-input"), 59 | vecty.Class("col-form-label"), 60 | ), 61 | vecty.Text("Filename"), 62 | ), 63 | v.input, 64 | ), 65 | ), 66 | ).Build() 67 | } 68 | 69 | func (v *AddFileModal) save(*vecty.Event) { 70 | value := v.input.Node().Get("value").String() 71 | if strings.Contains(value, "/") { 72 | v.app.Fail(fmt.Errorf("filename %s must not contain a slash", value)) 73 | return 74 | } 75 | if !strings.HasSuffix(value, ".go") && !strings.Contains(value, ".") { 76 | value = value + ".go" 77 | } 78 | if v.app.Source.HasFile(v.app.Editor.CurrentPackage(), value) { 79 | v.app.Fail(fmt.Errorf("%s already exists", value)) 80 | return 81 | } 82 | v.app.Dispatch(&actions.ModalClose{Modal: models.AddFileModal}) 83 | v.app.Dispatch(&actions.AddFile{Name: value}) 84 | } 85 | -------------------------------------------------------------------------------- /views/add-package.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dave/play/actions" 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/gopherjs/gopherjs/js" 10 | "github.com/gopherjs/vecty" 11 | "github.com/gopherjs/vecty/elem" 12 | "github.com/gopherjs/vecty/event" 13 | "github.com/gopherjs/vecty/prop" 14 | ) 15 | 16 | type AddPackageModal struct { 17 | *Modal 18 | input *vecty.HTML 19 | } 20 | 21 | func NewAddPackageModal(app *stores.App) *AddPackageModal { 22 | v := &AddPackageModal{} 23 | v.Modal = &Modal{ 24 | app: app, 25 | id: models.AddPackageModal, 26 | title: "Add package", 27 | action: v.save, 28 | shown: func() { 29 | js.Global.Call("$", "#add-package-input").Call("focus") 30 | js.Global.Call("$", "#add-package-input").Call("val", "") 31 | }, 32 | } 33 | return v 34 | } 35 | 36 | func (v *AddPackageModal) Render() vecty.ComponentOrHTML { 37 | v.input = elem.Input( 38 | vecty.Markup( 39 | prop.Type(prop.TypeText), 40 | vecty.Class("form-control"), 41 | prop.ID("add-package-input"), 42 | event.KeyPress(func(ev *vecty.Event) { 43 | if ev.Get("keyCode").Int() == 13 { 44 | ev.Call("preventDefault") 45 | v.save(ev) 46 | } 47 | }), 48 | ), 49 | ) 50 | return v.Body( 51 | elem.Form( 52 | elem.Div( 53 | vecty.Markup(vecty.Class("form-group")), 54 | elem.Label( 55 | vecty.Markup( 56 | vecty.Property("for", "add-package-input"), 57 | vecty.Class("col-form-label"), 58 | ), 59 | vecty.Text("Package path"), 60 | ), 61 | v.input, 62 | ), 63 | ), 64 | ).Build() 65 | } 66 | 67 | func (v *AddPackageModal) save(*vecty.Event) { 68 | value := v.input.Node().Get("value").String() 69 | if v.app.Source.HasPackage(value) { 70 | v.app.Fail(fmt.Errorf("%s already exists", value)) 71 | return 72 | } 73 | v.app.Dispatch(&actions.ModalClose{Modal: models.AddPackageModal}) 74 | v.app.Dispatch(&actions.AddPackage{Path: value}) 75 | } 76 | -------------------------------------------------------------------------------- /views/build-tags.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dave/play/actions" 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/gopherjs/vecty" 10 | "github.com/gopherjs/vecty/elem" 11 | "github.com/gopherjs/vecty/prop" 12 | ) 13 | 14 | type BuildTagsModal struct { 15 | *Modal 16 | input *vecty.HTML 17 | } 18 | 19 | func NewBuildTagsModal(app *stores.App) *BuildTagsModal { 20 | v := &BuildTagsModal{} 21 | v.Modal = &Modal{ 22 | app: app, 23 | id: models.BuildTagsModal, 24 | title: "Build tags", 25 | action: v.action, 26 | } 27 | return v 28 | } 29 | 30 | func (v *BuildTagsModal) Render() vecty.ComponentOrHTML { 31 | v.input = elem.Input(vecty.Markup( 32 | vecty.Class("form-control"), 33 | prop.Type(prop.TypeText), 34 | prop.ID("build-tags-input"), 35 | prop.Value(strings.Join(v.app.Compile.Tags(), " ")), 36 | )) 37 | 38 | return v.Body( 39 | elem.Form( 40 | elem.Div( 41 | vecty.Markup( 42 | vecty.Class("form-group"), 43 | ), 44 | elem.Label( 45 | vecty.Markup( 46 | vecty.Property("for", "build-tags-input"), 47 | vecty.Class("col-form-label"), 48 | ), 49 | vecty.Text("Build tags"), 50 | ), 51 | v.input, 52 | ), 53 | ), 54 | ).Build() 55 | } 56 | 57 | func (v *BuildTagsModal) action(*vecty.Event) { 58 | tags := strings.Fields(v.input.Node().Get("value").String()) 59 | v.app.Dispatch(&actions.ModalClose{Modal: models.BuildTagsModal}) 60 | if !compare(v.app.Compile.Tags(), tags) { 61 | v.app.Dispatch(&actions.BuildTags{Tags: tags}) 62 | } 63 | } 64 | 65 | func compare(a, b []string) bool { 66 | if len(a) != len(b) { 67 | return false 68 | } 69 | for i := range a { 70 | if a[i] != b[i] { 71 | return false 72 | } 73 | } 74 | return true 75 | } 76 | -------------------------------------------------------------------------------- /views/clash-warning.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/gopherjs/vecty" 10 | "github.com/gopherjs/vecty/elem" 11 | ) 12 | 13 | type ClashWarningModal struct { 14 | *Modal 15 | } 16 | 17 | func NewClashWarningModal(app *stores.App) *ClashWarningModal { 18 | v := &ClashWarningModal{ 19 | &Modal{ 20 | app: app, 21 | id: models.ClashWarningModal, 22 | title: "Warning", 23 | action: nil, 24 | }, 25 | } 26 | return v 27 | } 28 | 29 | func (v *ClashWarningModal) Render() vecty.ComponentOrHTML { 30 | var paragraphs []vecty.MarkupOrChild 31 | for source, compiled := range v.app.Scanner.Clashes() { 32 | var paths []string 33 | for p := range compiled { 34 | paths = append(paths, p) 35 | } 36 | var text string 37 | if len(compiled) == 0 { 38 | continue 39 | } else { 40 | sort.Strings(paths) 41 | if len(compiled) == 1 { 42 | text = fmt.Sprintf("Source package %s is imported by this pre-compiled package:", source) 43 | } else { 44 | text = fmt.Sprintf("Source package %s is imported by these pre-compiled packages:", source) 45 | } 46 | var items []vecty.MarkupOrChild 47 | for _, path := range paths { 48 | items = append(items, elem.ListItem( 49 | vecty.Text(path), 50 | )) 51 | } 52 | paragraphs = append(paragraphs, elem.Paragraph( 53 | vecty.Text(text), 54 | elem.UnorderedList(items...), 55 | )) 56 | } 57 | 58 | } 59 | var text string 60 | if len(v.app.Scanner.Clashes()) == 1 { 61 | text = "If the external interface this source packages changes, your code may break at run-time. To solve this, either load the source for all pre-compiled packages, or use the Update feature each time you change the external interface of the source package." 62 | } else { 63 | text = "If the external interface these source packages change, your code may break at run-time. To solve this, either load the source for all pre-compiled packages, or use the Update feature each time you change the external interface of the source packages." 64 | } 65 | paragraphs = append(paragraphs, elem.Paragraph( 66 | vecty.Text(text), 67 | )) 68 | 69 | return v.Body( 70 | paragraphs..., 71 | ).Build() 72 | } 73 | -------------------------------------------------------------------------------- /views/delete-file.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/play/actions" 5 | "github.com/dave/play/models" 6 | "github.com/dave/play/stores" 7 | "github.com/gopherjs/vecty" 8 | "github.com/gopherjs/vecty/elem" 9 | "github.com/gopherjs/vecty/prop" 10 | ) 11 | 12 | type DeleteFileModal struct { 13 | *Modal 14 | sel *vecty.HTML 15 | } 16 | 17 | func NewDeleteFileModal(app *stores.App) *DeleteFileModal { 18 | v := &DeleteFileModal{} 19 | v.Modal = &Modal{ 20 | app: app, 21 | id: models.DeleteFileModal, 22 | title: "Delete file", 23 | action: v.action, 24 | } 25 | return v 26 | } 27 | 28 | func (v *DeleteFileModal) Render() vecty.ComponentOrHTML { 29 | items := []vecty.MarkupOrChild{ 30 | vecty.Markup( 31 | vecty.Class("form-control"), 32 | prop.ID("delete-file-select"), 33 | ), 34 | } 35 | for _, name := range v.app.Source.Filenames(v.app.Editor.CurrentPackage()) { 36 | items = append(items, 37 | elem.Option( 38 | vecty.Markup( 39 | prop.Value(name), 40 | vecty.Property("selected", v.app.Editor.CurrentFile() == name), 41 | ), 42 | vecty.Text(name), 43 | ), 44 | ) 45 | } 46 | v.sel = elem.Select(items...) 47 | 48 | return v.Body( 49 | elem.Form( 50 | elem.Div( 51 | vecty.Markup( 52 | vecty.Class("form-group"), 53 | ), 54 | elem.Label( 55 | vecty.Markup( 56 | vecty.Property("for", "delete-file-select"), 57 | vecty.Class("col-form-label"), 58 | ), 59 | vecty.Text("File"), 60 | ), 61 | v.sel, 62 | ), 63 | ), 64 | ).Build() 65 | } 66 | 67 | func (v *DeleteFileModal) action(*vecty.Event) { 68 | n := v.sel.Node() 69 | i := n.Get("selectedIndex").Int() 70 | value := n.Get("options").Index(i).Get("value").String() 71 | v.app.Dispatch(&actions.ModalClose{Modal: models.DeleteFileModal}) 72 | v.app.Dispatch(&actions.DeleteFile{Name: value}) 73 | } 74 | -------------------------------------------------------------------------------- /views/deploy-done.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/play/models" 5 | "github.com/dave/play/stores" 6 | "github.com/gopherjs/vecty" 7 | "github.com/gopherjs/vecty/elem" 8 | "github.com/gopherjs/vecty/event" 9 | "github.com/gopherjs/vecty/prop" 10 | ) 11 | 12 | type DeployDoneModal struct { 13 | *Modal 14 | } 15 | 16 | func NewDeployDoneModal(app *stores.App) *DeployDoneModal { 17 | v := &DeployDoneModal{ 18 | &Modal{ 19 | app: app, 20 | id: models.DeployDoneModal, 21 | title: "Deployed", 22 | }, 23 | } 24 | return v 25 | } 26 | 27 | func (v *DeployDoneModal) Render() vecty.ComponentOrHTML { 28 | return v.Body( 29 | elem.Form( 30 | elem.Div( 31 | vecty.Markup(vecty.Class("form-group")), 32 | elem.Label( 33 | vecty.Markup( 34 | vecty.Property("for", "deploy-done-modal"), 35 | vecty.Class("col-form-label"), 36 | ), 37 | vecty.Text("Link"), 38 | ), 39 | elem.Input( 40 | vecty.Markup( 41 | prop.Type(prop.TypeText), 42 | vecty.Class("form-control"), 43 | prop.ID("deploy-done-input-link"), 44 | event.Focus(func(ev *vecty.Event) { 45 | ev.Target.Call("select") 46 | }).PreventDefault(), 47 | prop.Value(v.app.Deploy.Index()), 48 | ), 49 | ), 50 | elem.Small( 51 | vecty.Markup( 52 | vecty.Class("form-text", "text-muted"), 53 | ), 54 | elem.Anchor( 55 | vecty.Markup( 56 | prop.Href(v.app.Deploy.Index()), 57 | vecty.Property("target", "_blank"), 58 | ), 59 | vecty.Text("Click here"), 60 | ), 61 | vecty.Text(". Use the link for testing and toy projects. Remember you're sharing the jsgo.io domain with everyone else, so the browser environment should be considered toxic."), 62 | ), 63 | elem.Label( 64 | vecty.Markup( 65 | vecty.Property("for", "deploy-done-modal"), 66 | vecty.Class("col-form-label"), 67 | ), 68 | vecty.Text("Loader JS"), 69 | ), 70 | elem.Input( 71 | vecty.Markup( 72 | prop.Type(prop.TypeText), 73 | vecty.Class("form-control"), 74 | prop.ID("deploy-done-input-loader"), 75 | event.Focus(func(ev *vecty.Event) { 76 | ev.Target.Call("select") 77 | }).PreventDefault(), 78 | prop.Value(v.app.Deploy.LoaderJs()), 79 | ), 80 | ), 81 | elem.Small( 82 | vecty.Markup( 83 | vecty.Class("form-text", "text-muted"), 84 | ), 85 | vecty.Text("For production, use the Loader JS in a script tag on your own site."), 86 | ), 87 | ), 88 | ), 89 | ).Build() 90 | } 91 | -------------------------------------------------------------------------------- /views/editor.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "time" 5 | 6 | "strings" 7 | 8 | "github.com/dave/play/actions" 9 | "github.com/dave/play/stores" 10 | "github.com/gopherjs/gopherjs/js" 11 | "github.com/gopherjs/vecty" 12 | "github.com/gopherjs/vecty/elem" 13 | "github.com/gopherjs/vecty/prop" 14 | "github.com/tulir/gopher-ace" 15 | "honnef.co/go/js/dom" 16 | ) 17 | 18 | type Editor struct { 19 | vecty.Core 20 | app *stores.App 21 | 22 | editor ace.Editor 23 | } 24 | 25 | func NewEditor(app *stores.App) *Editor { 26 | v := &Editor{ 27 | app: app, 28 | } 29 | return v 30 | } 31 | 32 | func getEditorMode(filename string) string { 33 | filename = strings.ToLower(filename) 34 | switch { 35 | case strings.HasSuffix(filename, ".go"): 36 | return "ace/mode/golang" 37 | case strings.HasSuffix(filename, ".html"): 38 | return "ace/mode/html" 39 | case strings.HasSuffix(filename, ".js"): 40 | return "ace/mode/javascript" 41 | case strings.HasSuffix(filename, ".md"): 42 | return "ace/mode/markdown" 43 | default: 44 | return "ace/mode/plain_text" 45 | } 46 | } 47 | 48 | func (v *Editor) Mount() { 49 | v.app.Watch(v, func(done chan struct{}) { 50 | defer close(done) 51 | if v.app.Source.Current() != v.editor.GetValue() { 52 | // only update the editor if the text is changed 53 | v.editor.SetValue(v.app.Source.Current()) 54 | v.editor.ClearSelection() 55 | v.editor.MoveCursorTo(0, 0) 56 | } 57 | correctMode := getEditorMode(v.app.Editor.CurrentFile()) 58 | currentMode := v.editor.GetOption("mode").String() 59 | if correctMode != currentMode { 60 | v.editor.SetOptions(map[string]interface{}{ 61 | "mode": correctMode, 62 | }) 63 | } 64 | }) 65 | 66 | v.editor = ace.Edit("editor") 67 | v.editor.SetOptions(map[string]interface{}{ 68 | "mode": "ace/mode/golang", 69 | "enableLinking": true, 70 | }) 71 | v.editor.On("linkClick", func(d *js.Object) { 72 | data, ok := d.Interface().(map[string]interface{}) 73 | if !ok { 74 | return 75 | } 76 | token, ok := data["token"].(map[string]interface{}) 77 | if !ok { 78 | return 79 | } 80 | t, ok := token["type"].(string) 81 | if !ok { 82 | return 83 | } 84 | if t != "markup.underline" { 85 | return 86 | } 87 | value, ok := token["value"].(string) 88 | if !ok { 89 | return 90 | } 91 | if v.app.Source.HasFile(v.app.Editor.CurrentPackage(), value) { 92 | v.app.Dispatch(&actions.ChangeFile{ 93 | Path: v.app.Editor.CurrentPackage(), 94 | Name: value, 95 | }) 96 | } 97 | }) 98 | 99 | dom.GetWindow().AddEventListener("resize", false, func(event dom.Event) { 100 | v.Resize() 101 | }) 102 | 103 | v.editor.Get("renderer").Call("on", "afterRender", func() { 104 | v.Resize() 105 | }) 106 | 107 | var last *struct{} 108 | v.editor.OnChange(func(ev *js.Object) { 109 | last = &struct{}{} 110 | before := last 111 | go func() { 112 | <-time.After(time.Millisecond * 250) 113 | if before == last { 114 | value := v.editor.GetValue() 115 | if value == v.app.Source.Current() { 116 | // don't fire event if text hasn't changed 117 | return 118 | } 119 | v.app.Dispatch(&actions.UserChangedText{ 120 | Text: value, 121 | }) 122 | } 123 | }() 124 | }) 125 | } 126 | 127 | func (v *Editor) Resize() { 128 | if v.editor.Object != nil { 129 | v.editor.Call("resize") 130 | } else { 131 | v.app.Debug("************************************************************") 132 | v.app.Debug("*** skipped editor resize because v.editor.Object == nil ***") 133 | v.app.Debug("************************************************************") 134 | } 135 | } 136 | 137 | func (v *Editor) Unmount() { 138 | v.app.Delete(v) 139 | } 140 | 141 | func (v *Editor) Render() vecty.ComponentOrHTML { 142 | 143 | editorDisplay := "none" 144 | if len(v.app.Source.Packages()) > 0 && len(v.app.Source.Files(v.app.Editor.CurrentPackage())) > 0 { 145 | editorDisplay = "" 146 | } 147 | 148 | return elem.Div( 149 | vecty.Markup( 150 | prop.ID("editor"), 151 | vecty.Class("editor"), 152 | vecty.Style("display", editorDisplay), 153 | ), 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /views/help.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/play/actions" 5 | "github.com/dave/play/models" 6 | "github.com/dave/play/stores" 7 | "github.com/gopherjs/vecty" 8 | "github.com/gopherjs/vecty/elem" 9 | "github.com/russross/blackfriday" 10 | ) 11 | 12 | type HelpModal struct { 13 | *Modal 14 | } 15 | 16 | func NewHelpModal(app *stores.App) *HelpModal { 17 | v := &HelpModal{} 18 | v.Modal = &Modal{ 19 | app: app, 20 | id: models.HelpModal, 21 | title: "Help", 22 | action: v.action, 23 | large: true, 24 | } 25 | return v 26 | } 27 | 28 | func (v *HelpModal) Render() vecty.ComponentOrHTML { 29 | 30 | // Render the markdown input into HTML using Blackfriday. 31 | unsafeHTML := blackfriday.MarkdownCommon([]byte(helpMarkdown)) 32 | 33 | // Sanitize the HTML. 34 | //safeHTML := string(bluemonday.UGCPolicy().SanitizeBytes(unsafeHTML)) 35 | 36 | return v.Body( 37 | elem.Div( 38 | vecty.Markup( 39 | vecty.UnsafeHTML(string(unsafeHTML)), 40 | ), 41 | ), 42 | ).Build() 43 | } 44 | 45 | func (v *HelpModal) action(*vecty.Event) { 46 | v.app.Dispatch(&actions.ModalClose{Modal: models.HelpModal}) 47 | } 48 | 49 | var helpMarkdown = ` 50 | Edit and run Go in the browser, supporting arbitrary import paths! 51 | 52 | https://play.jsgo.io/ 53 | 54 | [title936803092](https://play.jsgo.io/) 55 | 56 | The jsgo playground is an extension of the jsgo compiler. The compiler allows you to easily compile Go 57 | to JS using GopherJS, and automatically host the results in an aggressively cached CDN. The playground 58 | adds an online editor and many other features (see below). 59 | 60 | The unique feature of the jsgo playground is that it supports arbitrary import paths. Other Go playgrounds 61 | are limited to just the Go standard library. 62 | 63 | For more for more info: 64 | 65 | * jsgo compiler: https://github.com/dave/jsgo 66 | * jsgo playground: https://github.com/dave/play 67 | 68 | ## Demos 69 | 70 | Here's the simplest demo - it just writes to the console and to the page: 71 | 72 | * https://play.jsgo.io/github.com/dave/jstest 73 | 74 | Here's a couple of simple demos that accept files by drag and drop. The first compresses dropped files to 75 | a zip. The second compresses images to jpg. They use the Go standard library zip / image libraries, which 76 | work flawlessly in the browser: 77 | 78 | * https://play.jsgo.io/github.com/dave/zip 79 | * https://play.jsgo.io/github.com/dave/img 80 | 81 | The amazing ebiten 2D games library is a perfect example of the power of Go in the browser. Here's some 82 | demos: 83 | 84 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/2048 85 | * https://play.jsgo.io/github.com/hajimehoshi/go-inovation 86 | * https://play.jsgo.io/github.com/hajimehoshi/ebiten/examples/flappy 87 | 88 | ## Contact 89 | 90 | If you'd like to chat more about the project, feel free to [add an issue](https://github.com/dave/play/issues), 91 | mention [@dave](https://github.com/dave/) or post in the #gopherjs channel of the Gophers Slack. I'm 92 | happy to help! 93 | 94 | ## Features 95 | 96 | #### Initialise 97 | The URL can be used to initialise with code in several ways: 98 | 99 | * Load a Go package with ` + "`" + `/{{ Package path }}` + "`" + ` 100 | * Load a Github Gist with ` + "`" + `/gist.github.com/{{ Gist ID }}` + "`" + ` 101 | * Load a shared project with ` + "`" + `/{{ Share ID }}` + "`" + ` 102 | * Load a ` + "`" + `play.golang.org` + "`" + ` share with ` + "`" + `/p/{{ Go playground ID }}` + "`" + ` 103 | 104 | run 105 | 106 | #### Run 107 | Click the ` + "`" + `Run` + "`" + ` button to run your code in the right-hand panel. If the imports have been changed recently, 108 | the dependencies will be refreshed before running. 109 | 110 |
111 | 112 | format 113 | 114 | #### Format code 115 | Use the ` + "`" + `Format code` + "`" + ` option to run ` + "`" + `gofmt` + "`" + ` on your code. This is executed automatically when the ` + "`" + `Run` + "`" + `, 116 | ` + "`" + `Update` + "`" + `, ` + "`" + `Share` + "`" + ` or ` + "`" + `Deploy` + "`" + ` features are used. 117 | 118 |
119 | 120 | update 121 | 122 | #### Update 123 | If you update a dependency, use the ` + "`" + `Update` + "`" + ` option, which does the equivalent of ` + "`" + `go get -u` + "`" + ` and refreshes 124 | the changes in any import or dependency. 125 | 126 |
127 | 128 | share 129 | 130 | #### Share 131 | To share your project with others, use the ` + "`" + `Share` + "`" + ` option. Your project will be persisted to a json file 132 | on ` + "`" + `src.jsgo.io` + "`" + ` and the page will update to a sharable URL. 133 | 134 |
135 | 136 | deploy 137 | 138 | #### Deploy 139 | To deploy your code to [jsgo.io](https://jsgo.io), use the ` + "`" + `Deploy` + "`" + ` feature. A modal will be displayed with the 140 | link to the page on ` + "`" + `jsgo.io` + "`" + `, and the Loader JS on ` + "`" + `pkg.jsgo.io` + "`" + `. 141 | 142 | Use the ` + "`" + `jsgo.io` + "`" + ` link for testing and toy projects. Remember you're sharing the ` + "`" + `jsgo.io` + "`" + ` domain with 143 | everyone else, so the browser environment should be considered toxic. 144 | 145 | The Loader JS on ` + "`" + `pkg.jsgo.io` + "`" + ` can be used in production, and should be added to a script tag on your 146 | own website. See [github.com/dave/jsgo](https://github.com/dave/jsgo) for more information. 147 | 148 |
149 | 150 | console 151 | 152 | #### Console 153 | Writes to ` + "`" + `os.Stdout` + "`" + ` are redirected to a playground console, which can be toggled using the ` + "`" + `Show console` + "`" + ` 154 | option. The console will automatically appear the first time it's written to. 155 | 156 |
157 | 158 | minify 159 | 160 | #### Minify 161 | In normal usage, all JS is minified. For debugging, this can be toggled with the ` + "`" + `Minify JS` + "`" + ` option. 162 | 163 |
164 | 165 | tags 166 | 167 | #### Build tags 168 | The build tags used when compiling can be edited with the ` + "`" + `Build tags...` + "`" + ` option. The selected build 169 | tags are persisted when using the ` + "`" + `Share` + "`" + ` feature. 170 | 171 |
172 | 173 | download 174 | 175 | #### Download 176 | The ` + "`" + `Download` + "`" + ` option downloads the project. Single file projects are downloaded as a single file, while 177 | multi-file projects download as a zip. 178 | 179 |
180 | 181 | download 182 | 183 | #### Upload 184 | Files can be uploaded to the project simply by drag+drop. Zip files generated by the ` + "`" + `Download` + "`" + ` feature 185 | can be uploaded to restore a multi-file project. 186 | 187 |
188 | 189 | files 190 | 191 | #### File menu 192 | Change the selected file with the file menu. 193 | 194 |
195 | 196 | add-file 197 | 198 | #### Add file 199 | Add a file to the current package with the ` + "`" + `Add file` + "`" + ` option. Only ` + "`" + `.go` + "`" + `, ` + "`" + `.md` + "`" + ` and ` + "`" + `.inc.js` + "`" + ` files are 200 | supported. If no extension is supplied, ` + "`" + `.go` + "`" + ` is added. 201 | 202 |
203 | 204 | delete-file 205 | 206 | #### Delete file 207 | Delete a file from the current package with the ` + "`" + `Delete file` + "`" + ` option. 208 | 209 |
210 | 211 | package 212 | 213 | #### Package menu 214 | Change the selected package with the package menu. 215 | 216 |
217 | 218 | add-package 219 | 220 | #### Add package 221 | Add an empty package with the ` + "`" + `Add package` + "`" + ` option. 222 | 223 |
224 | 225 | load-package 226 | 227 | #### Load package 228 | The source for an import or dependency can be loaded with the ` + "`" + `Load package` + "`" + ` option. By default, only 229 | the direct imports of your project are listed. Use the ` + "`" + `Show all dependencies` + "`" + ` option to show the entire 230 | dependency tree. 231 | 232 |
233 | 234 | remove-package 235 | 236 | #### Remove package 237 | A package can be removed with the ` + "`" + `Remove package` + "`" + ` option. 238 | ` 239 | -------------------------------------------------------------------------------- /views/load-package.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/dave/play/actions" 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/gopherjs/vecty" 10 | "github.com/gopherjs/vecty/elem" 11 | "github.com/gopherjs/vecty/event" 12 | "github.com/gopherjs/vecty/prop" 13 | ) 14 | 15 | type LoadPackageModal struct { 16 | *Modal 17 | imps *vecty.HTML 18 | } 19 | 20 | func NewLoadPackageModal(app *stores.App) *LoadPackageModal { 21 | v := &LoadPackageModal{} 22 | v.Modal = &Modal{ 23 | app: app, 24 | id: models.LoadPackageModal, 25 | title: "Load package", 26 | action: v.action, 27 | hidden: func() { 28 | app.Dispatch(&actions.ShowAllDepsChange{State: false}) 29 | }, 30 | } 31 | return v 32 | } 33 | 34 | func (v *LoadPackageModal) Render() vecty.ComponentOrHTML { 35 | items := []vecty.MarkupOrChild{ 36 | vecty.Markup( 37 | vecty.Class("form-control"), 38 | prop.ID("load-package-imps-select"), 39 | ), 40 | } 41 | var paths []string 42 | if !v.app.Page.ShowAllDeps() { 43 | paths = v.app.Scanner.AllImportsOrdered() 44 | } else { 45 | imps := v.app.Scanner.AllImports() 46 | for p := range imps { 47 | paths = append(paths, p) 48 | } 49 | for p := range v.app.Archive.Cache() { 50 | if !imps[p] { 51 | paths = append(paths, p) 52 | } 53 | } 54 | sort.Strings(paths) 55 | } 56 | for _, path := range paths { 57 | items = append(items, 58 | elem.Option( 59 | vecty.Markup( 60 | prop.Value(path), 61 | ), 62 | vecty.Text(path), 63 | ), 64 | ) 65 | } 66 | v.imps = elem.Select(items...) 67 | 68 | infoDisplay := "none" 69 | if v.app.Page.ShowAllDeps() && !v.app.Archive.AllFresh() { 70 | infoDisplay = "" 71 | } 72 | 73 | return v.Body( 74 | elem.Form( 75 | elem.Div( 76 | vecty.Markup( 77 | vecty.Class("form-group"), 78 | ), 79 | elem.Label( 80 | vecty.Markup( 81 | vecty.Property("for", "load-package-imps-select"), 82 | vecty.Class("col-form-label"), 83 | ), 84 | vecty.Text("Imports"), 85 | ), 86 | v.imps, 87 | ), 88 | elem.Div( 89 | vecty.Markup( 90 | vecty.Class("form-check"), 91 | ), 92 | elem.Input( 93 | vecty.Markup( 94 | prop.Type(prop.TypeCheckbox), 95 | vecty.Class("form-check-input"), 96 | prop.ID("load-package-show-all-deps-checkbox"), 97 | prop.Checked(v.app.Page.ShowAllDeps()), 98 | event.Click(func(e *vecty.Event) { 99 | v.app.Dispatch(&actions.ShowAllDepsChange{ 100 | State: e.Target.Get("checked").Bool(), 101 | }) 102 | }).PreventDefault(), 103 | ), 104 | ), 105 | elem.Label( 106 | vecty.Markup( 107 | vecty.Class("form-check-label"), 108 | prop.For("load-package-show-all-deps-checkbox"), 109 | ), 110 | vecty.Text("Show all dependencies"), 111 | ), 112 | ), 113 | elem.Div( 114 | vecty.Markup( 115 | vecty.Class("form-text"), 116 | vecty.Style("display", infoDisplay), 117 | ), 118 | vecty.Tag( 119 | "svg", 120 | vecty.Markup( 121 | vecty.Namespace("http://www.w3.org/2000/svg"), 122 | vecty.Attribute("width", "14"), 123 | vecty.Attribute("height", "16"), 124 | vecty.Attribute("viewBox", "0 0 14 16"), 125 | vecty.Class("octicon"), 126 | vecty.Style("margin-right", "4px"), 127 | ), 128 | vecty.Tag( 129 | "path", 130 | vecty.Markup( 131 | vecty.Namespace("http://www.w3.org/2000/svg"), 132 | vecty.Attribute("fill-rule", "evenodd"), 133 | vecty.Attribute("d", "M6.3 5.71a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 8.01c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V8v.01zM7 2.32C3.86 2.32 1.3 4.86 1.3 8c0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 1c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"), 134 | ), 135 | ), 136 | ), 137 | vecty.Text("Dependencies are not fully loaded. "), 138 | elem.Anchor( 139 | vecty.Markup( 140 | prop.Href(""), 141 | event.Click(func(e *vecty.Event) { 142 | v.app.Dispatch(&actions.ModalClose{Modal: models.LoadPackageModal}) 143 | v.app.Dispatch(&actions.RequestStart{Type: models.UpdateRequest}) 144 | }).PreventDefault(), 145 | ), 146 | vecty.Text("Update"), 147 | ), 148 | vecty.Text(" to load."), 149 | ), 150 | ), 151 | ).Build() 152 | } 153 | 154 | func (v *LoadPackageModal) action(*vecty.Event) { 155 | n := v.imps.Node() 156 | i := n.Get("selectedIndex").Int() 157 | value := n.Get("options").Index(i).Get("value").String() 158 | v.app.Dispatch(&actions.ModalClose{Modal: models.LoadPackageModal}) 159 | v.app.Dispatch(&actions.RequestStart{Type: models.GetRequest, Path: value}) 160 | } 161 | -------------------------------------------------------------------------------- /views/menu.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dave/play/actions" 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/gopherjs/vecty" 10 | "github.com/gopherjs/vecty/elem" 11 | "github.com/gopherjs/vecty/event" 12 | "github.com/gopherjs/vecty/prop" 13 | ) 14 | 15 | type Menu struct { 16 | vecty.Core 17 | app *stores.App 18 | 19 | compileButton *vecty.HTML 20 | optionsButton *vecty.HTML 21 | } 22 | 23 | func NewMenu(app *stores.App) *Menu { 24 | v := &Menu{ 25 | app: app, 26 | } 27 | return v 28 | } 29 | 30 | func (v *Menu) Render() vecty.ComponentOrHTML { 31 | 32 | clashWarningDisplay := "none" 33 | if len(v.app.Scanner.Clashes()) > 0 { 34 | clashWarningDisplay = "" 35 | } 36 | 37 | buildTagsText := "Build tags..." 38 | if len(v.app.Compile.Tags()) > 0 { 39 | buildTagsText = fmt.Sprintf("Build tags (%d)...", len(v.app.Compile.Tags())) 40 | } 41 | 42 | return elem.Navigation( 43 | vecty.Markup( 44 | vecty.Class("menu", "navbar", "navbar-expand", "navbar-light", "bg-light"), 45 | ), 46 | elem.UnorderedList( 47 | vecty.Markup( 48 | vecty.Class("navbar-nav", "mr-auto"), 49 | ), 50 | v.renderPackageDropdown(), 51 | v.renderFileDropdown(), 52 | 53 | elem.ListItem( 54 | vecty.Markup( 55 | vecty.Class("nav-item"), 56 | vecty.Style("display", clashWarningDisplay), 57 | ), 58 | elem.Anchor( 59 | vecty.Markup( 60 | prop.Href(""), 61 | vecty.Class("nav-link", "octicon"), 62 | event.Click(func(e *vecty.Event) { 63 | v.app.Dispatch(&actions.ModalOpen{Modal: models.ClashWarningModal}) 64 | }).PreventDefault(), 65 | ), 66 | vecty.Tag( 67 | "svg", 68 | vecty.Markup( 69 | vecty.Namespace("http://www.w3.org/2000/svg"), 70 | vecty.Attribute("width", "14"), 71 | vecty.Attribute("height", "16"), 72 | vecty.Attribute("viewBox", "0 0 14 16"), 73 | ), 74 | vecty.Tag( 75 | "path", 76 | vecty.Markup( 77 | vecty.Namespace("http://www.w3.org/2000/svg"), 78 | vecty.Attribute("fill-rule", "evenodd"), 79 | vecty.Attribute("d", "M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"), 80 | ), 81 | ), 82 | ), 83 | ), 84 | ), 85 | ), 86 | elem.UnorderedList( 87 | vecty.Markup( 88 | vecty.Class("navbar-nav", "ml-auto"), 89 | ), 90 | elem.ListItem( 91 | vecty.Markup( 92 | vecty.Class("nav-item"), 93 | ), 94 | elem.Span( 95 | vecty.Markup( 96 | vecty.Class("navbar-text"), 97 | vecty.Style("margin-right", "10px"), 98 | prop.ID("message"), 99 | ), 100 | vecty.Text(""), 101 | ), 102 | ), 103 | /* 104 | elem.ListItem( 105 | vecty.Markup( 106 | vecty.Class("nav-item"), 107 | ), 108 | elem.Anchor( 109 | vecty.Markup( 110 | prop.Href(""), 111 | vecty.Class("nav-link", "octicon"), 112 | event.Click(func(e *vecty.Event) { 113 | v.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal}) 114 | }).PreventDefault(), 115 | ), 116 | vecty.Tag( 117 | "svg", 118 | vecty.Markup( 119 | vecty.Namespace("http://www.w3.org/2000/svg"), 120 | vecty.Attribute("width", "14"), 121 | vecty.Attribute("height", "16"), 122 | vecty.Attribute("viewBox", "0 0 14 16"), 123 | ), 124 | vecty.Tag( 125 | "path", 126 | vecty.Markup( 127 | vecty.Namespace("http://www.w3.org/2000/svg"), 128 | vecty.Attribute("fill-rule", "evenodd"), 129 | vecty.Attribute("d", "M6 10h2v2H6v-2zm4-3.5C10 8.64 8 9 8 9H6c0-.55.45-1 1-1h.5c.28 0 .5-.22.5-.5v-1c0-.28-.22-.5-.5-.5h-1c-.28 0-.5.22-.5.5V7H4c0-1.5 1.5-3 3-3s3 1 3 2.5zM7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7z"), 130 | ), 131 | ), 132 | ), 133 | ), 134 | ), 135 | */ 136 | elem.ListItem( 137 | vecty.Markup( 138 | vecty.Class("nav-item", "btn-group"), 139 | ), 140 | elem.Button( 141 | vecty.Markup( 142 | vecty.Property("type", "button"), 143 | vecty.Class("btn", "btn-primary"), 144 | event.Click(func(e *vecty.Event) { 145 | if v.app.Connection.Open() || v.app.Compile.Compiling() { 146 | return 147 | } else { 148 | v.app.Dispatch(&actions.FormatCode{ 149 | Then: &actions.CompileStart{}, 150 | }) 151 | } 152 | }).PreventDefault(), 153 | ), 154 | vecty.Text("Run"), 155 | ), 156 | elem.Button( 157 | vecty.Markup( 158 | vecty.Property("type", "button"), 159 | vecty.Data("toggle", "dropdown"), 160 | vecty.Property("aria-haspopup", "true"), 161 | vecty.Property("aria-expanded", "false"), 162 | vecty.Class("btn", "btn-primary", "dropdown-toggle", "dropdown-toggle-split"), 163 | ), 164 | elem.Span(vecty.Markup(vecty.Class("sr-only")), vecty.Text("Options")), 165 | ), 166 | elem.Div( 167 | vecty.Markup( 168 | vecty.Class("dropdown-menu", "dropdown-menu-right"), 169 | ), 170 | elem.Anchor( 171 | vecty.Markup( 172 | vecty.Class("dropdown-item"), 173 | prop.Href(""), 174 | event.Click(func(e *vecty.Event) { 175 | v.app.Dispatch(&actions.FormatCode{}) 176 | }).PreventDefault(), 177 | ), 178 | vecty.Text("Format code"), 179 | ), 180 | elem.Div( 181 | vecty.Markup( 182 | vecty.Class("dropdown-divider"), 183 | ), 184 | ), 185 | elem.Anchor( 186 | vecty.Markup( 187 | vecty.Class("dropdown-item"), 188 | prop.Href(""), 189 | event.Click(func(e *vecty.Event) { 190 | v.app.Dispatch(&actions.FormatCode{ 191 | Then: &actions.RequestStart{Type: models.UpdateRequest}, 192 | }) 193 | }).PreventDefault(), 194 | ), 195 | vecty.Text("Update"), 196 | ), 197 | elem.Anchor( 198 | vecty.Markup( 199 | vecty.Class("dropdown-item"), 200 | prop.Href(""), 201 | event.Click(func(e *vecty.Event) { 202 | v.app.Dispatch(&actions.FormatCode{ 203 | Then: &actions.ShareStart{}, 204 | }) 205 | }).PreventDefault(), 206 | ), 207 | vecty.Text("Share"), 208 | ), 209 | elem.Anchor( 210 | vecty.Markup( 211 | vecty.Class("dropdown-item"), 212 | prop.Href(""), 213 | event.Click(func(e *vecty.Event) { 214 | v.app.Dispatch(&actions.FormatCode{ 215 | Then: &actions.DeployStart{}, 216 | }) 217 | }).PreventDefault(), 218 | ), 219 | vecty.Text("Deploy"), 220 | ), 221 | elem.Div( 222 | vecty.Markup( 223 | vecty.Class("dropdown-divider"), 224 | ), 225 | ), 226 | elem.Anchor( 227 | vecty.Markup( 228 | vecty.Class("dropdown-item"), 229 | prop.Href("#"), 230 | event.Click(func(e *vecty.Event) {}).StopPropagation(), 231 | ), 232 | elem.Input( 233 | vecty.Markup( 234 | prop.Type(prop.TypeCheckbox), 235 | vecty.Class("form-check-input", "dropdown-item"), 236 | prop.ID("dropdownCheckConsole"), 237 | prop.Checked(v.app.Page.Console()), 238 | event.Change(func(e *vecty.Event) { 239 | v.app.Dispatch(&actions.ConsoleToggleClick{}) 240 | }), 241 | vecty.Style("cursor", "pointer"), 242 | ), 243 | ), 244 | elem.Label( 245 | vecty.Markup( 246 | vecty.Class("form-check-label"), 247 | prop.For("dropdownCheckConsole"), 248 | vecty.Style("cursor", "pointer"), 249 | ), 250 | vecty.Text("Show console"), 251 | ), 252 | ), 253 | elem.Anchor( 254 | vecty.Markup( 255 | vecty.Class("dropdown-item"), 256 | prop.Href("#"), 257 | event.Click(func(e *vecty.Event) {}).StopPropagation(), 258 | ), 259 | elem.Input( 260 | vecty.Markup( 261 | prop.Type(prop.TypeCheckbox), 262 | vecty.Class("form-check-input", "dropdown-item"), 263 | prop.ID("dropdownCheckMinify"), 264 | prop.Checked(v.app.Page.Minify()), 265 | event.Change(func(e *vecty.Event) { 266 | v.app.Dispatch(&actions.MinifyToggleClick{}) 267 | }), 268 | vecty.Style("cursor", "pointer"), 269 | ), 270 | ), 271 | elem.Label( 272 | vecty.Markup( 273 | vecty.Class("form-check-label"), 274 | prop.For("dropdownCheckMinify"), 275 | vecty.Style("cursor", "pointer"), 276 | ), 277 | vecty.Text("Minify JS"), 278 | ), 279 | ), 280 | elem.Anchor( 281 | vecty.Markup( 282 | vecty.Class("dropdown-item"), 283 | prop.Href(""), 284 | event.Click(func(e *vecty.Event) { 285 | v.app.Dispatch(&actions.ModalOpen{Modal: models.BuildTagsModal}) 286 | }).PreventDefault(), 287 | ), 288 | vecty.Text(buildTagsText), 289 | ), 290 | elem.Div( 291 | vecty.Markup( 292 | vecty.Class("dropdown-divider"), 293 | ), 294 | ), 295 | elem.Anchor( 296 | vecty.Markup( 297 | vecty.Class("dropdown-item"), 298 | prop.Href(""), 299 | event.Click(func(e *vecty.Event) { 300 | v.app.Dispatch(&actions.DownloadClick{}) 301 | }).PreventDefault(), 302 | ), 303 | vecty.Text("Download"), 304 | ), 305 | elem.Div( 306 | vecty.Markup( 307 | vecty.Class("dropdown-divider"), 308 | ), 309 | ), 310 | elem.Anchor( 311 | vecty.Markup( 312 | vecty.Class("dropdown-item"), 313 | event.Click(func(e *vecty.Event) { 314 | v.app.Dispatch(&actions.ModalOpen{Modal: models.HelpModal}) 315 | }).PreventDefault(), 316 | ), 317 | vecty.Text("Help"), 318 | ), 319 | elem.Anchor( 320 | vecty.Markup( 321 | vecty.Class("dropdown-item"), 322 | prop.Href("https://github.com/dave/play"), 323 | vecty.Property("target", "_blank"), 324 | ), 325 | vecty.Text("More info"), 326 | ), 327 | elem.Anchor( 328 | vecty.Markup( 329 | vecty.Class("dropdown-item"), 330 | prop.Href("https://patreon.com/davebrophy"), 331 | vecty.Property("target", "_blank"), 332 | ), 333 | vecty.Text("Help with my hosting bills"), 334 | ), 335 | ), 336 | ), 337 | ), 338 | ) 339 | } 340 | 341 | func (v *Menu) renderFileDropdown() *vecty.HTML { 342 | var fileItems []vecty.MarkupOrChild 343 | fileItems = append(fileItems, 344 | vecty.Markup( 345 | vecty.Class("dropdown-menu"), 346 | vecty.Property("aria-labelledby", "fileDropdown"), 347 | ), 348 | ) 349 | for _, name := range v.app.Source.Filenames(v.app.Editor.CurrentPackage()) { 350 | name := name 351 | fileItems = append(fileItems, 352 | elem.Anchor( 353 | vecty.Markup( 354 | vecty.Class("dropdown-item"), 355 | vecty.ClassMap{ 356 | "disabled": name == v.app.Editor.CurrentFile(), 357 | }, 358 | prop.Href(""), 359 | event.Click(func(e *vecty.Event) { 360 | v.app.Dispatch(&actions.UserChangedFile{ 361 | Name: name, 362 | }) 363 | }).PreventDefault(), 364 | ), 365 | vecty.Text(name), 366 | ), 367 | ) 368 | } 369 | fileItems = append(fileItems, 370 | elem.Div( 371 | vecty.Markup( 372 | vecty.Class("dropdown-divider"), 373 | ), 374 | ), 375 | elem.Anchor( 376 | vecty.Markup( 377 | vecty.Class("dropdown-item"), 378 | prop.Href(""), 379 | event.Click(func(e *vecty.Event) { 380 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddFileModal}) 381 | }).PreventDefault(), 382 | ), 383 | vecty.Text("Add file"), 384 | ), 385 | elem.Anchor( 386 | vecty.Markup( 387 | vecty.Class("dropdown-item"), 388 | prop.Href(""), 389 | event.Click(func(e *vecty.Event) { 390 | v.app.Dispatch(&actions.ModalOpen{Modal: models.DeleteFileModal}) 391 | }).PreventDefault(), 392 | ), 393 | vecty.Text("Delete file"), 394 | ), 395 | ) 396 | 397 | classes := vecty.Class("nav-item", "dropdown", "d-none") 398 | if len(v.app.Source.Files(v.app.Editor.CurrentPackage())) > 0 { 399 | classes = vecty.Class("nav-item", "dropdown") 400 | } 401 | 402 | return elem.ListItem( 403 | vecty.Markup( 404 | classes, 405 | ), 406 | elem.Anchor( 407 | vecty.Markup( 408 | prop.ID("fileDropdown"), 409 | prop.Href(""), 410 | vecty.Class("nav-link", "dropdown-toggle"), 411 | vecty.Property("role", "button"), 412 | vecty.Data("toggle", "dropdown"), 413 | vecty.Property("aria-haspopup", "true"), 414 | vecty.Property("aria-expanded", "false"), 415 | event.Click(func(ev *vecty.Event) {}).PreventDefault(), 416 | ), 417 | vecty.Text(v.app.Editor.CurrentFile()), 418 | ), 419 | elem.Div( 420 | fileItems..., 421 | ), 422 | ) 423 | } 424 | 425 | func (v *Menu) renderPackageDropdown() *vecty.HTML { 426 | var packageItems []vecty.MarkupOrChild 427 | packageItems = append(packageItems, 428 | vecty.Markup( 429 | vecty.Class("dropdown-menu"), 430 | vecty.Property("aria-labelledby", "packageDropdown"), 431 | ), 432 | ) 433 | for _, path := range v.app.Source.Packages() { 434 | path := path 435 | packageItems = append(packageItems, 436 | elem.Anchor( 437 | vecty.Markup( 438 | vecty.Class("dropdown-item"), 439 | vecty.ClassMap{ 440 | "disabled": path == v.app.Editor.CurrentPackage(), 441 | }, 442 | prop.Href(""), 443 | event.Click(func(e *vecty.Event) { 444 | v.app.Dispatch(&actions.UserChangedPackage{ 445 | Path: path, 446 | }) 447 | }).PreventDefault(), 448 | ), 449 | vecty.Text(v.app.Scanner.DisplayPath(path)), 450 | ), 451 | ) 452 | } 453 | packageItems = append(packageItems, 454 | elem.Div( 455 | vecty.Markup( 456 | vecty.Class("dropdown-divider"), 457 | ), 458 | ), 459 | elem.Anchor( 460 | vecty.Markup( 461 | vecty.Class("dropdown-item"), 462 | prop.Href(""), 463 | event.Click(func(e *vecty.Event) { 464 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddPackageModal}) 465 | }).PreventDefault(), 466 | ), 467 | vecty.Text("Add package"), 468 | ), 469 | elem.Anchor( 470 | vecty.Markup( 471 | vecty.Class("dropdown-item"), 472 | prop.Href(""), 473 | event.Click(func(e *vecty.Event) { 474 | v.app.Dispatch(&actions.ModalOpen{Modal: models.LoadPackageModal}) 475 | }).PreventDefault(), 476 | ), 477 | vecty.Text("Load package"), 478 | ), 479 | elem.Anchor( 480 | vecty.Markup( 481 | vecty.Class("dropdown-item"), 482 | prop.Href(""), 483 | event.Click(func(e *vecty.Event) { 484 | v.app.Dispatch(&actions.ModalOpen{Modal: models.RemovePackageModal}) 485 | }).PreventDefault(), 486 | ), 487 | vecty.Text("Remove package"), 488 | ), 489 | ) 490 | 491 | classes := vecty.Class("nav-item", "dropdown", "d-none") 492 | if len(v.app.Source.Packages()) > 0 { 493 | classes = vecty.Class("nav-item", "dropdown") 494 | } 495 | 496 | return elem.ListItem( 497 | vecty.Markup( 498 | classes, 499 | ), 500 | elem.Anchor( 501 | vecty.Markup( 502 | prop.ID("packageDropdown"), 503 | prop.Href(""), 504 | vecty.Class("nav-link", "dropdown-toggle"), 505 | vecty.Property("role", "button"), 506 | vecty.Data("toggle", "dropdown"), 507 | vecty.Property("aria-haspopup", "true"), 508 | vecty.Property("aria-expanded", "false"), 509 | event.Click(func(ev *vecty.Event) {}).PreventDefault(), 510 | ), 511 | vecty.Text(v.app.Scanner.DisplayName(v.app.Editor.CurrentPackage())), 512 | ), 513 | elem.Div( 514 | packageItems..., 515 | ), 516 | ) 517 | } 518 | -------------------------------------------------------------------------------- /views/modal.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/play/actions" 5 | "github.com/dave/play/models" 6 | "github.com/dave/play/stores" 7 | "github.com/gopherjs/gopherjs/js" 8 | "github.com/gopherjs/vecty" 9 | "github.com/gopherjs/vecty/elem" 10 | "github.com/gopherjs/vecty/event" 11 | "github.com/gopherjs/vecty/prop" 12 | ) 13 | 14 | type Modal struct { 15 | vecty.Core 16 | app *stores.App 17 | id models.Modal 18 | title string 19 | action func(*vecty.Event) 20 | body []vecty.MarkupOrChild // This should be set on every Render 21 | shown func() 22 | hidden func() 23 | large bool 24 | } 25 | 26 | func (m *Modal) Mount() { 27 | m.app.Watch(m, func(done chan struct{}) { 28 | defer close(done) 29 | modalIsVisible := js.Global.Call("$", "#"+m.id).Call("hasClass", "show").Bool() 30 | shouldBeVisible := m.app.Page.ModalOpen(m.id) 31 | if modalIsVisible != shouldBeVisible { 32 | if shouldBeVisible { 33 | js.Global.Call("$", "#"+m.id).Call("modal", "show") 34 | } else { 35 | js.Global.Call("$", "#"+m.id).Call("modal", "hide") 36 | } 37 | } 38 | }) 39 | js.Global.Call("$", "#"+m.id).Call("on", "shown.bs.modal", func() { 40 | if !m.app.Page.ModalOpen(m.id) { 41 | m.app.Dispatch(&actions.ModalOpen{Modal: m.id}) 42 | } 43 | if m.shown != nil { 44 | m.shown() 45 | } 46 | }) 47 | js.Global.Call("$", "#"+m.id).Call("on", "hidden.bs.modal", func() { 48 | if m.app.Page.ModalOpen(m.id) { 49 | m.app.Dispatch(&actions.ModalClose{Modal: m.id}) 50 | } 51 | if m.hidden != nil { 52 | m.hidden() 53 | } 54 | }) 55 | } 56 | 57 | func (m *Modal) Unmount() { 58 | m.app.Delete(m) 59 | js.Global.Call("$", "#"+m.id).Call("unbind") 60 | } 61 | 62 | func (m *Modal) Body(body ...vecty.MarkupOrChild) *Modal { 63 | m.body = body 64 | return m 65 | } 66 | 67 | func (m *Modal) Build() vecty.ComponentOrHTML { 68 | 69 | body := []vecty.MarkupOrChild{ 70 | vecty.Markup( 71 | vecty.Class("modal-body"), 72 | ), 73 | } 74 | body = append(body, m.body...) 75 | 76 | okDisplay := "" 77 | if m.action == nil { 78 | okDisplay = "none" 79 | } 80 | 81 | modalClass := vecty.Class("modal-dialog") 82 | if m.large { 83 | modalClass = vecty.Class("modal-dialog", "modal-lg") 84 | } 85 | 86 | return elem.Div( 87 | vecty.Markup( 88 | prop.ID(string(m.id)), 89 | vecty.Class("modal"), 90 | vecty.Property("tabindex", "-1"), 91 | vecty.Property("role", "dialog"), 92 | ), 93 | elem.Div( 94 | vecty.Markup( 95 | modalClass, 96 | vecty.Property("role", "dialog"), 97 | ), 98 | elem.Div( 99 | vecty.Markup( 100 | vecty.Class("modal-content"), 101 | ), 102 | elem.Div( 103 | vecty.Markup( 104 | vecty.Class("modal-header"), 105 | ), 106 | elem.Heading5( 107 | vecty.Markup( 108 | vecty.Class("modal-title"), 109 | ), 110 | vecty.Text(m.title), 111 | ), 112 | elem.Button( 113 | vecty.Markup( 114 | prop.Type(prop.TypeButton), 115 | vecty.Class("close"), 116 | vecty.Data("dismiss", "modal"), 117 | vecty.Property("aria-label", "Close"), 118 | ), 119 | elem.Span( 120 | vecty.Markup( 121 | vecty.Property("aria-hidden", "true"), 122 | ), 123 | vecty.Text("×"), 124 | ), 125 | ), 126 | ), 127 | elem.Div( 128 | body..., 129 | ), 130 | elem.Div( 131 | vecty.Markup( 132 | vecty.Class("modal-footer"), 133 | ), 134 | elem.Button( 135 | vecty.Markup( 136 | prop.Type(prop.TypeButton), 137 | vecty.Class("btn", "btn-primary"), 138 | event.Click(m.action).PreventDefault(), 139 | vecty.Style("display", okDisplay), 140 | ), 141 | vecty.Text("OK"), 142 | ), 143 | elem.Button( 144 | vecty.Markup( 145 | prop.Type(prop.TypeButton), 146 | vecty.Class("btn", "btn-secondary"), 147 | vecty.Data("dismiss", "modal"), 148 | ), 149 | vecty.Text("Close"), 150 | ), 151 | ), 152 | ), 153 | ), 154 | ) 155 | } 156 | -------------------------------------------------------------------------------- /views/page.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/dropper" 5 | "github.com/dave/jsgo/config" 6 | "github.com/dave/play/actions" 7 | "github.com/dave/play/models" 8 | "github.com/dave/play/stores" 9 | "github.com/dave/splitter" 10 | "github.com/gopherjs/gopherjs/js" 11 | "github.com/gopherjs/vecty" 12 | "github.com/gopherjs/vecty/elem" 13 | "github.com/gopherjs/vecty/event" 14 | "github.com/gopherjs/vecty/prop" 15 | "honnef.co/go/js/dom" 16 | ) 17 | 18 | type Page struct { 19 | vecty.Core 20 | app *stores.App 21 | 22 | split1, split2 *splitter.Split 23 | editor *Editor 24 | } 25 | 26 | func NewPage(app *stores.App) *Page { 27 | v := &Page{ 28 | app: app, 29 | } 30 | return v 31 | } 32 | 33 | func (v *Page) Mount() { 34 | v.app.Watch(v, func(done chan struct{}) { 35 | defer close(done) 36 | 37 | sizes := v.app.Editor.Sizes() 38 | if v.split1.Changed(sizes) { 39 | v.split1.SetSizes(sizes) 40 | } 41 | 42 | if v.app.Page.Console() && !v.split2.Initialised() { 43 | v.split2.Init( 44 | js.S{"#iframe-holder", "#console-holder"}, 45 | js.M{ 46 | "direction": "vertical", 47 | "sizes": []float64{50.0, 50.0}, 48 | }, 49 | ) 50 | } else if !v.app.Page.Console() && v.split2.Initialised() { 51 | v.split2.Destroy() 52 | } 53 | }) 54 | 55 | v.split1 = splitter.New("split") 56 | v.split1.Init( 57 | js.S{"#left", "#right"}, 58 | js.M{ 59 | "sizes": v.app.Editor.Sizes(), 60 | "onDragEnd": func() { 61 | v.app.Dispatch(&actions.UserChangedSplit{ 62 | Sizes: v.split1.GetSizes(), 63 | }) 64 | }, 65 | }, 66 | ) 67 | 68 | v.split2 = splitter.New("split") 69 | 70 | events := dropper.Initialise(dom.GetWindow().Document().GetElementByID("left")) 71 | go func() { 72 | for ev := range events { 73 | switch ev := ev.(type) { 74 | case dropper.EnterEvent: 75 | v.app.Dispatch(&actions.DragEnter{}) 76 | case dropper.LeaveEvent: 77 | v.app.Dispatch(&actions.DragLeave{}) 78 | case dropper.DropEvent: 79 | v.app.Dispatch(&actions.DragDrop{ 80 | Files: ev, 81 | }) 82 | } 83 | } 84 | }() 85 | 86 | } 87 | 88 | func (v *Page) Unmount() { 89 | v.app.Delete(v) 90 | } 91 | 92 | const Styles = ` 93 | html, body { 94 | height: 100%; 95 | } 96 | #left { 97 | display: flex; 98 | flex-flow: column; 99 | height: 100%; 100 | } 101 | .menu { 102 | min-height: 56px; 103 | } 104 | .editor, .empty-panel { 105 | flex: 1; 106 | width: 100%; 107 | } 108 | .empty-panel { 109 | display: flex; 110 | align-items: center; 111 | justify-content: center; 112 | } 113 | .split { 114 | height: 100%; 115 | width: 100%; 116 | } 117 | .gutter { 118 | height: 100%; 119 | background-color: #eee; 120 | background-repeat: no-repeat; 121 | background-position: 50%; 122 | } 123 | .gutter.gutter-horizontal { 124 | cursor: col-resize; 125 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==') 126 | } 127 | .gutter.gutter-vertical { 128 | cursor: row-resize; 129 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=') 130 | } 131 | .split { 132 | -webkit-box-sizing: border-box; 133 | -moz-box-sizing: border-box; 134 | box-sizing: border-box; 135 | } 136 | .split, .gutter.gutter-horizontal { 137 | float: left; 138 | } 139 | .preview { 140 | border: 0; 141 | height: 100%; 142 | width: 100%; 143 | } 144 | #console-holder { 145 | overflow: auto; 146 | } 147 | #console { 148 | padding:5px; 149 | } 150 | .octicon { 151 | display: inline-block; 152 | vertical-align: text-top; 153 | fill: currentColor; 154 | } 155 | #help-modal table { 156 | clear: both; 157 | } 158 | #help-modal img { 159 | margin-left: 20px; 160 | margin-bottom: 30px; 161 | } 162 | #help-modal h2 { 163 | padding-bottom: 0.3em; 164 | font-size: 1.5em; 165 | border-bottom: 1px solid #eaecef; 166 | 167 | margin-top: 24px; 168 | margin-bottom: 16px; 169 | font-weight: 600; 170 | line-height: 1.25; 171 | } 172 | #help-modal h4 { 173 | font-size: 1em; 174 | 175 | margin-top: 24px; 176 | margin-bottom: 16px; 177 | font-weight: 600; 178 | line-height: 1.25; 179 | } 180 | #help-modal .modal-lg { 181 | max-width: 700px; 182 | } 183 | #help-modal a { 184 | color: #0366d6; 185 | } 186 | #help-modal code { 187 | padding: 0.2em 0.4em; 188 | margin: 0; 189 | font-size: 85%; 190 | background-color: rgba(27,31,35,0.05); 191 | border-radius: 3px; 192 | color: #24292e; 193 | } 194 | ` 195 | 196 | func (v *Page) Render() vecty.ComponentOrHTML { 197 | githubBannerDisplay := "" 198 | if v.app.Compile.Compiled() { 199 | githubBannerDisplay = "none" 200 | } 201 | forkMe := "https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" 202 | if config.LOCAL { 203 | forkMe = "/_local/forkme_right_gray_6d6d6d.png" 204 | } 205 | return elem.Body( 206 | elem.Div( 207 | vecty.Markup( 208 | vecty.Class("container-fluid", "p-0", "split", "split-horizontal"), 209 | ), 210 | v.renderLeft(), 211 | v.renderRight(), 212 | ), 213 | NewAddFileModal(v.app), 214 | NewDeleteFileModal(v.app), 215 | NewAddPackageModal(v.app), 216 | NewRemovePackageModal(v.app), 217 | NewDeployDoneModal(v.app), 218 | NewLoadPackageModal(v.app), 219 | NewClashWarningModal(v.app), 220 | NewBuildTagsModal(v.app), 221 | NewHelpModal(v.app), 222 | elem.Anchor( 223 | vecty.Markup( 224 | prop.Href("https://github.com/dave/play"), 225 | vecty.Style("display", githubBannerDisplay), 226 | vecty.Property("target", "_blank"), 227 | ), 228 | elem.Image( 229 | vecty.Markup( 230 | vecty.Style("position", "absolute"), 231 | vecty.Style("top", "0"), 232 | vecty.Style("right", "0"), 233 | vecty.Style("border", "0"), 234 | prop.Src(forkMe), 235 | vecty.Property("alt", "Fork me on GitHub"), 236 | ), 237 | ), 238 | ), 239 | ) 240 | } 241 | 242 | func (v *Page) renderLeft() *vecty.HTML { 243 | 244 | v.editor = NewEditor(v.app) 245 | 246 | emptyDisplay := "none" 247 | addFileDisplay := "none" 248 | addPackageDisplay := "none" 249 | loadingDisplay := "none" 250 | if !v.app.Editor.Loaded() { 251 | emptyDisplay = "" 252 | loadingDisplay = "" 253 | } else if len(v.app.Source.Packages()) == 0 { 254 | emptyDisplay = "" 255 | addPackageDisplay = "" 256 | } else if len(v.app.Source.Files(v.app.Editor.CurrentPackage())) == 0 { 257 | emptyDisplay = "" 258 | addFileDisplay = "" 259 | } 260 | 261 | return elem.Div( 262 | vecty.Markup( 263 | prop.ID("left"), 264 | vecty.Class("split"), 265 | ), 266 | NewMenu(v.app), 267 | v.editor, 268 | elem.Div( 269 | vecty.Markup( 270 | vecty.Class("empty-panel"), 271 | vecty.Style("display", emptyDisplay), 272 | ), 273 | elem.Span( 274 | vecty.Markup( 275 | vecty.Style("display", loadingDisplay), 276 | ), 277 | vecty.Text("Loading..."), 278 | ), 279 | elem.Button( 280 | vecty.Markup( 281 | vecty.Property("type", "button"), 282 | vecty.Class("btn", "btn-primary"), 283 | event.Click(func(e *vecty.Event) { 284 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddFileModal}) 285 | }).PreventDefault(), 286 | vecty.Style("display", addFileDisplay), 287 | ), 288 | vecty.Text("Add file"), 289 | ), 290 | elem.Button( 291 | vecty.Markup( 292 | vecty.Property("type", "button"), 293 | vecty.Class("btn", "btn-primary"), 294 | event.Click(func(e *vecty.Event) { 295 | v.app.Dispatch(&actions.ModalOpen{Modal: models.AddPackageModal}) 296 | }).PreventDefault(), 297 | vecty.Style("display", addPackageDisplay), 298 | ), 299 | vecty.Text("Add package"), 300 | ), 301 | ), 302 | ) 303 | } 304 | 305 | func (v *Page) renderRight() *vecty.HTML { 306 | consoleDisplay := "" 307 | if !v.app.Page.Console() { 308 | consoleDisplay = "none" 309 | } 310 | return elem.Div( 311 | vecty.Markup( 312 | prop.ID("right"), 313 | vecty.Class("split", "split-vertical"), 314 | ), 315 | elem.Div( 316 | vecty.Markup( 317 | prop.ID("iframe-holder"), 318 | ), 319 | ), 320 | elem.Div( 321 | vecty.Markup( 322 | prop.ID("console-holder"), 323 | vecty.Style("display", consoleDisplay), 324 | ), 325 | elem.Preformatted( 326 | vecty.Markup( 327 | prop.ID("console"), 328 | ), 329 | ), 330 | ), 331 | ) 332 | } 333 | -------------------------------------------------------------------------------- /views/remove-package.go: -------------------------------------------------------------------------------- 1 | package views 2 | 3 | import ( 4 | "github.com/dave/play/actions" 5 | "github.com/dave/play/models" 6 | "github.com/dave/play/stores" 7 | "github.com/gopherjs/vecty" 8 | "github.com/gopherjs/vecty/elem" 9 | "github.com/gopherjs/vecty/prop" 10 | ) 11 | 12 | type RemovePackageModal struct { 13 | *Modal 14 | sel *vecty.HTML 15 | } 16 | 17 | func NewRemovePackageModal(app *stores.App) *RemovePackageModal { 18 | v := &RemovePackageModal{} 19 | v.Modal = &Modal{ 20 | app: app, 21 | id: models.RemovePackageModal, 22 | title: "Remove package", 23 | action: v.action, 24 | } 25 | return v 26 | } 27 | 28 | func (v *RemovePackageModal) Render() vecty.ComponentOrHTML { 29 | items := []vecty.MarkupOrChild{ 30 | vecty.Markup( 31 | vecty.Class("form-control"), 32 | prop.ID("remove-package-select"), 33 | ), 34 | } 35 | for _, path := range v.app.Source.Packages() { 36 | items = append(items, 37 | elem.Option( 38 | vecty.Markup( 39 | prop.Value(path), 40 | vecty.Property("selected", v.app.Editor.CurrentPackage() == path), 41 | ), 42 | vecty.Text(path), 43 | ), 44 | ) 45 | } 46 | v.sel = elem.Select(items...) 47 | 48 | return v.Body( 49 | elem.Form( 50 | elem.Div( 51 | vecty.Markup( 52 | vecty.Class("form-group"), 53 | ), 54 | elem.Label( 55 | vecty.Markup( 56 | vecty.Property("for", "remove-package-select"), 57 | vecty.Class("col-form-label"), 58 | ), 59 | vecty.Text("Package path"), 60 | ), 61 | v.sel, 62 | ), 63 | ), 64 | ).Build() 65 | } 66 | 67 | func (v *RemovePackageModal) action(*vecty.Event) { 68 | n := v.sel.Node() 69 | i := n.Get("selectedIndex").Int() 70 | value := n.Get("options").Index(i).Get("value").String() 71 | v.app.Dispatch(&actions.ModalClose{Modal: models.RemovePackageModal}) 72 | v.app.Dispatch(&actions.RemovePackage{Path: value}) 73 | } 74 | --------------------------------------------------------------------------------