on 5cde2aa help sign in
11 files changed+155−34
since splitsnapshots
unifiedsplit
default.nix+30−2 dep/reflex-platform/github.json+5−5 dep/webabi/default.nix+7 dep/webabi/github.json+6 lib/backend/obelisk-backend.cabal+1 lib/backend/src/Obelisk/Backend.hs+86−10 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 skeleton/frontend/frontend.cabal+2−1
default.nix+30−2
@@ -2,6 +2,7 @@
22, profiling ? false
33, iosSdkVersion ? "10.2"
44, config ? {}
5+, enableWasm ? true
56, reflex-platform-func ? import ./dep/reflex-platform
67}:
78let
@@ -85,6 +86,31 @@ in rec {
8586 echo "//# sourceMappingURL=all.js.map" >> all.js
8687 ''}
8788 '';
89+ webabi = pkgs.callPackage ./dep/webabi {};
90+ # wasm-opt seems to have no effect
91+ stripWasm = frontend: pkgs.runCommand "stripWasm" {} ''
92+ mkdir $out
93+ cd $out
94+ ln -s "${frontend}/bin/frontend" frontend.original.wasm
95+ cp "${frontend}/bin/frontend" frontend.wasm
96+ chmod u+rw frontend.wasm
97+ chmod a-x frontend.wasm
98+ ${pkgs.wabt}/bin/wasm-strip frontend.wasm
99+ '';
100+
101+ # The ghcjs and wasm files are put in a single asset, as they need to be in sync and served together.
102+ combinedJsWasmAssets = frontendJs: optimizationLevel: frontendWasm: pkgs.runCommand "combinedJsWasmAssets" {} ''
103+ mkdir $out
104+ cd $out
105+ ${if frontendJs != null then ''
106+ ln -s ${compressedJs frontendJs optimizationLevel}/* .
107+ '' else '' ''}
108+ ${if frontendWasm != null then ''
109+ ln -s ${stripWasm frontendWasm}/* .
110+ ln -s ${webabi}/lib/node_modules/webabi/jsaddleJS/* .
111+ ln -s ${webabi}/lib/node_modules/webabi/build/mainthread_runner.js .
112+ '' else '' ''}
113+ '';
88114
89115 serverModules = {
90116 mkBaseEc2 = { nixosPkgs, ... }: {...}: {
@@ -167,13 +193,13 @@ in rec {
167193
168194 inherit mkAssets;
169195
170− serverExe = backend: frontend: assets: optimizationLevel: version:
196+ serverExe = backend: frontendJs: assets: optimizationLevel: frontendWasm: version:
171197 pkgs.runCommand "serverExe" {} ''
172198 mkdir $out
173199 set -eux
174200 ln -s "${if profiling then backend else haskellLib.justStaticExecutables backend}"/bin/* $out/
175201 ln -s "${mkAssets assets}" $out/static.assets
176− ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
202+ ln -s ${mkAssets (combinedJsWasmAssets frontendJs optimizationLevel frontendWasm)} $out/frontend.jsexe.assets
177203 echo ${version} > $out/version
178204 '';
179205
@@ -280,6 +306,7 @@ in rec {
280306 lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcSavedSplices;
281307 ghc = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghc;
282308 ghcjs = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
309+ wasm = lib.filter (x: lib.hasAttr x self.combinedPackages) self.shells-ghcjs;
283310 };
284311 android = self.__androidWithConfig (self.base + "/config");
285312 ios = self.__iosWithConfig (self.base + "/config");
@@ -305,6 +332,7 @@ in rec {
305332 mainProjectOut.ghcjs.frontend
306333 projectInst.passthru.staticFiles
307334 projectInst.passthru.__closureCompilerOptimizationLevel
335+ (if enableWasm then mainProjectOut.wasm.frontend else null)
308336 version;
309337 linuxExe = serverOn (projectOut { system = "x86_64-linux"; });
310338 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": "2ab56c00a3d1b5f0c026fa8b339968fab66bdcc3",
7+ "sha256": "05ilpykbp6ipzfack54zjkc9dvfdvl134vx2k36d0gvb0nswmlcm"
8+}
dep/webabi/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
dep/webabi/github.jsonnew file+6
@@ -0,0 +1,6 @@
1+{
2+ "owner": "WebGHC",
3+ "repo": "webabi",
4+ "rev": "666af48756dc9f49f0320d56731ef355534292c2",
5+ "sha256": "1brs0b0ybfaa0fcw4cbb7vmbpf7x48niv5y5hskvk6s9zgj1yp5j"
6+}
lib/backend/obelisk-backend.cabal+1
@@ -13,6 +13,7 @@ library
1313 data-default,
1414 dependent-sum,
1515 dependent-sum-template,
16+ filepath,
1617 lens,
1718 mtl,
1819 obelisk-asset-serve-snap,
lib/backend/src/Obelisk/Backend.hs+86−10
@@ -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
@@ -39,6 +40,7 @@ module Obelisk.Backend
3940 , getPublicConfigs
4041 ) where
4142
43+import Control.Exception (try)
4244import Control.Monad
4345import Control.Monad.Except
4446import Control.Monad.Fail (MonadFail)
@@ -46,6 +48,7 @@ import Data.ByteString (ByteString)
4648import qualified Data.ByteString.Char8 as BSC8
4749import Data.Default (Default (..))
4850import Data.Dependent.Sum
51+import Data.Either (isRight)
4952import Data.Functor.Identity
5053import Data.Map (Map)
5154import qualified Data.Map as Map
@@ -63,6 +66,7 @@ import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, htt
6366 , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
6467 , rqCookies, Cookie(..) , setHeader)
6568import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
69+import System.FilePath ((</>))
6670import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
6771
6872data Backend backendRoute frontendRoute = Backend
@@ -73,8 +77,9 @@ data Backend backendRoute frontendRoute = Backend
7377data BackendConfig frontendRoute = BackendConfig
7478 { _backendConfig_runSnap :: !(Snap () -> IO ()) -- ^ Function to run the snap server
7579 , _backendConfig_staticAssets :: !StaticAssets -- ^ Static assets
76− , _backendConfig_ghcjsWidgets :: !(GhcjsWidgets (Text -> FrontendWidgetT (R frontendRoute) ()))
77− -- ^ Given the URL of all.js, return the widgets which are responsible for
80+ , _backendConfig_ghcjsAssets :: !StaticAssets -- ^ Frontend ghcjs (and wasm) assets
81+ , _backendConfig_ghcjsWidgets :: !(GhcjsWidgets (GhcjsWasmAssets -> FrontendWidgetT (R frontendRoute) ()))
82+ -- ^ Given the URL of all.js and wasm files, return the widgets which are responsible for
7883 -- loading the script.
7984 } deriving (Generic)
8085
@@ -92,10 +97,14 @@ data GhcjsWidgets a = GhcjsWidgets
9297 -- ^ A script widget, placed in the document body
9398 } deriving (Functor, Generic)
9499
100+data GhcjsWasmAssets = GhcjsWasmAssets
101+ { _ghcjsWasmAssets_allJs :: Text
102+ , _ghcjsWasmAssets_wasmPathRoot :: Maybe Text
103+ }
95104
96105-- | Given the URL of all.js, return the widgets which are responsible for
97106-- loading the script. Defaults to 'preloadGhcjs' and 'deferredGhcjsScript'.
98−defaultGhcjsWidgets :: GhcjsWidgets (Text -> FrontendWidgetT r ())
107+defaultGhcjsWidgets :: GhcjsWidgets (GhcjsWasmAssets -> FrontendWidgetT r ())
99108defaultGhcjsWidgets = GhcjsWidgets
100109 { _ghcjsWidgets_preload = preloadGhcjs
101110 , _ghcjsWidgets_script = deferredGhcjsScript
@@ -132,6 +141,9 @@ defaultStaticAssets = StaticAssets
132141 , _staticAssets_unprocessed = "static"
133142 }
134143
144+-- This has both ghcjs and wasm assets.
145+-- The 'unprocessed' is a fallback path, and in case of wasm is invalid (does not contain wasm files)
146+-- For the standard obelisk deployment the unprocessed path would not be used.
135147defaultFrontendGhcjsAssets :: StaticAssets
136148defaultFrontendGhcjsAssets = StaticAssets
137149 { _staticAssets_processed = "frontend.jsexe.assets"
@@ -167,6 +179,10 @@ renderAllJsPath :: Encoder Identity Identity (R (FullRoute a b)) PageName -> Tex
167179renderAllJsPath validFullEncoder =
168180 renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ ["all.js"]
169181
182+renderWasmPathRoot :: Encoder Identity Identity (R (FullRoute a b)) PageName -> Text
183+renderWasmPathRoot validFullEncoder =
184+ renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_Ghcjs) :/ []
185+
170186serveObeliskApp
171187 :: (MonadSnap m, HasCookies m, MonadFail m)
172188 => (R appRoute -> Text)
@@ -221,7 +237,7 @@ serveGhcjsApp urlEnc ghcjsWidgets app config = \case
221237
222238-- | Default obelisk backend configuration.
223239defaultBackendConfig :: BackendConfig frontendRoute
224−defaultBackendConfig = BackendConfig runSnapWithCommandLineArgs defaultStaticAssets defaultGhcjsWidgets
240+defaultBackendConfig = BackendConfig runSnapWithCommandLineArgs defaultStaticAssets defaultFrontendGhcjsAssets defaultGhcjsWidgets
225241
226242-- | Run an obelisk backend with the default configuration.
227243runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
@@ -233,21 +249,34 @@ runBackendWith
233249 -> Backend backendRoute frontendRoute
234250 -> Frontend (R frontendRoute)
235251 -> IO ()
236−runBackendWith (BackendConfig runSnap staticAssets ghcjsWidgets) backend frontend = case checkEncoder $ _backend_routeEncoder backend of
252+runBackendWith (BackendConfig runSnap staticAssets ghcjsAssets ghcjsWidgets) backend frontend = case checkEncoder $ _backend_routeEncoder backend of
237253 Left e -> fail $ "backend error:\n" <> T.unpack e
238254 Right validFullEncoder -> do
239255 publicConfigs <- getPublicConfigs
256+ enableWasm <- checkWasmFile
240257 _backend_run backend $ \serveRoute ->
241258 runSnap $
242259 getRouteWith validFullEncoder >>= \case
243260 Identity r -> case r of
244261 FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
245262 FullRoute_Frontend obeliskRoute :/ a ->
246− serveDefaultObeliskApp routeToUrl (($ allJsUrl) <$> ghcjsWidgets) (serveStaticAssets staticAssets) frontend publicConfigs $
263+ serveObeliskApp routeToUrl widgets (serveStaticAssets staticAssets) frontendApp publicConfigs $
247264 obeliskRoute :/ a
248265 where
249266 routeToUrl (k :/ v) = renderObeliskRoute validFullEncoder $ FullRoute_Frontend (ObeliskRoute_App k) :/ v
250267 allJsUrl = renderAllJsPath validFullEncoder
268+ mWasmUrls = if enableWasm then Just (renderWasmPathRoot validFullEncoder) else Nothing
269+ widgets = ($ GhcjsWasmAssets allJsUrl mWasmUrls) <$> ghcjsWidgets
270+ frontendApp = GhcjsApp
271+ { _ghcjsApp_compiled = ghcjsAssets
272+ , _ghcjsApp_value = frontend
273+ }
274+ where
275+ checkWasmFile = do
276+ let base = _staticAssets_processed ghcjsAssets
277+ p = "frontend.wasm"
278+ assetType :: (Either IOError ByteString) <- liftIO $ try $ BSC8.readFile $ base </> p </> "type"
279+ pure $ isRight assetType
251280
252281renderGhcjsFrontend
253282 :: (MonadSnap m, HasCookies m)
@@ -263,13 +292,60 @@ renderGhcjsFrontend urlEnc ghcjsWidgets route configs f = do
263292
264293-- | Preload all.js in a link tag.
265294-- This is the default preload method.
266−preloadGhcjs :: Text -> FrontendWidgetT r ()
267−preloadGhcjs allJsUrl = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
295+preloadGhcjs :: GhcjsWasmAssets -> FrontendWidgetT r ()
296+preloadGhcjs (GhcjsWasmAssets allJsUrl mWasm) = case mWasm of
297+ Nothing -> elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: allJsUrl) blank
298+ Just wasmAssets -> fst (wasmScripts allJsUrl wasmAssets)
268299
269300-- | Load the script from the given URL in a deferred script tag.
270301-- This is the default method.
271−deferredGhcjsScript :: Text -> FrontendWidgetT r ()
272−deferredGhcjsScript allJsUrl = elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
302+deferredGhcjsScript :: GhcjsWasmAssets -> FrontendWidgetT r ()
303+deferredGhcjsScript (GhcjsWasmAssets allJsUrl mWasm) = case mWasm of
304+ Nothing -> elAttr "script" ("type" =: "text/javascript" <> "src" =: allJsUrl <> "defer" =: "defer") blank
305+ Just wasmAssets -> snd (wasmScripts allJsUrl wasmAssets)
306+
307+wasmScripts :: Text -> Text -> (FrontendWidgetT r (), FrontendWidgetT r ())
308+wasmScripts allJsUrl wasmRoot = (script preloadScript, script runJsScript)
309+ where
310+ script t = elAttr "script" ("type" =: "text/javascript") $ text t
311+ jsaddleJs = wasmRoot <> "/jsaddle_core.js"
312+ interfaceJs = wasmRoot <> "/jsaddle_mainthread_interface.js"
313+ runnerJs = wasmRoot <> "/mainthread_runner.js"
314+ wasmUrl = wasmRoot <> "/frontend.wasm"
315+
316+ preloadScript =
317+ "add_preload_tag = function (docSrc, docType) { \
318+ \ var link_tag = document.createElement('link'); \
319+ \ link_tag.rel = 'preload'; \
320+ \ link_tag.as = docType; \
321+ \ link_tag.href = docSrc; \
322+ \ document.head.appendChild(link_tag); \
323+ \}; \
324+ \if (typeof(WebAssembly) === 'undefined') { \
325+ \ add_preload_tag('" <> allJsUrl <> "', 'script'); \
326+ \} else { \
327+ \ add_preload_tag('" <> wasmUrl <> "', 'script'); \
328+ \ add_preload_tag('" <> jsaddleJs <> "', 'script'); \
329+ \ add_preload_tag('" <> interfaceJs <> "', 'script'); \
330+ \ add_preload_tag('" <> runnerJs <> "', 'script'); \
331+ \}"
332+
333+ runJsScript =
334+ "add_deferload_tag = function (docSrc) { \
335+ \ var tag = document.createElement('script'); \
336+ \ tag.type = 'text/javascript'; \
337+ \ tag.src = docSrc; \
338+ \ tag.setAttribute = ('defer', 'defer'); \
339+ \ document.body.appendChild(tag); \
340+ \}; \
341+ \if (typeof(WebAssembly) === 'undefined') { \
342+ \ add_deferload_tag('" <> allJsUrl <> "'); \
343+ \} else { \
344+ \ var wasmFile = '" <> wasmUrl <> "'; \
345+ \ add_deferload_tag('" <> jsaddleJs <> "'); \
346+ \ add_deferload_tag('" <> interfaceJs <> "'); \
347+ \ add_deferload_tag('" <> runnerJs <> "'); \
348+ \}"
273349
274350-- | An all.js script which is loaded after waiting for some time to pass. This
275351-- 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 }
skeleton/frontend/frontend.cabal+2−1
@@ -29,6 +29,7 @@ executable frontend
2929 , obelisk-generated-static
3030 , frontend
3131 --TODO: Make these ghc-options optional
32− ghc-options: -threaded
32+ if !arch(wasm32)
33+ ghc-options: -threaded
3334 if os(darwin)
3435 ghc-options: -dynamic