gromer

#golang#htmx#ssr

git clone https://git.pyrossh.dev/gromer

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


fdf8772 — pyros2097 2020-12-05T12:21:38+05:30
improve example

Generated files not shown: go.sum

app_nowasm.go CHANGED
@@ -2,10 +2,72 @@
2
2
 
3
3
  package app
4
4
 
5
+ import (
6
+ "bytes"
7
+ "fmt"
8
+ "net/http"
9
+ "os"
10
+ "path/filepath"
11
+ "strconv"
12
+ "strings"
13
+
14
+ "github.com/akrylysov/algnhsa"
15
+ "github.com/markbates/pkger"
16
+ )
17
+
18
+ func Run(isAwsLambda bool, routes map[string]RenderFunc) {
19
+ wd, err := os.Getwd()
20
+ if err != nil {
21
+ fmt.Printf("could not get wd")
22
+ return
23
+ }
24
+ assetsFileServer := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
25
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
26
+ println("route: " + r.URL.Path)
27
+ renderFunc := MatchRoute(routes, r.URL.Path)
28
+ if strings.Contains(r.URL.Path, "/assets") {
29
+ r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
30
+ assetsFileServer.ServeHTTP(w, r)
31
+ return
32
+ }
33
+ page := createPage("wapp-example", renderFunc(NewRenderContext()))
34
+ w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
35
+ w.Header().Set("Content-Type", "text/html")
36
+ w.WriteHeader(http.StatusOK)
37
+ w.Write(page.Bytes())
38
+ })
39
+ if !isAwsLambda {
40
+ println("Serving on HTTP port: 1234")
41
+ http.ListenAndServe(":1234", nil)
42
+ } else {
43
+ println("algnhsa serving default mux")
44
+ algnhsa.ListenAndServe(http.DefaultServeMux, nil)
45
+ }
46
+ }
47
+
5
48
  func Reload() {
6
49
  panic("wasm required")
7
50
  }
8
51
 
9
- func Run(r RenderFunc) {
10
- panic("wasm required")
52
+ func createPage(title string, ui UI) *bytes.Buffer {
53
+ page := bytes.NewBuffer(nil)
54
+ page.WriteString("<!DOCTYPE html>\n")
55
+ elems := FilterUIElems(ui)
56
+ Html(
57
+ Head(
58
+ Title(title),
59
+ Meta("author", "pyros2097"),
60
+ Meta("description", "Description"),
61
+ Meta("keywords", ""),
62
+ Meta("theme-color", ""),
63
+ Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
64
+ Link("icon", "/assets/icon.png"),
65
+ Link("apple-touch-icon", "/assets/icon.png"),
66
+ Link("stylesheet", "/assets/styles.css"),
67
+ Link("manifest", "manifest"),
68
+ Script(`const enosys = () => { const a = new Error("not implemented"); return a.code = "ENOSYS", a }; let outputBuf = ""; window.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, writeSync(a, b) { outputBuf += decoder.decode(b); const c = outputBuf.lastIndexOf("\n"); return -1 != c && (console.log(outputBuf.substr(0, c)), outputBuf = outputBuf.substr(c + 1)), b.length }, write(a, b, c, d, e, f) { if (0 !== c || d !== b.length || null !== e) return void f(enosys()); const g = this.writeSync(a, b); f(null, g) } }; const encoder = new TextEncoder("utf-8"), decoder = new TextDecoder("utf-8"); class Go { constructor() { this.argv = ["js"], this.env = {}, this.exit = a => { 0 !== a && console.warn("exit code:", a) }, this._exitPromise = new Promise(a => { this._resolveExitPromise = a }), this._pendingEvent = null, this._scheduledTimeouts = new Map, this._nextCallbackTimeoutID = 1; const a = (a, b) => { this.mem.setUint32(a + 0, b, !0), this.mem.setUint32(a + 4, Math.floor(b / 4294967296), !0) }, b = a => { const b = this.mem.getUint32(a + 0, !0), c = this.mem.getInt32(a + 4, !0); return b + 4294967296 * c }, c = a => { const b = this.mem.getFloat64(a, !0); if (0 !== b) { if (!isNaN(b)) return b; const c = this.mem.getUint32(a, !0); return this._values[c] } }, d = (a, b) => { const c = 2146959360; if ("number" == typeof b) return isNaN(b) ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 0, !0)) : 0 === b ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 1, !0)) : void this.mem.setFloat64(a, b, !0); switch (b) { case void 0: return void this.mem.setFloat64(a, 0, !0); case null: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 2, !0); case !0: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 3, !0); case !1: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 4, !0); }let d = this._ids.get(b); d === void 0 && (d = this._idPool.pop(), d === void 0 && (d = this._values.length), this._values[d] = b, this._goRefCounts[d] = 0, this._ids.set(b, d)), this._goRefCounts[d]++; let e = 1; switch (typeof b) { case "string": e = 2; break; case "symbol": e = 3; break; case "function": e = 4; }this.mem.setUint32(a + 4, 2146959360 | e, !0), this.mem.setUint32(a, d, !0) }, e = a => { const c = b(a + 0), d = b(a + 8); return new Uint8Array(this._inst.exports.mem.buffer, c, d) }, f = d => { const e = b(d + 0), f = b(d + 8), g = Array(f); for (let a = 0; a < f; a++)g[a] = c(e + 8 * a); return g }, g = a => { const c = b(a + 0), d = b(a + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, c, d)) }, h = Date.now() - performance.now(); this.importObject = { go: { "runtime.wasmExit": a => { const b = this.mem.getInt32(a + 8, !0); this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(b) }, "runtime.wasmWrite": a => { const c = b(a + 8), d = b(a + 16), e = this.mem.getInt32(a + 24, !0); fs.writeSync(c, new Uint8Array(this._inst.exports.mem.buffer, d, e)) }, "runtime.resetMemoryDataView": () => { this.mem = new DataView(this._inst.exports.mem.buffer) }, "runtime.nanotime1": b => { a(b + 8, 1e6 * (h + performance.now())) }, "runtime.walltime1": b => { const c = new Date().getTime(); a(b + 8, c / 1e3), this.mem.setInt32(b + 16, 1e6 * (c % 1e3), !0) }, "runtime.scheduleTimeoutEvent": a => { const c = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(c, setTimeout(() => { for (this._resume(); this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume() }, b(a + 8) + 1)), this.mem.setInt32(a + 16, c, !0) }, "runtime.clearTimeoutEvent": a => { const b = this.mem.getInt32(a + 8, !0); clearTimeout(this._scheduledTimeouts.get(b)), this._scheduledTimeouts.delete(b) }, "runtime.getRandomData": a => { crypto.getRandomValues(e(a + 8)) }, "syscall/js.finalizeRef": a => { const b = this.mem.getUint32(a + 8, !0); if (this._goRefCounts[b]--, 0 === this._goRefCounts[b]) { const a = this._values[b]; this._values[b] = null, this._ids.delete(a), this._idPool.push(b) } }, "syscall/js.stringVal": a => { d(a + 24, g(a + 8)) }, "syscall/js.valueGet": a => { const b = Reflect.get(c(a + 8), g(a + 16)); a = this._inst.exports.getsp(), d(a + 32, b) }, "syscall/js.valueSet": a => { Reflect.set(c(a + 8), g(a + 16), c(a + 32)) }, "syscall/js.valueDelete": a => { Reflect.deleteProperty(c(a + 8), g(a + 16)) }, "syscall/js.valueIndex": a => { d(a + 24, Reflect.get(c(a + 8), b(a + 16))) }, "syscall/js.valueSetIndex": a => { Reflect.set(c(a + 8), b(a + 16), c(a + 24)) }, "syscall/js.valueCall": a => { try { const b = c(a + 8), e = Reflect.get(b, g(a + 16)), h = f(a + 32), i = Reflect.apply(e, b, h); a = this._inst.exports.getsp(), d(a + 56, i), this.mem.setUint8(a + 64, 1) } catch (b) { d(a + 56, b), this.mem.setUint8(a + 64, 0) } }, "syscall/js.valueInvoke": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.apply(b, void 0, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueNew": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.construct(b, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueLength": b => { a(b + 16, parseInt(c(b + 8).length)) }, "syscall/js.valuePrepareString": b => { const e = encoder.encode(c(b + 8) + ""); d(b + 16, e), a(b + 24, e.length) }, "syscall/js.valueLoadString": a => { const b = c(a + 8); e(a + 16).set(b) }, "syscall/js.valueInstanceOf": a => { this.mem.setUint8(a + 24, c(a + 8) instanceof c(a + 16)) }, "syscall/js.copyBytesToGo": b => { const d = e(b + 8), f = c(b + 32); if (!(f instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, "syscall/js.copyBytesToJS": b => { const d = c(b + 8), f = e(b + 16); if (!(d instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, debug: a => { console.log(a) } } } } async run(a) { this._inst = a, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [NaN, 0, null, !0, !1, window, this], this._goRefCounts = [], this._ids = new Map, this._idPool = [], this.exited = !1; let b = 4096; const c = a => { const c = b, d = encoder.encode(a + "\0"); return new Uint8Array(this.mem.buffer, b, d.length).set(d), b += d.length, 0 != b % 8 && (b += 8 - b % 8), c }, d = this.argv.length, e = []; this.argv.forEach(a => { e.push(c(a)) }), e.push(0); const f = Object.keys(this.env).sort(); f.forEach(a => { e.push(c(a + "=" + this.env[a])) }), e.push(0); const g = b; e.forEach(a => { this.mem.setUint32(b, a, !0), this.mem.setUint32(b + 4, 0, !0), b += 8 }), this._inst.exports.run(d, g), this.exited && this._resolveExitPromise(), await this._exitPromise } _resume() { if (this.exited) throw new Error("Go program has already exited"); this._inst.exports.resume(), this.exited && this._resolveExitPromise() } _makeFuncWrapper(a) { const b = this; return function () { const c = { id: a, this: this, args: arguments }; return b._pendingEvent = c, b._resume(), c.result } } } const go = new Go; WebAssembly.instantiateStreaming(fetch("/assets/main.wasm"), go.importObject).then(a => go.run(a.instance)).catch(a => console.error("could not load wasm", a));`),
69
+ ),
70
+ Body(elems[0]),
71
+ ).Html(page)
72
+ return page
11
73
  }
app_wasm.go CHANGED
@@ -11,7 +11,8 @@ var (
11
11
  rootPrefix string
12
12
  )
13
13
 
14
- func Run(render RenderFunc) {
14
+ func Run(isAwsLambda bool, routes map[string]RenderFunc) {
15
+ renderFunc := MatchRoute(routes, js.Window.URL().Path)
15
16
  defer func() {
16
17
  err := recover()
17
18
  // show alert
@@ -20,10 +21,10 @@ func Run(render RenderFunc) {
20
21
 
21
22
  initBody()
22
23
  initContent()
23
- if err := body.replaceChildAt(0, render); err != nil {
24
+ if err := body.replaceChildAt(0, renderFunc); err != nil {
24
25
  panic("replacing content failed")
25
26
  }
26
- content = render
27
+ content = renderFunc
27
28
 
28
29
  for {
29
30
  select {
cli/cli.go CHANGED
@@ -1,353 +1,362 @@
1
1
  package cli
2
2
 
3
- import (
4
- "bytes"
5
- "fmt"
6
- "io/ioutil"
7
- "log"
8
- "net/http"
9
- "os"
10
- "os/exec"
11
- "path/filepath"
12
- "plugin"
13
- "strconv"
14
- "strings"
15
- "text/template"
16
- "time"
17
-
18
- "github.com/markbates/pkger"
19
- app "github.com/pyros2097/wapp"
20
- "gopkg.in/fsnotify.v1"
21
- )
22
-
23
- const wasmExecTemplate = `const enosys=()=>{const a=new Error("not implemented");return a.code="ENOSYS",a};let outputBuf="";window.fs={constants:{O_WRONLY:-1,O_RDWR:-1,O_CREAT:-1,O_TRUNC:-1,O_APPEND:-1,O_EXCL:-1},writeSync(a,b){outputBuf+=decoder.decode(b);const c=outputBuf.lastIndexOf("\n");return-1!=c&&(console.log(outputBuf.substr(0,c)),outputBuf=outputBuf.substr(c+1)),b.length},write(a,b,c,d,e,f){if(0!==c||d!==b.length||null!==e)return void f(enosys());const g=this.writeSync(a,b);f(null,g)}};const encoder=new TextEncoder("utf-8"),decoder=new TextDecoder("utf-8");class Go{constructor(){this.argv=["js"],this.env={},this.exit=a=>{0!==a&&console.warn("exit code:",a)},this._exitPromise=new Promise(a=>{this._resolveExitPromise=a}),this._pendingEvent=null,this._scheduledTimeouts=new Map,this._nextCallbackTimeoutID=1;const a=(a,b)=>{this.mem.setUint32(a+0,b,!0),this.mem.setUint32(a+4,Math.floor(b/4294967296),!0)},b=a=>{const b=this.mem.getUint32(a+0,!0),c=this.mem.getInt32(a+4,!0);return b+4294967296*c},c=a=>{const b=this.mem.getFloat64(a,!0);if(0!==b){if(!isNaN(b))return b;const c=this.mem.getUint32(a,!0);return this._values[c]}},d=(a,b)=>{const c=2146959360;if("number"==typeof b)return isNaN(b)?(this.mem.setUint32(a+4,2146959360,!0),void this.mem.setUint32(a,0,!0)):0===b?(this.mem.setUint32(a+4,2146959360,!0),void this.mem.setUint32(a,1,!0)):void this.mem.setFloat64(a,b,!0);switch(b){case void 0:return void this.mem.setFloat64(a,0,!0);case null:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,2,!0);case!0:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,3,!0);case!1:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,4,!0);}let d=this._ids.get(b);d===void 0&&(d=this._idPool.pop(),d===void 0&&(d=this._values.length),this._values[d]=b,this._goRefCounts[d]=0,this._ids.set(b,d)),this._goRefCounts[d]++;let e=1;switch(typeof b){case"string":e=2;break;case"symbol":e=3;break;case"function":e=4;}this.mem.setUint32(a+4,2146959360|e,!0),this.mem.setUint32(a,d,!0)},e=a=>{const c=b(a+0),d=b(a+8);return new Uint8Array(this._inst.exports.mem.buffer,c,d)},f=d=>{const e=b(d+0),f=b(d+8),g=Array(f);for(let a=0;a<f;a++)g[a]=c(e+8*a);return g},g=a=>{const c=b(a+0),d=b(a+8);return decoder.decode(new DataView(this._inst.exports.mem.buffer,c,d))},h=Date.now()-performance.now();this.importObject={go:{"runtime.wasmExit":a=>{const b=this.mem.getInt32(a+8,!0);this.exited=!0,delete this._inst,delete this._values,delete this._goRefCounts,delete this._ids,delete this._idPool,this.exit(b)},"runtime.wasmWrite":a=>{const c=b(a+8),d=b(a+16),e=this.mem.getInt32(a+24,!0);fs.writeSync(c,new Uint8Array(this._inst.exports.mem.buffer,d,e))},"runtime.resetMemoryDataView":()=>{this.mem=new DataView(this._inst.exports.mem.buffer)},"runtime.nanotime1":b=>{a(b+8,1e6*(h+performance.now()))},"runtime.walltime1":b=>{const c=new Date().getTime();a(b+8,c/1e3),this.mem.setInt32(b+16,1e6*(c%1e3),!0)},"runtime.scheduleTimeoutEvent":a=>{const c=this._nextCallbackTimeoutID;this._nextCallbackTimeoutID++,this._scheduledTimeouts.set(c,setTimeout(()=>{for(this._resume();this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"),this._resume()},b(a+8)+1)),this.mem.setInt32(a+16,c,!0)},"runtime.clearTimeoutEvent":a=>{const b=this.mem.getInt32(a+8,!0);clearTimeout(this._scheduledTimeouts.get(b)),this._scheduledTimeouts.delete(b)},"runtime.getRandomData":a=>{crypto.getRandomValues(e(a+8))},"syscall/js.finalizeRef":a=>{const b=this.mem.getUint32(a+8,!0);if(this._goRefCounts[b]--,0===this._goRefCounts[b]){const a=this._values[b];this._values[b]=null,this._ids.delete(a),this._idPool.push(b)}},"syscall/js.stringVal":a=>{d(a+24,g(a+8))},"syscall/js.valueGet":a=>{const b=Reflect.get(c(a+8),g(a+16));a=this._inst.exports.getsp(),d(a+32,b)},"syscall/js.valueSet":a=>{Reflect.set(c(a+8),g(a+16),c(a+32))},"syscall/js.valueDelete":a=>{Reflect.deleteProperty(c(a+8),g(a+16))},"syscall/js.valueIndex":a=>{d(a+24,Reflect.get(c(a+8),b(a+16)))},"syscall/js.valueSetIndex":a=>{Reflect.set(c(a+8),b(a+16),c(a+24))},"syscall/js.valueCall":a=>{try{const b=c(a+8),e=Reflect.get(b,g(a+16)),h=f(a+32),i=Reflect.apply(e,b,h);a=this._inst.exports.getsp(),d(a+56,i),this.mem.setUint8(a+64,1)}catch(b){d(a+56,b),this.mem.setUint8(a+64,0)}},"syscall/js.valueInvoke":a=>{try{const b=c(a+8),e=f(a+16),g=Reflect.apply(b,void 0,e);a=this._inst.exports.getsp(),d(a+40,g),this.mem.setUint8(a+48,1)}catch(b){d(a+40,b),this.mem.setUint8(a+48,0)}},"syscall/js.valueNew":a=>{try{const b=c(a+8),e=f(a+16),g=Reflect.construct(b,e);a=this._inst.exports.getsp(),d(a+40,g),this.mem.setUint8(a+48,1)}catch(b){d(a+40,b),this.mem.setUint8(a+48,0)}},"syscall/js.valueLength":b=>{a(b+16,parseInt(c(b+8).length))},"syscall/js.valuePrepareString":b=>{const e=encoder.encode(c(b+8)+"");d(b+16,e),a(b+24,e.length)},"syscall/js.valueLoadString":a=>{const b=c(a+8);e(a+16).set(b)},"syscall/js.valueInstanceOf":a=>{this.mem.setUint8(a+24,c(a+8)instanceof c(a+16))},"syscall/js.copyBytesToGo":b=>{const d=e(b+8),f=c(b+32);if(!(f instanceof Uint8Array))return void this.mem.setUint8(b+48,0);const g=f.subarray(0,d.length);d.set(g),a(b+40,g.length),this.mem.setUint8(b+48,1)},"syscall/js.copyBytesToJS":b=>{const d=c(b+8),f=e(b+16);if(!(d instanceof Uint8Array))return void this.mem.setUint8(b+48,0);const g=f.subarray(0,d.length);d.set(g),a(b+40,g.length),this.mem.setUint8(b+48,1)},debug:a=>{console.log(a)}}}}async run(a){this._inst=a,this.mem=new DataView(this._inst.exports.mem.buffer),this._values=[NaN,0,null,!0,!1,window,this],this._goRefCounts=[],this._ids=new Map,this._idPool=[],this.exited=!1;let b=4096;const c=a=>{const c=b,d=encoder.encode(a+"\0");return new Uint8Array(this.mem.buffer,b,d.length).set(d),b+=d.length,0!=b%8&&(b+=8-b%8),c},d=this.argv.length,e=[];this.argv.forEach(a=>{e.push(c(a))}),e.push(0);const f=Object.keys(this.env).sort();f.forEach(a=>{e.push(c(a+"="+this.env[a]))}),e.push(0);const g=b;e.forEach(a=>{this.mem.setUint32(b,a,!0),this.mem.setUint32(b+4,0,!0),b+=8}),this._inst.exports.run(d,g),this.exited&&this._resolveExitPromise(),await this._exitPromise}_resume(){if(this.exited)throw new Error("Go program has already exited");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(a){const b=this;return function(){const c={id:a,this:this,args:arguments};return b._pendingEvent=c,b._resume(),c.result}}}const go=new Go;WebAssembly.instantiateStreaming(fetch("__path__"),go.importObject).then(a=>go.run(a.instance)).catch(a=>console.error("could not load wasm",a));`
24
-
25
- func wasmExecJs(path string) string {
26
- return strings.Replace(wasmExecTemplate, "__path__", path, 1)
27
- }
28
-
29
- var watchDelta = 1000 * time.Millisecond
30
-
31
- type Watcher struct {
32
- rootdir string
33
- watcher *fsnotify.Watcher
34
- watchVendor bool
35
- update chan string
36
- }
37
-
38
- // MustRegisterWatcher creates a new Watcher and starts listening to
39
- // given folders
40
- func MustRegisterWatcher() *Watcher {
41
- w := &Watcher{
42
- update: make(chan string),
43
- watchVendor: false,
44
- }
45
- var err error
46
- w.watcher, err = fsnotify.NewWatcher()
47
- if err != nil {
48
- log.Fatalf("Could not register watcher: %s", err)
49
- }
50
- w.watchFolders()
51
- return w
52
- }
53
-
54
- // Watch listens file updates, and sends signal to
55
- // update channel when .go and .tmpl files are updated
56
- func (w *Watcher) Watch() {
57
- eventSent := false
58
- for {
59
- select {
60
- case event := <-w.watcher.Events:
61
- // discard chmod events
62
- if event.Op&fsnotify.Chmod != fsnotify.Chmod {
63
- // test files do not need a rebuild
64
- if isTestFile(event.Name) {
65
- continue
66
- }
67
- if !isWatchedFileType(event.Name) {
68
- continue
69
- }
70
- if eventSent {
71
- continue
72
- }
73
- eventSent = true
74
- // prevent consequent builds
75
- go func() {
76
- w.update <- event.Name
77
- time.Sleep(watchDelta)
78
- eventSent = false
79
- }()
80
-
81
- }
82
- case err := <-w.watcher.Errors:
83
- if err != nil {
84
- log.Fatalf("Watcher error: %s", err)
85
- }
86
- return
87
- }
88
- }
89
- }
90
-
91
- func isTestFile(fileName string) bool {
92
- return strings.HasSuffix(filepath.Base(fileName), "_test.go")
93
- }
94
-
95
- func isWatchedFileType(fileName string) bool {
96
- ext := filepath.Ext(fileName)
97
-
98
- return ext == ".go"
99
- }
100
-
101
- // Close closes the fsnotify watcher channel
102
- func (w *Watcher) Close() {
103
- w.watcher.Close()
104
- close(w.update)
105
- }
106
-
107
- // watchFolders recursively adds folders that will be watched against the changes,
108
- // starting from the working directory
109
- func (w *Watcher) watchFolders() {
110
- wd, err := os.Getwd()
111
-
112
- if err != nil {
113
- log.Fatalf("Could not get root working directory: %s", err)
114
- }
115
-
116
- filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
117
- // skip files
118
- if info == nil {
119
- log.Fatalf("wrong watcher package: %s", path)
120
- }
121
-
122
- if !info.IsDir() {
123
- return nil
124
- }
125
-
126
- if !w.watchVendor {
127
- // skip vendor directory
128
- vendor := fmt.Sprintf("%s/vendor", wd)
129
- if strings.HasPrefix(path, vendor) {
130
- return filepath.SkipDir
131
- }
132
- }
133
-
134
- // skip hidden folders
135
- if len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") {
136
- return filepath.SkipDir
137
- }
138
-
139
- w.addFolder(path)
140
-
141
- return err
142
- })
143
- }
144
-
145
- // addFolder adds given folder name to the watched folders, and starts
146
- // watching it for further changes
147
- func (w *Watcher) addFolder(name string) {
148
- println("watch: " + name)
149
- if err := w.watcher.Add(name); err != nil {
150
- log.Fatalf("Could not watch folder: %s", err)
151
- }
152
- }
153
-
154
- type RouteInfo struct {
155
- Path string
156
- FuncName string
157
- RenderFunc func(*app.RenderContext) app.UI
158
- }
159
-
160
- var routesMap = map[string]RouteInfo{}
161
-
162
- func getSOPath(p string) string {
163
- return "build/" + filepath.Base(filepath.Dir(p)) + ".so"
164
- }
165
-
166
- func getWasmPath(p string) string {
167
- return "build/" + filepath.Base(filepath.Dir(p)) + ".wasm"
168
- }
169
-
170
- func buildSo(path string) (string, error) {
171
- soPath := getSOPath(path)
172
- out, err := exec.Command("go", "build", "-buildmode=plugin", "-o", soPath, path).CombinedOutput()
173
- if err != nil {
174
- println(string(out))
175
- println(err.Error())
176
- return "", err
177
- }
178
- fmt.Printf("wrote: %s\n", soPath)
179
- return soPath, nil
180
- }
181
-
182
- func buildWasm(path string) (string, error) {
183
- wasmPath := getWasmPath(path)
184
- cmd := exec.Command("go", "build", "-o", wasmPath, path)
185
- cmd.Env = os.Environ()
186
- cmd.Env = append(cmd.Env, "GOOS=js", "GOARCH=wasm")
187
- out, err := cmd.CombinedOutput()
188
- if err != nil {
189
- println(string(out))
190
- println(err.Error())
191
- return "", err
192
- }
193
- fmt.Printf("wrote: %s\n", wasmPath)
194
- return wasmPath, nil
195
- }
196
-
197
- func getRoute(basePath, p string) (string, string) {
198
- clean := strings.Replace(strings.Replace(p, basePath, "", 1), ".go", "", -1)
199
- return strings.Replace(clean, "index", "", -1), strings.Title(strings.Replace(clean, "/", "", -1))
200
- }
201
-
202
- func writeMainFile(basePath string) {
203
- tpl, err := template.New("writeMain").Parse(`// GENERATED FILE DO NOT EDIT
204
- package main
205
-
206
- import (
207
- . "github.com/pyros2097/wapp"
208
- "github.com/pyros2097/wapp/js"
209
- )
210
-
211
- func main() {
212
- {{range $key, $element := .}}
213
- if js.Window.URL().Path == "{{.Path}}" {
214
- Run({{.FuncName}})
215
- }
216
- {{end}}
217
- }
218
- `)
219
- if err != nil {
220
- panic(err)
221
- }
222
- buf := bytes.NewBuffer(nil)
223
- err = tpl.Execute(buf, routesMap)
224
- if err != nil {
225
- panic(err)
226
- }
227
- err = ioutil.WriteFile("pages/main.go", buf.Bytes(), 0644)
228
- if err != nil {
229
- panic(err)
230
- }
231
- }
232
-
233
- func loadRoutes(p *plugin.Plugin, basePath string, dry bool) {
234
- err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
235
- if err != nil {
236
- fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
237
- return err
238
- }
239
- if !info.IsDir() {
240
- routePath, routeFunc := getRoute(basePath, path)
241
- if routePath == "/main" {
242
- return nil
243
- }
244
- routesMap[routePath] = RouteInfo{
245
- Path: routePath,
246
- FuncName: routeFunc,
247
- }
248
- if dry {
249
- // println("Dry")
250
- return nil
251
- }
252
- fmt.Printf("route: %s routeFunc: %s\n", routePath, routeFunc)
253
- renderFn, err := p.Lookup(routeFunc)
254
- if err != nil {
255
- return err
256
- }
257
- routesMap[routePath] = RouteInfo{
258
- Path: routePath,
259
- FuncName: routeFunc,
260
- RenderFunc: renderFn.(func(*app.RenderContext) app.UI),
261
- }
262
- // println(createPage(routesMap[routePath](app.NewRenderContext())).String())
263
- }
264
- return nil
265
- })
266
- if err != nil {
267
- fmt.Printf("error walking the path %q: %v\n", basePath, err)
268
- panic(err)
269
- }
270
- }
271
-
272
- func createPage(ui app.UI, wasmPath string) *bytes.Buffer {
273
- page := bytes.NewBuffer(nil)
274
- page.WriteString("<!DOCTYPE html>\n")
275
- elems := app.FilterUIElems(ui)
276
- app.Html(
277
- app.Head(
278
- app.Title("Title"),
279
- app.Meta("author", "pyros2097"),
280
- app.Meta("description", "Description"),
281
- app.Meta("keywords", ""),
282
- app.Meta("theme-color", ""),
283
- app.Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
284
- app.Link("icon", "/assets/icon.png"),
285
- app.Link("apple-touch-icon", "/assets/icon.png"),
286
- app.Link("stylesheet", "/assets/styles.css"),
287
- app.Link("manifest", "manifest"),
288
- app.Script(wasmExecJs(wasmPath)),
289
- ),
290
- app.Body(elems[0]),
291
- ).Html(page)
292
- return page
293
- }
294
-
295
- func serve(wd string) {
296
-
297
- assetsFileServer := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
298
- buildFileServer := http.FileServer(pkger.Dir(filepath.Join(wd, "build")))
299
-
300
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
301
- if routeInfo, ok := routesMap[r.URL.Path]; ok {
302
- wasmPath := "/build/" + filepath.Base(wd) + ".wasm"
303
- page := createPage(routeInfo.RenderFunc(app.NewRenderContext()), wasmPath)
304
- w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
305
- w.Header().Set("Content-Type", "text/html")
306
- w.WriteHeader(http.StatusOK)
307
- w.Write(page.Bytes())
308
- } else if strings.Contains(r.URL.Path, "/build") {
309
- r.URL.Path = strings.Replace(r.URL.Path, "/build", "", 1)
310
- buildFileServer.ServeHTTP(w, r)
311
- } else {
312
- r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
313
- assetsFileServer.ServeHTTP(w, r)
314
- }
315
- })
316
- log.Printf("Serving on HTTP port: 1234")
317
- log.Fatal(http.ListenAndServe(":1234", nil))
318
- }
319
-
320
- func buildAll(basePath string) {
321
- loadRoutes(nil, basePath, true)
322
- writeMainFile(basePath)
323
- buildWasm(basePath)
324
- soPath, err := buildSo(basePath)
325
- if err != nil {
326
- println("could not build")
327
- panic(err)
328
- }
329
- p, err := plugin.Open(soPath)
330
- if err != nil {
331
- println("could not load so plugin")
332
- panic(err)
333
- }
334
- // fmt.Printf("%+v\n", p)
335
- loadRoutes(p, basePath, false)
336
- }
337
-
338
- func Watch() {
339
- wd, err := os.Getwd()
340
- if err != nil {
341
- fmt.Printf("could not get wd")
342
- return
343
- }
344
- basePath := filepath.Join(wd, "pages")
345
- buildAll(basePath)
346
- watcher := MustRegisterWatcher()
347
- go watcher.Watch()
348
- go serve(wd)
349
- for file := range watcher.update {
350
- println("changed: " + file)
351
- buildAll(basePath)
352
- }
353
- }
3
+ // import (
4
+ // "bytes"
5
+ // "fmt"
6
+ // "io/ioutil"
7
+ // "log"
8
+ // "net/http"
9
+ // "os"
10
+ // "os/exec"
11
+ // "path/filepath"
12
+ // "plugin"
13
+ // "strconv"
14
+ // "strings"
15
+ // "text/template"
16
+ // "time"
17
+
18
+ // "github.com/akrylysov/algnhsa"
19
+ // "github.com/markbates/pkger"
20
+ // app "github.com/pyros2097/wapp"
21
+ // "gopkg.in/fsnotify.v1"
22
+ // )
23
+
24
+ // const wasmExecTemplate = `const enosys=()=>{const a=new Error("not implemented");return a.code="ENOSYS",a};let outputBuf="";window.fs={constants:{O_WRONLY:-1,O_RDWR:-1,O_CREAT:-1,O_TRUNC:-1,O_APPEND:-1,O_EXCL:-1},writeSync(a,b){outputBuf+=decoder.decode(b);const c=outputBuf.lastIndexOf("\n");return-1!=c&&(console.log(outputBuf.substr(0,c)),outputBuf=outputBuf.substr(c+1)),b.length},write(a,b,c,d,e,f){if(0!==c||d!==b.length||null!==e)return void f(enosys());const g=this.writeSync(a,b);f(null,g)}};const encoder=new TextEncoder("utf-8"),decoder=new TextDecoder("utf-8");class Go{constructor(){this.argv=["js"],this.env={},this.exit=a=>{0!==a&&console.warn("exit code:",a)},this._exitPromise=new Promise(a=>{this._resolveExitPromise=a}),this._pendingEvent=null,this._scheduledTimeouts=new Map,this._nextCallbackTimeoutID=1;const a=(a,b)=>{this.mem.setUint32(a+0,b,!0),this.mem.setUint32(a+4,Math.floor(b/4294967296),!0)},b=a=>{const b=this.mem.getUint32(a+0,!0),c=this.mem.getInt32(a+4,!0);return b+4294967296*c},c=a=>{const b=this.mem.getFloat64(a,!0);if(0!==b){if(!isNaN(b))return b;const c=this.mem.getUint32(a,!0);return this._values[c]}},d=(a,b)=>{const c=2146959360;if("number"==typeof b)return isNaN(b)?(this.mem.setUint32(a+4,2146959360,!0),void this.mem.setUint32(a,0,!0)):0===b?(this.mem.setUint32(a+4,2146959360,!0),void this.mem.setUint32(a,1,!0)):void this.mem.setFloat64(a,b,!0);switch(b){case void 0:return void this.mem.setFloat64(a,0,!0);case null:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,2,!0);case!0:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,3,!0);case!1:return this.mem.setUint32(a+4,c,!0),void this.mem.setUint32(a,4,!0);}let d=this._ids.get(b);d===void 0&&(d=this._idPool.pop(),d===void 0&&(d=this._values.length),this._values[d]=b,this._goRefCounts[d]=0,this._ids.set(b,d)),this._goRefCounts[d]++;let e=1;switch(typeof b){case"string":e=2;break;case"symbol":e=3;break;case"function":e=4;}this.mem.setUint32(a+4,2146959360|e,!0),this.mem.setUint32(a,d,!0)},e=a=>{const c=b(a+0),d=b(a+8);return new Uint8Array(this._inst.exports.mem.buffer,c,d)},f=d=>{const e=b(d+0),f=b(d+8),g=Array(f);for(let a=0;a<f;a++)g[a]=c(e+8*a);return g},g=a=>{const c=b(a+0),d=b(a+8);return decoder.decode(new DataView(this._inst.exports.mem.buffer,c,d))},h=Date.now()-performance.now();this.importObject={go:{"runtime.wasmExit":a=>{const b=this.mem.getInt32(a+8,!0);this.exited=!0,delete this._inst,delete this._values,delete this._goRefCounts,delete this._ids,delete this._idPool,this.exit(b)},"runtime.wasmWrite":a=>{const c=b(a+8),d=b(a+16),e=this.mem.getInt32(a+24,!0);fs.writeSync(c,new Uint8Array(this._inst.exports.mem.buffer,d,e))},"runtime.resetMemoryDataView":()=>{this.mem=new DataView(this._inst.exports.mem.buffer)},"runtime.nanotime1":b=>{a(b+8,1e6*(h+performance.now()))},"runtime.walltime1":b=>{const c=new Date().getTime();a(b+8,c/1e3),this.mem.setInt32(b+16,1e6*(c%1e3),!0)},"runtime.scheduleTimeoutEvent":a=>{const c=this._nextCallbackTimeoutID;this._nextCallbackTimeoutID++,this._scheduledTimeouts.set(c,setTimeout(()=>{for(this._resume();this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"),this._resume()},b(a+8)+1)),this.mem.setInt32(a+16,c,!0)},"runtime.clearTimeoutEvent":a=>{const b=this.mem.getInt32(a+8,!0);clearTimeout(this._scheduledTimeouts.get(b)),this._scheduledTimeouts.delete(b)},"runtime.getRandomData":a=>{crypto.getRandomValues(e(a+8))},"syscall/js.finalizeRef":a=>{const b=this.mem.getUint32(a+8,!0);if(this._goRefCounts[b]--,0===this._goRefCounts[b]){const a=this._values[b];this._values[b]=null,this._ids.delete(a),this._idPool.push(b)}},"syscall/js.stringVal":a=>{d(a+24,g(a+8))},"syscall/js.valueGet":a=>{const b=Reflect.get(c(a+8),g(a+16));a=this._inst.exports.getsp(),d(a+32,b)},"syscall/js.valueSet":a=>{Reflect.set(c(a+8),g(a+16),c(a+32))},"syscall/js.valueDelete":a=>{Reflect.deleteProperty(c(a+8),g(a+16))},"syscall/js.valueIndex":a=>{d(a+24,Reflect.get(c(a+8),b(a+16)))},"syscall/js.valueSetIndex":a=>{Reflect.set(c(a+8),b(a+16),c(a+24))},"syscall/js.valueCall":a=>{try{const b=c(a+8),e=Reflect.get(b,g(a+16)),h=f(a+32),i=Reflect.apply(e,b,h);a=this._inst.exports.getsp(),d(a+56,i),this.mem.setUint8(a+64,1)}catch(b){d(a+56,b),this.mem.setUint8(a+64,0)}},"syscall/js.valueInvoke":a=>{try{const b=c(a+8),e=f(a+16),g=Reflect.apply(b,void 0,e);a=this._inst.exports.getsp(),d(a+40,g),this.mem.setUint8(a+48,1)}catch(b){d(a+40,b),this.mem.setUint8(a+48,0)}},"syscall/js.valueNew":a=>{try{const b=c(a+8),e=f(a+16),g=Reflect.construct(b,e);a=this._inst.exports.getsp(),d(a+40,g),this.mem.setUint8(a+48,1)}catch(b){d(a+40,b),this.mem.setUint8(a+48,0)}},"syscall/js.valueLength":b=>{a(b+16,parseInt(c(b+8).length))},"syscall/js.valuePrepareString":b=>{const e=encoder.encode(c(b+8)+"");d(b+16,e),a(b+24,e.length)},"syscall/js.valueLoadString":a=>{const b=c(a+8);e(a+16).set(b)},"syscall/js.valueInstanceOf":a=>{this.mem.setUint8(a+24,c(a+8)instanceof c(a+16))},"syscall/js.copyBytesToGo":b=>{const d=e(b+8),f=c(b+32);if(!(f instanceof Uint8Array))return void this.mem.setUint8(b+48,0);const g=f.subarray(0,d.length);d.set(g),a(b+40,g.length),this.mem.setUint8(b+48,1)},"syscall/js.copyBytesToJS":b=>{const d=c(b+8),f=e(b+16);if(!(d instanceof Uint8Array))return void this.mem.setUint8(b+48,0);const g=f.subarray(0,d.length);d.set(g),a(b+40,g.length),this.mem.setUint8(b+48,1)},debug:a=>{console.log(a)}}}}async run(a){this._inst=a,this.mem=new DataView(this._inst.exports.mem.buffer),this._values=[NaN,0,null,!0,!1,window,this],this._goRefCounts=[],this._ids=new Map,this._idPool=[],this.exited=!1;let b=4096;const c=a=>{const c=b,d=encoder.encode(a+"\0");return new Uint8Array(this.mem.buffer,b,d.length).set(d),b+=d.length,0!=b%8&&(b+=8-b%8),c},d=this.argv.length,e=[];this.argv.forEach(a=>{e.push(c(a))}),e.push(0);const f=Object.keys(this.env).sort();f.forEach(a=>{e.push(c(a+"="+this.env[a]))}),e.push(0);const g=b;e.forEach(a=>{this.mem.setUint32(b,a,!0),this.mem.setUint32(b+4,0,!0),b+=8}),this._inst.exports.run(d,g),this.exited&&this._resolveExitPromise(),await this._exitPromise}_resume(){if(this.exited)throw new Error("Go program has already exited");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(a){const b=this;return function(){const c={id:a,this:this,args:arguments};return b._pendingEvent=c,b._resume(),c.result}}}const go=new Go;WebAssembly.instantiateStreaming(fetch("__path__"),go.importObject).then(a=>go.run(a.instance)).catch(a=>console.error("could not load wasm",a));`
25
+
26
+ // func wasmExecJs(path string) string {
27
+ // return strings.Replace(wasmExecTemplate, "__path__", path, 1)
28
+ // }
29
+
30
+ // var watchDelta = 1000 * time.Millisecond
31
+
32
+ // type Watcher struct {
33
+ // rootdir string
34
+ // watcher *fsnotify.Watcher
35
+ // watchVendor bool
36
+ // update chan string
37
+ // }
38
+
39
+ // // MustRegisterWatcher creates a new Watcher and starts listening to
40
+ // // given folders
41
+ // func MustRegisterWatcher() *Watcher {
42
+ // w := &Watcher{
43
+ // update: make(chan string),
44
+ // watchVendor: false,
45
+ // }
46
+ // var err error
47
+ // w.watcher, err = fsnotify.NewWatcher()
48
+ // if err != nil {
49
+ // log.Fatalf("Could not register watcher: %s", err)
50
+ // }
51
+ // w.watchFolders()
52
+ // return w
53
+ // }
54
+
55
+ // // Watch listens file updates, and sends signal to
56
+ // // update channel when .go and .tmpl files are updated
57
+ // func (w *Watcher) Watch() {
58
+ // eventSent := false
59
+ // for {
60
+ // select {
61
+ // case event := <-w.watcher.Events:
62
+ // // discard chmod events
63
+ // if event.Op&fsnotify.Chmod != fsnotify.Chmod {
64
+ // // test files do not need a rebuild
65
+ // if isTestFile(event.Name) {
66
+ // continue
67
+ // }
68
+ // if !isWatchedFileType(event.Name) {
69
+ // continue
70
+ // }
71
+ // if eventSent {
72
+ // continue
73
+ // }
74
+ // eventSent = true
75
+ // // prevent consequent builds
76
+ // go func() {
77
+ // w.update <- event.Name
78
+ // time.Sleep(watchDelta)
79
+ // eventSent = false
80
+ // }()
81
+
82
+ // }
83
+ // case err := <-w.watcher.Errors:
84
+ // if err != nil {
85
+ // log.Fatalf("Watcher error: %s", err)
86
+ // }
87
+ // return
88
+ // }
89
+ // }
90
+ // }
91
+
92
+ // func isTestFile(fileName string) bool {
93
+ // return strings.HasSuffix(filepath.Base(fileName), "_test.go")
94
+ // }
95
+
96
+ // func isWatchedFileType(fileName string) bool {
97
+ // ext := filepath.Ext(fileName)
98
+
99
+ // return ext == ".go"
100
+ // }
101
+
102
+ // // Close closes the fsnotify watcher channel
103
+ // func (w *Watcher) Close() {
104
+ // w.watcher.Close()
105
+ // close(w.update)
106
+ // }
107
+
108
+ // // watchFolders recursively adds folders that will be watched against the changes,
109
+ // // starting from the working directory
110
+ // func (w *Watcher) watchFolders() {
111
+ // wd, err := os.Getwd()
112
+
113
+ // if err != nil {
114
+ // log.Fatalf("Could not get root working directory: %s", err)
115
+ // }
116
+
117
+ // filepath.Walk(wd, func(path string, info os.FileInfo, err error) error {
118
+ // // skip files
119
+ // if info == nil {
120
+ // log.Fatalf("wrong watcher package: %s", path)
121
+ // }
122
+
123
+ // if !info.IsDir() {
124
+ // return nil
125
+ // }
126
+
127
+ // if !w.watchVendor {
128
+ // // skip vendor directory
129
+ // vendor := fmt.Sprintf("%s/vendor", wd)
130
+ // if strings.HasPrefix(path, vendor) {
131
+ // return filepath.SkipDir
132
+ // }
133
+ // }
134
+
135
+ // // skip hidden folders
136
+ // if len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") {
137
+ // return filepath.SkipDir
138
+ // }
139
+
140
+ // w.addFolder(path)
141
+
142
+ // return err
143
+ // })
144
+ // }
145
+
146
+ // // addFolder adds given folder name to the watched folders, and starts
147
+ // // watching it for further changes
148
+ // func (w *Watcher) addFolder(name string) {
149
+ // println("watch: " + name)
150
+ // if err := w.watcher.Add(name); err != nil {
151
+ // log.Fatalf("Could not watch folder: %s", err)
152
+ // }
153
+ // }
154
+
155
+ // type RouteInfo struct {
156
+ // Path string
157
+ // FuncName string
158
+ // RenderFunc func(*app.RenderContext) app.UI
159
+ // }
160
+
161
+ // var routesMap = map[string]RouteInfo{}
162
+
163
+ // func getSOPath(p string) string {
164
+ // return "build/" + filepath.Base(filepath.Dir(p)) + ".so"
165
+ // }
166
+
167
+ // func getWasmPath(p string) string {
168
+ // return "build/" + filepath.Base(filepath.Dir(p)) + ".wasm"
169
+ // }
170
+
171
+ // func buildSo(path string) (string, error) {
172
+ // soPath := getSOPath(path)
173
+ // out, err := exec.Command("go", "build", "-buildmode=plugin", "-o", soPath, path).CombinedOutput()
174
+ // if err != nil {
175
+ // println(string(out))
176
+ // println(err.Error())
177
+ // return "", err
178
+ // }
179
+ // fmt.Printf("wrote: %s\n", soPath)
180
+ // return soPath, nil
181
+ // }
182
+
183
+ // func buildWasm(path string) (string, error) {
184
+ // wasmPath := getWasmPath(path)
185
+ // cmd := exec.Command("go", "build", "-o", wasmPath, path)
186
+ // cmd.Env = os.Environ()
187
+ // cmd.Env = append(cmd.Env, "GOOS=js", "GOARCH=wasm")
188
+ // out, err := cmd.CombinedOutput()
189
+ // if err != nil {
190
+ // println(string(out))
191
+ // println(err.Error())
192
+ // return "", err
193
+ // }
194
+ // fmt.Printf("wrote: %s\n", wasmPath)
195
+ // return wasmPath, nil
196
+ // }
197
+
198
+ // func getRoute(basePath, p string) (string, string) {
199
+ // clean := strings.Replace(strings.Replace(p, basePath, "", 1), ".go", "", -1)
200
+ // return strings.Replace(clean, "index", "", -1), strings.Title(strings.Replace(clean, "/", "", -1))
201
+ // }
202
+
203
+ // func writeMainFile(basePath string) {
204
+ // tpl, err := template.New("writeMain").Parse(`// GENERATED FILE DO NOT EDIT
205
+ // package main
206
+
207
+ // import (
208
+ // . "github.com/pyros2097/wapp"
209
+ // "github.com/pyros2097/wapp/js"
210
+ // )
211
+
212
+ // func main() {
213
+ // {{range $key, $element := .}}
214
+ // if js.Window.URL().Path == "{{.Path}}" {
215
+ // Run({{.FuncName}})
216
+ // }
217
+ // {{end}}
218
+ // }
219
+ // `)
220
+ // if err != nil {
221
+ // panic(err)
222
+ // }
223
+ // buf := bytes.NewBuffer(nil)
224
+ // err = tpl.Execute(buf, routesMap)
225
+ // if err != nil {
226
+ // panic(err)
227
+ // }
228
+ // err = ioutil.WriteFile("pages/main.go", buf.Bytes(), 0644)
229
+ // if err != nil {
230
+ // panic(err)
231
+ // }
232
+ // }
233
+
234
+ // func loadRoutes(p *plugin.Plugin, basePath string, dry bool) {
235
+ // err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
236
+ // if err != nil {
237
+ // fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
238
+ // return err
239
+ // }
240
+ // if !info.IsDir() {
241
+ // routePath, routeFunc := getRoute(basePath, path)
242
+ // if routePath == "/main" {
243
+ // return nil
244
+ // }
245
+ // routesMap[routePath] = RouteInfo{
246
+ // Path: routePath,
247
+ // FuncName: routeFunc,
248
+ // }
249
+ // if dry {
250
+ // // println("Dry")
251
+ // return nil
252
+ // }
253
+ // fmt.Printf("route: %s routeFunc: %s\n", routePath, routeFunc)
254
+ // renderFn, err := p.Lookup(routeFunc)
255
+ // if err != nil {
256
+ // return err
257
+ // }
258
+ // routesMap[routePath] = RouteInfo{
259
+ // Path: routePath,
260
+ // FuncName: routeFunc,
261
+ // RenderFunc: renderFn.(func(*app.RenderContext) app.UI),
262
+ // }
263
+ // // println(createPage(routesMap[routePath](app.NewRenderContext())).String())
264
+ // }
265
+ // return nil
266
+ // })
267
+ // if err != nil {
268
+ // fmt.Printf("error walking the path %q: %v\n", basePath, err)
269
+ // panic(err)
270
+ // }
271
+ // }
272
+
273
+ // func CreatePage(title string, ui app.UI, wasmPath string) *bytes.Buffer {
274
+ // page := bytes.NewBuffer(nil)
275
+ // page.WriteString("<!DOCTYPE html>\n")
276
+ // elems := app.FilterUIElems(ui)
277
+ // app.Html(
278
+ // app.Head(
279
+ // app.Title(title),
280
+ // app.Meta("author", "pyros2097"),
281
+ // app.Meta("description", "Description"),
282
+ // app.Meta("keywords", ""),
283
+ // app.Meta("theme-color", ""),
284
+ // app.Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
285
+ // app.Link("icon", "/assets/icon.png"),
286
+ // app.Link("apple-touch-icon", "/assets/icon.png"),
287
+ // app.Link("stylesheet", "/assets/styles.css"),
288
+ // app.Link("manifest", "manifest"),
289
+ // app.Script(`const enosys = () => { const a = new Error("not implemented"); return a.code = "ENOSYS", a }; let outputBuf = ""; window.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, writeSync(a, b) { outputBuf += decoder.decode(b); const c = outputBuf.lastIndexOf("\n"); return -1 != c && (console.log(outputBuf.substr(0, c)), outputBuf = outputBuf.substr(c + 1)), b.length }, write(a, b, c, d, e, f) { if (0 !== c || d !== b.length || null !== e) return void f(enosys()); const g = this.writeSync(a, b); f(null, g) } }; const encoder = new TextEncoder("utf-8"), decoder = new TextDecoder("utf-8"); class Go { constructor() { this.argv = ["js"], this.env = {}, this.exit = a => { 0 !== a && console.warn("exit code:", a) }, this._exitPromise = new Promise(a => { this._resolveExitPromise = a }), this._pendingEvent = null, this._scheduledTimeouts = new Map, this._nextCallbackTimeoutID = 1; const a = (a, b) => { this.mem.setUint32(a + 0, b, !0), this.mem.setUint32(a + 4, Math.floor(b / 4294967296), !0) }, b = a => { const b = this.mem.getUint32(a + 0, !0), c = this.mem.getInt32(a + 4, !0); return b + 4294967296 * c }, c = a => { const b = this.mem.getFloat64(a, !0); if (0 !== b) { if (!isNaN(b)) return b; const c = this.mem.getUint32(a, !0); return this._values[c] } }, d = (a, b) => { const c = 2146959360; if ("number" == typeof b) return isNaN(b) ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 0, !0)) : 0 === b ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 1, !0)) : void this.mem.setFloat64(a, b, !0); switch (b) { case void 0: return void this.mem.setFloat64(a, 0, !0); case null: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 2, !0); case !0: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 3, !0); case !1: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 4, !0); }let d = this._ids.get(b); d === void 0 && (d = this._idPool.pop(), d === void 0 && (d = this._values.length), this._values[d] = b, this._goRefCounts[d] = 0, this._ids.set(b, d)), this._goRefCounts[d]++; let e = 1; switch (typeof b) { case "string": e = 2; break; case "symbol": e = 3; break; case "function": e = 4; }this.mem.setUint32(a + 4, 2146959360 | e, !0), this.mem.setUint32(a, d, !0) }, e = a => { const c = b(a + 0), d = b(a + 8); return new Uint8Array(this._inst.exports.mem.buffer, c, d) }, f = d => { const e = b(d + 0), f = b(d + 8), g = Array(f); for (let a = 0; a < f; a++)g[a] = c(e + 8 * a); return g }, g = a => { const c = b(a + 0), d = b(a + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, c, d)) }, h = Date.now() - performance.now(); this.importObject = { go: { "runtime.wasmExit": a => { const b = this.mem.getInt32(a + 8, !0); this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(b) }, "runtime.wasmWrite": a => { const c = b(a + 8), d = b(a + 16), e = this.mem.getInt32(a + 24, !0); fs.writeSync(c, new Uint8Array(this._inst.exports.mem.buffer, d, e)) }, "runtime.resetMemoryDataView": () => { this.mem = new DataView(this._inst.exports.mem.buffer) }, "runtime.nanotime1": b => { a(b + 8, 1e6 * (h + performance.now())) }, "runtime.walltime1": b => { const c = new Date().getTime(); a(b + 8, c / 1e3), this.mem.setInt32(b + 16, 1e6 * (c % 1e3), !0) }, "runtime.scheduleTimeoutEvent": a => { const c = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(c, setTimeout(() => { for (this._resume(); this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume() }, b(a + 8) + 1)), this.mem.setInt32(a + 16, c, !0) }, "runtime.clearTimeoutEvent": a => { const b = this.mem.getInt32(a + 8, !0); clearTimeout(this._scheduledTimeouts.get(b)), this._scheduledTimeouts.delete(b) }, "runtime.getRandomData": a => { crypto.getRandomValues(e(a + 8)) }, "syscall/js.finalizeRef": a => { const b = this.mem.getUint32(a + 8, !0); if (this._goRefCounts[b]--, 0 === this._goRefCounts[b]) { const a = this._values[b]; this._values[b] = null, this._ids.delete(a), this._idPool.push(b) } }, "syscall/js.stringVal": a => { d(a + 24, g(a + 8)) }, "syscall/js.valueGet": a => { const b = Reflect.get(c(a + 8), g(a + 16)); a = this._inst.exports.getsp(), d(a + 32, b) }, "syscall/js.valueSet": a => { Reflect.set(c(a + 8), g(a + 16), c(a + 32)) }, "syscall/js.valueDelete": a => { Reflect.deleteProperty(c(a + 8), g(a + 16)) }, "syscall/js.valueIndex": a => { d(a + 24, Reflect.get(c(a + 8), b(a + 16))) }, "syscall/js.valueSetIndex": a => { Reflect.set(c(a + 8), b(a + 16), c(a + 24)) }, "syscall/js.valueCall": a => { try { const b = c(a + 8), e = Reflect.get(b, g(a + 16)), h = f(a + 32), i = Reflect.apply(e, b, h); a = this._inst.exports.getsp(), d(a + 56, i), this.mem.setUint8(a + 64, 1) } catch (b) { d(a + 56, b), this.mem.setUint8(a + 64, 0) } }, "syscall/js.valueInvoke": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.apply(b, void 0, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueNew": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.construct(b, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueLength": b => { a(b + 16, parseInt(c(b + 8).length)) }, "syscall/js.valuePrepareString": b => { const e = encoder.encode(c(b + 8) + ""); d(b + 16, e), a(b + 24, e.length) }, "syscall/js.valueLoadString": a => { const b = c(a + 8); e(a + 16).set(b) }, "syscall/js.valueInstanceOf": a => { this.mem.setUint8(a + 24, c(a + 8) instanceof c(a + 16)) }, "syscall/js.copyBytesToGo": b => { const d = e(b + 8), f = c(b + 32); if (!(f instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, "syscall/js.copyBytesToJS": b => { const d = c(b + 8), f = e(b + 16); if (!(d instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, debug: a => { console.log(a) } } } } async run(a) { this._inst = a, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [NaN, 0, null, !0, !1, window, this], this._goRefCounts = [], this._ids = new Map, this._idPool = [], this.exited = !1; let b = 4096; const c = a => { const c = b, d = encoder.encode(a + "\0"); return new Uint8Array(this.mem.buffer, b, d.length).set(d), b += d.length, 0 != b % 8 && (b += 8 - b % 8), c }, d = this.argv.length, e = []; this.argv.forEach(a => { e.push(c(a)) }), e.push(0); const f = Object.keys(this.env).sort(); f.forEach(a => { e.push(c(a + "=" + this.env[a])) }), e.push(0); const g = b; e.forEach(a => { this.mem.setUint32(b, a, !0), this.mem.setUint32(b + 4, 0, !0), b += 8 }), this._inst.exports.run(d, g), this.exited && this._resolveExitPromise(), await this._exitPromise } _resume() { if (this.exited) throw new Error("Go program has already exited"); this._inst.exports.resume(), this.exited && this._resolveExitPromise() } _makeFuncWrapper(a) { const b = this; return function () { const c = { id: a, this: this, args: arguments }; return b._pendingEvent = c, b._resume(), c.result } } } const go = new Go; WebAssembly.instantiateStreaming(fetch("/assets/main.wasm"), go.importObject).then(a => go.run(a.instance)).catch(a => console.error("could not load wasm", a));`),
290
+ // ),
291
+ // app.Body(elems[0]),
292
+ // ).Html(page)
293
+ // return page
294
+ // }
295
+
296
+ // func serve(wd string, isDev bool) {
297
+ // assetsFileServer := http.FileServer(pkger.Dir(filepath.Join(wd, "assets")))
298
+ // buildFileServer := http.FileServer(pkger.Dir(filepath.Join(wd, "build")))
299
+ // http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
300
+ // println("path: " + r.URL.Path)
301
+ // if routeInfo, ok := routesMap[r.URL.Path]; ok {
302
+ // wasmPath := "/build/" + filepath.Base(wd) + ".wasm"
303
+ // page := CreatePage(routeInfo.RenderFunc(app.NewRenderContext()), wasmPath)
304
+ // w.Header().Set("Content-Length", strconv.Itoa(page.Len()))
305
+ // w.Header().Set("Content-Type", "text/html")
306
+ // w.WriteHeader(http.StatusOK)
307
+ // w.Write(page.Bytes())
308
+ // } else if strings.Contains(r.URL.Path, "/build") {
309
+ // r.URL.Path = strings.Replace(r.URL.Path, "/build", "", 1)
310
+ // buildFileServer.ServeHTTP(w, r)
311
+ // } else {
312
+ // r.URL.Path = strings.Replace(r.URL.Path, "/assets", "", 1)
313
+ // assetsFileServer.ServeHTTP(w, r)
314
+ // }
315
+ // })
316
+ // if isDev {
317
+ // println("Serving on HTTP port: 1234")
318
+ // http.ListenAndServe(":1234", nil)
319
+ // } else {
320
+ // println("algnhsa serving default mux")
321
+ // algnhsa.ListenAndServe(http.DefaultServeMux, nil)
322
+ // }
323
+ // }
324
+
325
+ // func buildAll(basePath string) {
326
+ // loadRoutes(nil, basePath, true)
327
+ // writeMainFile(basePath)
328
+ // buildWasm(basePath)
329
+ // soPath, err := buildSo(basePath)
330
+ // if err != nil {
331
+ // println("could not build")
332
+ // panic(err)
333
+ // }
334
+ // p, err := plugin.Open(soPath)
335
+ // if err != nil {
336
+ // println("could not load so plugin")
337
+ // panic(err)
338
+ // }
339
+ // // fmt.Printf("%+v\n", p)
340
+ // loadRoutes(p, basePath, false)
341
+ // }
342
+
343
+ // func Watch(isDev bool) {
344
+ // wd, err := os.Getwd()
345
+ // if err != nil {
346
+ // fmt.Printf("could not get wd")
347
+ // return
348
+ // }
349
+ // if isDev {
350
+ // basePath := filepath.Join(wd, "pages")
351
+ // buildAll(basePath)
352
+ // watcher := MustRegisterWatcher()
353
+ // go watcher.Watch()
354
+ // go serve(wd, isDev)
355
+ // for file := range watcher.update {
356
+ // println("changed: " + file)
357
+ // buildAll(basePath)
358
+ // }
359
+ // } else {
360
+ // serve(wd, isDev)
361
+ // }
362
+ // }
example/{pages/about.go → about.go} RENAMED
File without changes
example/{config.css → assets/config.css} RENAMED
File without changes
example/assets/manifest.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "short_name": "{{.ShortName}}",
3
- "name": "{{.Name}}",
4
- "icons": [
5
- {
6
- "src": "{{.DefaultIcon}}",
7
- "type": "image/png",
8
- "sizes": "192x192"
9
- },
10
- {
11
- "src": "{{.LargeIcon}}",
12
- "type": "image/png",
13
- "sizes": "512x512"
14
- }
15
- ],
16
- "scope": "{{.Scope}}",
17
- "start_url": "{{.StartURL}}",
18
- "background_color": "{{.BackgroundColor}}",
19
- "display": "standalone",
20
- "theme_color": "{{.ThemeColor}}"
21
- }
example/{pages/clock.go → clock.go} RENAMED
File without changes
example/{pages/container.go → container.go} RENAMED
File without changes
example/{pages/index.go → index.go} RENAMED
File without changes
example/main.go CHANGED
@@ -1,9 +1,16 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "github.com/pyros2097/wapp/cli"
4
+ app "github.com/pyros2097/wapp"
5
5
  )
6
6
 
7
+ var routes = map[string]app.RenderFunc{
8
+ "/about": About,
9
+ "/clock": Clock,
10
+ "/container": Container,
11
+ "/": Index,
12
+ }
13
+
7
14
  func main() {
8
- cli.Watch()
15
+ app.Run(false, routes)
9
16
  }
example/makefile CHANGED
@@ -1,2 +1,11 @@
1
+ run:
2
+ go run *.go
3
+
4
+ build:
5
+ go build
6
+
7
+ build-wasm:
8
+ GOOS=js GOARCH=wasm go build -o assets/main.wasm
9
+
1
10
  build-css:
2
- npx tailwindcss-cli@latest build config.css -o assets/styles.css
11
+ npx tailwindcss-cli@latest build assets/config.css -o assets/styles.css
example/pages/main.go DELETED
@@ -1,27 +0,0 @@
1
- // GENERATED FILE DO NOT EDIT
2
- package main
3
-
4
- import (
5
- . "github.com/pyros2097/wapp"
6
- "github.com/pyros2097/wapp/js"
7
- )
8
-
9
- func main() {
10
-
11
- if js.Window.URL().Path == "/" {
12
- Run(Index)
13
- }
14
-
15
- if js.Window.URL().Path == "/about" {
16
- Run(About)
17
- }
18
-
19
- if js.Window.URL().Path == "/clock" {
20
- Run(Clock)
21
- }
22
-
23
- if js.Window.URL().Path == "/container" {
24
- Run(Container)
25
- }
26
-
27
- }
example/readme.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  An example to demostrate the wapp framework
4
4
 
5
- Just run,
5
+ 1. Compile wasm frontend,
6
+ `GOOS=js GOARCH=wasm go build -o assets/main.wasm`
6
7
 
8
+ 2. Run the server,
7
- `go run main.go`
9
+ `go run *.go`
10
+
11
+ These commands are also available in the makefile for convenience
example/samconfig.toml ADDED
@@ -0,0 +1,9 @@
1
+ version = 0.1
2
+ [default]
3
+ [default.deploy]
4
+ [default.deploy.parameters]
5
+ stack_name = "go-app"
6
+ s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-grb3hle00qk9"
7
+ s3_prefix = "go-app"
8
+ region = "us-east-1"
9
+ capabilities = "CAPABILITY_IAM"
example/template.yml ADDED
@@ -0,0 +1,43 @@
1
+ AWSTemplateFormatVersion: 2010-09-09
2
+ Outputs:
3
+ ApiUrl:
4
+ Export:
5
+ Name: ApiUrl
6
+ Value:
7
+ Fn::Sub: https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/Prod/
8
+ Resources:
9
+ HttpApi:
10
+ Properties:
11
+ AccessLogSettings:
12
+ DestinationArn:
13
+ Fn::GetAtt:
14
+ - HttpApiAccessLogs
15
+ - Arn
16
+ Format:
17
+ '{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp",
18
+ "caller":"$context.identity.caller", "user":"$context.identity.user", "requestTime":"$context.requestTime","routeKey":"$context.routeKey",
19
+ "status":"$context.status", "error": "$context.integrationErrorMessage"}'
20
+ StageName: Prod
21
+ Type: AWS::Serverless::HttpApi
22
+ HttpApiAccessLogs:
23
+ Properties:
24
+ LogGroupName: /aws/http/py-app
25
+ RetentionInDays: 365
26
+ Type: AWS::Logs::LogGroup
27
+ RouteIndex:
28
+ Properties:
29
+ Handler: ./example
30
+ Events:
31
+ ApiEvent:
32
+ Properties:
33
+ ApiId:
34
+ Ref: HttpApi
35
+ Method: GET
36
+ Path: $default
37
+ PayloadFormatVersion: '1.0'
38
+ Type: HttpApi
39
+ MemorySize: 128
40
+ Runtime: go1.x
41
+ Timeout: 30
42
+ Type: AWS::Serverless::Function
43
+ Transform: AWS::Serverless-2016-10-31
go.mod CHANGED
@@ -3,7 +3,9 @@ module github.com/pyros2097/wapp
3
3
  go 1.15
4
4
 
5
5
  require (
6
+ github.com/akrylysov/algnhsa v0.12.1
7
+ github.com/aws/aws-lambda-go v1.20.0
6
- github.com/fsnotify/fsnotify v1.4.7 // indirect
8
+ github.com/awslabs/goformation/v4 v4.15.6
7
9
  github.com/markbates/pkger v0.17.1
8
10
  github.com/stretchr/testify v1.6.1
9
11
  golang.org/x/sys v0.0.0-20201112073958-5cba982894dd // indirect
utils.go CHANGED
@@ -2,6 +2,7 @@ package app
2
2
 
3
3
  import (
4
4
  "io"
5
+ "regexp"
5
6
  "unsafe"
6
7
 
7
8
  "github.com/pyros2097/wapp/js"
@@ -48,3 +49,33 @@ func btos(b []byte) string {
48
49
  func stob(s string) []byte {
49
50
  return *(*[]byte)(unsafe.Pointer(&s))
50
51
  }
52
+
53
+ func matchPath(k, p string) bool {
54
+ validRoute := regexp.MustCompile(k)
55
+ if validRoute.MatchString(p) {
56
+ return true
57
+ }
58
+ return false
59
+ }
60
+
61
+ func MatchRoute(routes map[string]RenderFunc, path string) RenderFunc {
62
+ for key, renderFn := range routes {
63
+ if matchPath(key, path) {
64
+ return renderFn
65
+ }
66
+ }
67
+ notFound, ok := routes["/notfound"]
68
+ if ok {
69
+ return notFound
70
+ }
71
+ return func(c *RenderContext) UI {
72
+ return Col(
73
+ Row(
74
+ "This is the default 404 - Not Found Route handler",
75
+ ),
76
+ Row(
77
+ "Create a notfound.go file and add a func NotFound(c *RenderContext) UI {} to override it",
78
+ ),
79
+ )
80
+ }
81
+ }
utils_test.go ADDED
@@ -0,0 +1,22 @@
1
+ package app
2
+
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/assert"
7
+ )
8
+
9
+ func stubRoute(c *RenderContext) UI {
10
+ return Div(Text("Stub"))
11
+ }
12
+
13
+ var testRoutes = map[string]RenderFunc{
14
+ "/about": stubRoute,
15
+ "/clock": stubRoute,
16
+ "/container": stubRoute,
17
+ "/": stubRoute,
18
+ }
19
+
20
+ func TestMatchPath(t *testing.T) {
21
+ assert.Equal(t, true, matchPath("/", "/about"))
22
+ }