on master help sign in
Preliminary wasm support
commitc7d188e9b2bf821ad268a94330e671c2a05d928e authorDivam <dfordivam@gmail.com> dateFri 3 Apr 2020, 21:13 (7 years ago) parentd8768a1
8 files changed+114−33
default.nix+27−4 dep/reflex-platform/github.json+5−5 dep/webabi/default.nix+25 lib/backend/src/Obelisk/Backend.hs+39−8 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+4−2 lib/frontend/src/Obelisk/Frontend.hs+11−12 lib/run/src/Obelisk/Run.hs+2−1
default.nix+27−4
@@ -32,7 +32,7 @@ let
3232 # Development environments for obelisk packages.
3333 ghcObeliskEnvs = pkgs.lib.mapAttrs (n: v: reflex-platform.workOn ghcObelisk v) ghcObelisk;
3434
35− inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
35+ inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets mkAssetsWith gzipEncodings;
3636
3737 haskellLib = pkgs.haskell.lib;
3838
@@ -85,6 +85,25 @@ in rec {
8585 echo "//# sourceMappingURL=all.js.map" >> all.js
8686 ''}
8787 '';
88+ webabi = pkgs.callPackage ./dep/webabi {};
89+ # wasm-opt seems to have no effect
90+ stripWasm = frontend: pkgs.runCommand "stripWasm" {} ''
91+ mkdir $out
92+ cd $out
93+ ln -s "${frontend}/bin/frontend" frontend.original.wasm
94+ cp "${frontend}/bin/frontend" frontend.wasm
95+ chmod u+rw frontend.wasm
96+ chmod a-x frontend.wasm
97+ ${pkgs.wabt}/bin/wasm-strip frontend.wasm
98+ chmod u-r frontend.wasm
99+ '';
100+ wasm-assets = frontend : pkgs.runCommand "wasm-assets" {} ''
101+ mkdir $out
102+ cd $out
103+ cp ${stripWasm frontend}/frontend.wasm $out/frontend.wasm
104+ cp ${webabi}/lib/node_modules/webabi/jsaddleJS/* $out/
105+ cp ${webabi}/lib/node_modules/webabi/build/worker_runner.js $out/worker_runner.js
106+ '';
88107
89108 serverModules = {
90109 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
@@ -165,15 +184,17 @@ in rec {
165184 };
166185 };
167186
168− inherit mkAssets;
187+ inherit mkAssets mkAssetsWith gzipEncodings;
169188
170− serverExe = backend: frontend: assets: optimizationLevel: version:
189+ # TODO add ghcjs back
190+ # XXX the gzipEncodings helps faster debug cycles, use zopfliEncodings for best size
191+ serverExe = backend: frontend: assets: optimizationLevel: wasm-frontend: version:
171192 pkgs.runCommand "serverExe" {} ''
172193 mkdir $out
173194 set -eux
174195 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
175196 ln -s "${mkAssets assets}" $out/static.assets
176− ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
197+ ln -s ${mkAssetsWith gzipEncodings (wasm-assets wasm-frontend)} $out/frontend.wasm.assets
177198 echo ${version} > $out/version
178199 '';
179200
@@ -280,6 +301,7 @@ in rec {
280301 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
281302 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
282303 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
304+ wasm = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
283305 };
284306 android = self.__androidWithConfig (self.base + "/config");
285307 ios = self.__iosWithConfig (self.base + "/config");
@@ -305,6 +327,7 @@ in rec {
305327 mainProjectOut.ghcjs.frontend
306328 projectInst.passthru.staticFiles
307329 projectInst.passthru.__closureCompilerOptimizationLevel
330+ mainProjectOut.wasm.frontend
308331 version;
309332 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
310333 dummyVersion = "Version number is only available for deployments";
dep/reflex-platform/github.json+5−5
@@ -1,8 +1,8 @@
11{
2− "owner": "reflex-frp",
2+ "owner": "dfordivam",
33 "repo": "reflex-platform",
4− "branch": "master",
4+ "branch": "ob-1",
55 "private": false,
6− "rev": "41be4d952b75515a037318aa344dd6b13ad29cfe",
7− "sha256": "132yqzyzd3c5fjy7wwnwa5d6pjxv5ap2xz0swwbv3h5pwi8jwv0a"
8−}
6+ "rev": "d3db8e8a104788f8546b812180c5c20e2430cf98",
7+ "sha256": "08zkn0whgvb0grw571r6bdawbi90c9jpii7hak7lmq9i29wxl3bv"
8+}
dep/webabi/default.nixnew file+25
@@ -0,0 +1,25 @@
1+{ nodePackages, git, pkgs, system }:
2+
3+let
4+ runNode2nix = name: src: pkgs.runCommand "${name}-nix" { nativeBuildInputs = [nodePackages.node2nix]; } ''
5+ mkdir -p $out
6+ cd $out
7+ node2nix -i ${src}/package.json -l ${src}/package-lock.json --include-peer-dependencies --nodejs-10 -d -o ./node-packages.nix -e ./node-env.nix -c ./default.nix
8+ '';
9+
10+ callNode2nix = name: src: import (runNode2nix name src) { inherit pkgs system; };
11+
12+ webabiSrc = pkgs.fetchFromGitHub {
13+ owner = "WebGHC";
14+ repo = "webabi";
15+ rev = "7ffeff005dbdce60c96d830a5f67c4c461c0f5a1";
16+ sha256 = "0znwbhgpgf6kk0zwbnhvnx26rgvqql6isnblh87q6wd0dnc57h2x";
17+ };
18+
19+in (callNode2nix "webabi" webabiSrc).package.overrideAttrs (old: {
20+ postInstall = ''
21+ ${old.postInstall or ""}
22+ patchShebangs .
23+ npm run build
24+ '';
25+})
lib/backend/src/Obelisk/Backend.hs+39−8
@@ -20,6 +20,7 @@ module Obelisk.Backend
2020 , runBackendWith
2121 -- * Configuration of backend
2222 , GhcjsWidgets(..)
23+ , GhcjsWasmAssets(..)
2324 , defaultGhcjsWidgets
2425 -- * all.js script loading functions
2526 , deferredGhcjsScript
@@ -73,7 +74,7 @@ data Backend backendRoute frontendRoute = Backend
7374data BackendConfig frontendRoute = BackendConfig
7475 { _backendConfig_runSnap :: !(Snap () -> IO ()) -- ^ Function to run the snap server
7576 , _backendConfig_staticAssets :: !StaticAssets -- ^ Static assets
76− , _backendConfig_ghcjsWidgets :: !(GhcjsWidgets (Text -> FrontendWidgetT (R frontendRoute) ()))
77+ , _backendConfig_ghcjsWidgets :: !(GhcjsWidgets (GhcjsWasmAssets -> FrontendWidgetT (R frontendRoute) ()))
7778 -- ^ Given the URL of all.js, return the widgets which are responsible for
7879 -- loading the script.
7980 } deriving (Generic)
@@ -92,10 +93,14 @@ data GhcjsWidgets a = GhcjsWidgets
9293 -- ^ A script widget, placed in the document body
9394 } deriving (Functor, Generic)
9495
96+data GhcjsWasmAssets = GhcjsWasmAssets
97+ { _ghcjsWasmAssets_allJs :: Text
98+ , _ghcjsWasmAssets_wasm :: Maybe (Text, Text)
99+ }
95100
96101-- | Given the URL of all.js, return the widgets which are responsible for
97102-- loading the script. Defaults to 'preloadGhcjs' and 'deferredGhcjsScript'.
98−defaultGhcjsWidgets :: GhcjsWidgets (Text -> FrontendWidgetT r ())
103+defaultGhcjsWidgets :: GhcjsWidgets (GhcjsWasmAssets -> FrontendWidgetT r ())
99104defaultGhcjsWidgets = GhcjsWidgets
100105 { _ghcjsWidgets_preload = preloadGhcjs
101106 , _ghcjsWidgets_script = deferredGhcjsScript
@@ -134,7 +139,7 @@ defaultStaticAssets = StaticAssets
134139
135140defaultFrontendGhcjsAssets :: StaticAssets
136141defaultFrontendGhcjsAssets = StaticAssets
137− { _staticAssets_processed = "frontend.jsexe.assets"
142+ { _staticAssets_processed = "frontend.wasm.assets"
138143 , _staticAssets_unprocessed = "frontend.jsexe"
139144 }
140145
@@ -167,6 +172,11 @@ renderAllJsPath :: Encoder Identity Identity (R (FullRoute a b)) PageName -> Tex
167172renderAllJsPath validFullEncoder =
168173 renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ ["all.js"]
169174
175+renderWasmPaths :: Encoder Identity Identity (R (FullRoute a b)) PageName -> (Text, Text)
176+renderWasmPaths validFullEncoder = (,)
177+ (renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ [])
178+ (renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ ["frontend.wasm"])
179+
170180serveObeliskApp
171181 :: (MonadSnap m, HasCookies m, MonadFail m)
172182 => (R appRoute -> Text)
@@ -243,11 +253,12 @@ runBackendWith (BackendConfig runSnap staticAssets ghcjsWidgets) backend fronten
243253 Identity r -> case r of
244254 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
245255 FullRoute_Frontend obeliskRoute :/ a ->
246− serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
256+ serveDefaultObeliskApp routeToUrl (($ GhcjsWasmAssets allJsUrl (Just wasmUrls)) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
247257 obeliskRoute :/ a
248258 where
249259 routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
250260 allJsUrl = renderAllJsPath validFullEncoder
261+ wasmUrls = renderWasmPaths validFullEncoder
251262
252263renderGhcjsFrontend
253264 :: (MonadSnap m, HasCookies m)
@@ -263,13 +274,33 @@ renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
263274
264275-- | Preload all.js in a link tag.
265276-- This is the default preload method.
266−preloadGhcjs :: Text -> FrontendWidgetT r ()
267−preloadGhcjs allJsUrl = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
277+preloadGhcjs :: GhcjsWasmAssets -> FrontendWidgetT r ()
278+preloadGhcjs (GhcjsWasmAssets allJsUrl mWasm) = case mWasm of
279+ Nothing -> elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
280+ Just _ -> pure ()
268281
269282-- | Load the script from the given URL in a deferred script tag.
270283-- This is the default method.
271−deferredGhcjsScript :: Text -> FrontendWidgetT r ()
272−deferredGhcjsScript allJsUrl = elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
284+deferredGhcjsScript :: GhcjsWasmAssets -> FrontendWidgetT r ()
285+deferredGhcjsScript (GhcjsWasmAssets allJsUrl mWasm) = case mWasm of
286+ Nothing -> elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
287+ Just v -> wasmScript v
288+
289+wasmScript :: (Text, Text) -> FrontendWidgetT r ()
290+wasmScript (jsaddleAssets, wasmUrl) = do
291+ let sendMsgWorkerJs = jsaddleAssets <> "/jsaddle_sendMsgWorker.js"
292+ jsaddleJs = jsaddleAssets <> "/jsaddle.js"
293+ workerRunnerJs = jsaddleAssets <> "/worker_runner.js"
294+ elAttr "script" ("type" =: "text/javascript" ) $ text $
295+ "var jsaddle_sendMsgWorkerPath = '" <> sendMsgWorkerJs <> "';"
296+ elAttr "script" ("type" =: "text/javascript" <> "src" =: jsaddleJs) blank
297+ elAttr "script" ("type" =: "text/javascript") $ text $ mconcat $
298+ [ "var jsaddleVals = jsaddleJsInit();"
299+ , "const worker = new Worker(\"" <> workerRunnerJs <> "\");"
300+ , "worker.postMessage({ url: \"" <> wasmUrl <> "\""
301+ , " , jsaddleVals: jsaddleVals }"
302+ , " , [jsaddleVals.jsaddleListener]);"
303+ ]
273304
274305-- | An all.js script which is loaded after waiting for some time to pass. This
275306-- is useful to ensure any CSS animations on the page can play smoothly before
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+1−1
@@ -18,7 +18,7 @@ library
1818 build-depends: android-activity
1919 ld-options: -landroid
2020 else
21− if impl(ghcjs)
21+ if impl(ghcjs) || arch(wasm32)
2222 hs-source-dirs: src-ghcjs, src
2323 build-depends: ghcjs-dom
2424 else
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+4−2
@@ -2,11 +2,13 @@
22{-# LANGUAGE OverloadedStrings #-}
33module Obelisk.ExecutableConfig.Lookup where
44
5+import Control.Monad.IO.Class
6+import Control.Monad.Fail (MonadFail)
57import Data.ByteString (ByteString)
68import qualified Data.ByteString.Base64 as B64
79import Data.Map (Map)
810import qualified Data.Map as Map
9−import Data.Maybe (catMaybes)
11+import Data.Maybe (catMaybes, isJust)
1012import Data.Text (Text)
1113import qualified Data.Text.Encoding as T
1214import Data.Traversable (for)
@@ -19,7 +21,7 @@ import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
1921import GHCJS.DOM.ParentNode (querySelectorAll)
2022import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
2123
22−getConfigs :: IO (Map Text ByteString)
24+getConfigs :: (MonadJSM m, MonadFail m) => m (Map Text ByteString)
2325getConfigs = do
2426 Just doc <- currentDocument
2527 Just hd <- getHead doc
lib/frontend/src/Obelisk/Frontend.hs+11−12
@@ -151,21 +151,20 @@ runFrontend
151151 -> Frontend (R route)
152152 -> JSM ()
153153runFrontend validFullEncoder frontend = do
154+#if defined(ghcjs_HOST_OS) || defined(wasm32_HOST_ARCH)
154155 let mode = FrontendMode
155− { _frontendMode_hydrate =
156−#ifdef ghcjs_HOST_OS
157− True
158−#else
159− False
160−#endif
161− , _frontendMode_adjustRoute =
162−#ifdef ghcjs_HOST_OS
163− False
156+ { _frontendMode_hydrate = True
157+ , _frontendMode_adjustRoute = False
158+ }
159+ doLift = Prelude.id
164160#else
165− True
166−#endif
161+ let mode = FrontendMode
162+ { _frontendMode_hydrate = False
163+ , _frontendMode_adjustRoute = True
167164 }
168− configs <- liftIO Lookup.getConfigs
165+ doLift = liftIO
166+#endif
167+ configs <- doLift Lookup.getConfigs
169168 when (_frontendMode_hydrate mode) removeHTMLConfigs
170169 -- There's no fundamental reason that adjustRoute needs to control setting the
171170 -- initial route and *also* the useHash parameter; that's why these are
lib/run/src/Obelisk/Run.hs+2−1
@@ -97,9 +97,10 @@ run port serveStaticAsset backend frontend = do
9797 Identity r -> case r of
9898 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
9999 FullRoute_Frontend obeliskRoute :/ a ->
100− serveDefaultObeliskApp appRouteToUrl (($ allJsUrl) <$> defaultGhcjsWidgets) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
100+ serveDefaultObeliskApp appRouteToUrl (($ assetsUrl) <$> defaultGhcjsWidgets) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
101101 where
102102 appRouteToUrl (k :/ v) = renderObeliskRoute validFullEncoder (FullRoute_Frontend (ObeliskRoute_App k) :/ v)
103+ assetsUrl = GhcjsWasmAssets allJsUrl Nothing
103104 allJsUrl = renderAllJsPath validFullEncoder
104105
105106 let conf = defRunConfig { _runConfig_redirectPort = port }