on master help sign in
Merge branch 'develop' into tar-in-backend-path-2
af8f85eRyan Trinkle
.gitignore+1−3
@@ -4,12 +4,10 @@
4result
4result
5*.o
5*.o
6*.hi
6*.hi
7−
8TAGS
7TAGS
9tags
8tags
10ctags
9ctags
11dist-newstyle
10dist-newstyle
12cabal.project.local
11cabal.project.local
13.ghc.environment.*
12.ghc.environment.*
14−
15−**/ghcid-output.txt
13+**/ghcid-output.txt
CONTRIBUTING.md+16
@@ -5,6 +5,7 @@ Contributions and issue reports are encouraged and appreciated!
5- [Opening Issues](#opening-issues)
5- [Opening Issues](#opening-issues)
6- [Submitting Changes](#submitting-changes)
6- [Submitting Changes](#submitting-changes)
7 - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
7 - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
8 - [Code Quality](#code-quality)
9 - [Code Quality](#code-quality)
9 - [Documentation](#documentation)
10 - [Documentation](#documentation)
10
11
@@ -28,9 +29,16 @@ The summary line of your commit message should summarize the changes being made.
28One way to think about it is that your commit message should be able to complete the sentence:
29One way to think about it is that your commit message should be able to complete the sentence:
29"When applied, this commit will..."
30"When applied, this commit will..."
30
31
32+##### Note on bumping dependencies
33+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
34+
31#### Body
35#### Body
32For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
36For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
33
37
38+### Guidelines for Pull Requests
39+
40+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
41+
34### Code Quality
42### Code Quality
35
43
36#### Warnings
44#### Warnings
@@ -46,6 +54,14 @@ You can run the tests like this:
46$(nix-build -A selftest --no-out-link)
54$(nix-build -A selftest --no-out-link)
47```
55```
48
56
57+To test that your changes build across platforms, you can also try to build release.nix, like this:
58+```bash
59+nix-build release.nix
60+```
61+
62+Note, however, that to build release.nix you must accept the android license agreement and your machine must be configured to build both ios and android executables (usually via remote builders).
63+
64+
49### Documentation
65### Documentation
50
66
51#### In the code
67#### In the code
ChangeLog.md+22
@@ -2,6 +2,28 @@
2
2
3This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
3This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
4
4
5+## Unreleased
6+* Configs become ByteStrings.
7+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
8+ * The frontend will still only have access to configs that are placed in config/frontend and config/common, while the backend has access to the entire contents of the config directory via `Obelisk.ExecutableConfig.Lookup.getConfigs`.
9+* The backend no longer runs in BackendConfigsT.
10+* Add tabulation package. See Data.Tabulation for details.
11+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
12+* Use IP address for nginx proxy pass instead of localhost
13+
14+## v0.1.1.0 - 2019-05-17
15+
16+* Fix crashes of Android apps on 32-bit ARM devices.
17+* Provide a way to indicate acceptance of the Android SDK license by passing `config.android_sdk.accept_license = true;` in the arguments to the import of `.obelisk/impl` in the project's `default.nix`.
18+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
19+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
20+* Add the `ob shell` command to enter a nix shell for an obelisk project
21+* Allow skeleton's obelisk to be overridden. This changes the skeleton's default.nix interface: the arguments that it used to take are now part of the new "obelisk" argument.
22+* Removed `MonadIO` from `ObeliskWidget` to prevent accidental IO during prerendering. If you need to do IO in a widget it should be on the right hand side of a `prerender`.
23+* Significantly changed the interface to the "executable config" packages. `obelisk-executable-config-lookup` is a new internal package which looks up all configs in a platform-specific way. `obelisk-executable-frontend` and `obelisk-executable-backend` provide MTL-style monad classes (`HasFrontendConfigs` and `HasBackendConfigs`) which the frontend and backend, respectively, can use to look up configs. This replaces the old `get` function which ran in `IO`.
24+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
25+
26+
5## v0.1.0.0 - 2019-03-29
27## v0.1.0.0 - 2019-03-29
6
28
7* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
29* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
FAQ.md+76
@@ -2,6 +2,8 @@
2
2
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
41. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
41. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
5+1. [How do I fix `Ambiguous module name` errors?](#how-do-i-fix-ambiguous-module-name-errors)
6+1. [What does `ob run` actually do?](#what-does-ob-run-actually-do)
5
7
6### How do I fix invalid entitlements?
8### How do I fix invalid entitlements?
7
9
@@ -44,3 +46,77 @@ ob thunk pack $SOME_THUNK
44Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
46Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
45
47
46(Based on issue #351).
48(Based on issue #351).
49+
50+### How do I fix `Ambiguous module name` errors?
51+Since obelisk places the common/backend/frontend modules packages into the same ghci, and ghci doesn't "sandbox" them, it is possible to have conflicting module errors inside `ob repl/watch/run` that do not appear when doing a cabal build.
52+
53+You can disambiguate this via [PackageImports](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#package-qualified-imports). For instance, if you see
54+```
55+error:
56+ Ambiguous module name ‘Crypto.Hash’:
57+ it was found in multiple packages:
58+ cryptohash-0.11.9 cryptonite-0.25
59+```
60+then specify the package you want in the import, e.g:
61+`import "cryptonite" Crypto.Hash`
62+
63+
64+### What does `ob run` actually do?
65+
66+
67+#### Short version:
68+
69+`ob run` starts a [`ghcid`](https://github.com/ndmitchell/ghcid) process which
70+tries to build your project within a carefully crafted `nix-shell` with all the
71+project's dependencies and,
72+
73+* either displays compilation errors/warnings,
74+* or starts the Obelisk server, which serves:
75+ * your backend's route handlers,
76+ * the static assets,
77+ * the [JSaddle](https://github.com/ghcjs/jsaddle) frontend code (while opening its websocket).
78+
79+#### Longer version:
80+
81+Assuming we are in a project created with `ob init`, `ob run` calls (see
82+`lib/command/src/Obelisk/Command.hs`):
83+
84+ nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
85+
86+where
87+
88+* `shells.ghc` is defined in `./default.nix` by importing `./.obelisk/impl/default.nix` which
89+ is `default.nix` in the present (Obelisk) repository,
90+* `run-static-io` is a logging-enabled command wrapper
91+ (cf. `runObelisk` in `lib/command/src/Obelisk/App.hs`).
92+
93+In this case, it runs the function `Obelisk.Command.Command.run` (defined in
94+`lib/command/src/Obelisk/Command/Run.hs`).
95+
96+* It creates a GHCi config from a Nix expression which:
97+ * loads three packages: `backend`, `common`, `frontend`,
98+ * obtains a free port number.
99+* Then runs `ghcid`
100+ * with a command that reruns `Obelisk.Run.run` at each restart (option
101+ `--test`).
102+
103+It is defined in `lib/run/src/Obelisk/Run.hs`:
104+
105+* It creates a thread which starts the main backend.
106+ This runs `runSnapWithCommandLineArgs` and passes routes:
107+ * to the backend (result of the `_backend_run` field of your
108+ implementation of the `Backend fullRoute frontendRoute` record),
109+ * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110+ serve static assets.
111+* Starts `runWidget`
112+ which itself runs
113+ [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114+ (the *“TCP listen loop”*):
115+ * it binds to TCP socket, and
116+ * creates the HTTP connection manager
117+ ( [`newManager`](https://hackage.haskell.org/package/http-client-0.6.3/docs/Network-HTTP-Client.html#v:newManager))
118+ * calls `obeliskApp` which
119+ * has a route to serve the [JSaddle](https://github.com/ghcjs/jsaddle) frontend,
120+ * runs the frontend (*on* the server), to produce pre-rendered pages,
121+ * starts the JSaddle web-socket,
122+ * “falls back” to proxying the backend (`fallbackProxy`).
README.md+8−1
@@ -5,6 +5,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
5- [Installing Obelisk](#installing-obelisk)
5- [Installing Obelisk](#installing-obelisk)
6- [Developing an Obelisk project](#developing-an-obelisk-project)
6- [Developing an Obelisk project](#developing-an-obelisk-project)
7 - [Hoogle](#hoogle)
7 - [Hoogle](#hoogle)
8+ - [Adding Packages](#adding-packages)
8 - [Adding Package Overrides](#adding-package-overrides)
9 - [Adding Package Overrides](#adding-package-overrides)
9- [Deploying](#deploying)
10- [Deploying](#deploying)
10 - [Locally](#locally)
11 - [Locally](#locally)
@@ -24,6 +25,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
24 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
25 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
25 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
26 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
26 ```
27 ```
28+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
27 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
29 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
28 ```
30 ```
29 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
31 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
@@ -81,7 +83,7 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
81ob run
83ob run
82```
84```
83
85
84−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
86+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
85
87
86Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
88Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
87
89
@@ -91,6 +93,10 @@ To enter a nix-shell from which you can run the hoogle command-line client or a
91
93
92`nix-shell -A shells.ghc --arg withHoogle true`
94`nix-shell -A shells.ghc --arg withHoogle true`
93
95
96+### Adding packages
97+
98+In order to add package dependencies, declare them under the build-depends field in the appropriate cabal files (backend, common, and frontend each have their own). The corresponding Nix packages will automatically be selected when building.
99+
94### Adding package overrides
100### Adding package overrides
95
101
96To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
102To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
@@ -272,6 +278,7 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
272NOTE: Currently Android builds are only supported on Linux.
278NOTE: Currently Android builds are only supported on Linux.
273
279
2741. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
2801. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
281+1. In your project's `default.nix` pass `config.android_sdk.accept_license = true;` in the arguments to the import of of `.obelisk/impl` to indicate your acceptance of the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms), which is required to build Android apps.
2751. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2821. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2761. A debug version of the app should be generated at `result-android/android-app-debug.apk`
2831. A debug version of the app should be generated at `result-android/android-app-debug.apk`
277
284
default.nix+13−18
@@ -1,6 +1,7 @@
1{ system ? builtins.currentSystem
1{ system ? builtins.currentSystem
2, profiling ? false
2, profiling ? false
3, iosSdkVersion ? "10.2"
3, iosSdkVersion ? "10.2"
4+, config ? {}
4}:
5}:
5let
6let
6 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
7 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,7 +17,7 @@ let
16 ];
17 ];
17
18
18 getReflexPlatform = sys: import ./dep/reflex-platform {
19 getReflexPlatform = sys: import ./dep/reflex-platform {
19− inherit iosSdkVersion;
20+ inherit iosSdkVersion config;
20 system = sys;
21 system = sys;
21 enableLibraryProfiling = profiling;
22 enableLibraryProfiling = profiling;
22
23
@@ -38,21 +39,14 @@ let
38 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
39 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
39 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
40 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
40 }) {});
41 }) {});
41− # Need 8.0.2 build support
42− # PR: https://github.com/dmwit/universe/pull/33
43− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
44− owner = "obsidiansystems";
45− repo = "universe";
46− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
47− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
48− } + /template) {};
49 })
42 })
50
43
44+ pkgs.obeliskExecutableConfig.haskellOverlay
45+
51 # Add obelisk packages
46 # Add obelisk packages
52 (self: super: let
47 (self: super: let
53 pkgs = self.callPackage ({ pkgs }: pkgs) {};
48 pkgs = self.callPackage ({ pkgs }: pkgs) {};
54 in {
49 in {
55− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
56 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
50 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
57
51
58 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
52 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
@@ -65,6 +59,7 @@ let
65 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
59 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
66 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
60 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
67 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
61 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
62+ tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
68 })
63 })
69
64
70 (self: super: let
65 (self: super: let
@@ -203,7 +198,7 @@ in rec {
203 enableACME = enableHttps;
198 enableACME = enableHttps;
204 forceSSL = enableHttps;
199 forceSSL = enableHttps;
205 locations.${baseUrl} = {
200 locations.${baseUrl} = {
206− proxyPass = "http://localhost:" + toString internalPort;
201+ proxyPass = "http://127.0.0.1:" + toString internalPort;
207 proxyWebsockets = true;
202 proxyWebsockets = true;
208 };
203 };
209 };
204 };
@@ -299,21 +294,21 @@ in rec {
299 injectableConfig = builtins.filterSource (path: _:
294 injectableConfig = builtins.filterSource (path: _:
300 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
295 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
301 );
296 );
302− __android = configPath: {
297+ __androidWithConfig = configPath: {
303 ${if android == null then null else frontendName} = {
298 ${if android == null then null else frontendName} = {
304 executableName = "frontend";
299 executableName = "frontend";
305 ${if builtins.pathExists staticFiles then "assets" else null} =
300 ${if builtins.pathExists staticFiles then "assets" else null} =
306 nixpkgs.obeliskExecutableConfig.platforms.android.inject
301 nixpkgs.obeliskExecutableConfig.platforms.android.inject
307− (if configPath == null then null else injectableConfig configPath)
302+ (injectableConfig configPath)
308 processedStatic.symlinked;
303 processedStatic.symlinked;
309 } // android;
304 } // android;
310 };
305 };
311− __ios = configPath: {
306+ __iosWithConfig = configPath: {
312 ${if ios == null then null else frontendName} = {
307 ${if ios == null then null else frontendName} = {
313 executableName = "frontend";
308 executableName = "frontend";
314 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
309 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
315 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
310 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
316− (if configPath == null then null else injectableConfig configPath)
311+ (injectableConfig configPath)
317 processedStatic.symlinked;
312 processedStatic.symlinked;
318 } // ios;
313 } // ios;
319 };
314 };
@@ -336,9 +331,9 @@ in rec {
336 commonName
331 commonName
337 ];
332 ];
338 };
333 };
339− android = __android null;
334+ android = __androidWithConfig (base + "/config");
340− ios = __ios null;
335+ ios = __iosWithConfig (base + "/config");
341− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
336+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
342 };
337 };
343 in mkProject (projectDefinition args));
338 in mkProject (projectDefinition args));
344 serverOn = sys: version: serverExe
339 serverOn = sys: version: serverExe
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
1{
1{
2 "owner": "reflex-frp",
2 "owner": "reflex-frp",
3 "repo": "reflex-platform",
3 "repo": "reflex-platform",
4− "branch": "develop",
4+ "branch": "cg-bump-reflex-dom",
5− "rev": "f86e8363ab1a5068b8419a61856e1aefea952bb0",
5+ "rev": "7ecb4c255bb053a7a4796ffd6331cdbe210de91c",
6− "sha256": "1f5qhrkj9h07j5xzw8s10x0r7b19vbh8j9z9lbzy23yxwmj78y7f"
6+ "sha256": "1mngxa24cfpvxxq4hgh77nw36vny4abl5wi2xmlwpkk25wzm0h0x"
7}
7}
lib/asset/manifest/obelisk-asset-manifest.cabal+1
@@ -17,6 +17,7 @@ library
17 base
17 base
18 , bytestring
18 , bytestring
19 , containers
19 , containers
20+ , deepseq
20 , directory
21 , directory
21 , SHA
22 , SHA
22 , filepath
23 , filepath
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2
@@ -1,10 +1,12 @@
1{-# LANGUAGE OverloadedStrings #-}
1{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
2-- | Functions for collecting up files to be processed by the asset pipeline
3-- | Functions for collecting up files to be processed by the asset pipeline
3module Obelisk.Asset.Gather
4module Obelisk.Asset.Gather
4 ( gatherHashedPaths
5 ( gatherHashedPaths
5 , toHashedPath
6 , toHashedPath
6 ) where
7 ) where
7
8
9+import Control.DeepSeq
8import Control.Monad (forM)
10import Control.Monad (forM)
9import Data.Bits
11import Data.Bits
10import qualified Data.ByteString.Lazy as LBS
12import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +35,7 @@ gatherHashedPaths root = go ""
33 let relativePath = subdir </> sub
35 let relativePath = subdir </> sub
34 isFile <- doesFileExist $ root </> relativePath
36 isFile <- doesFileExist $ root </> relativePath
35 if isFile
37 if isFile
36− then do hashedRelativePath <- toHashedPath root relativePath
38+ then do !hashedRelativePath <- force <$> toHashedPath root relativePath
37 return $ Map.singleton relativePath hashedRelativePath
39 return $ Map.singleton relativePath hashedRelativePath
38 else go relativePath
40 else go relativePath
39
41
@@ -47,7 +49,8 @@ toHashedPath root relativePath = do
47 contents <- LBS.readFile path
49 contents <- LBS.readFile path
48 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
50 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
49 (dir, filename) = splitFileName relativePath
51 (dir, filename) = splitFileName relativePath
50− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
51
54
52-- | Convert a ByteString to base 32 in the way that Nix does
55-- | Convert a ByteString to base 32 in the way that Nix does
53toNixBase32 :: LBS.ByteString -> LBS.ByteString
56toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/backend/obelisk-backend.cabal+3
@@ -8,6 +8,7 @@ library
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10 categories,
10 categories,
11+ containers,
11 cookie,
12 cookie,
12 data-default,
13 data-default,
13 dependent-sum,
14 dependent-sum,
@@ -15,6 +16,8 @@ library
15 lens,
16 lens,
16 mtl,
17 mtl,
17 obelisk-asset-serve-snap,
18 obelisk-asset-serve-snap,
19+ obelisk-executable-config-inject,
20+ obelisk-executable-config-lookup,
18 obelisk-frontend,
21 obelisk-frontend,
19 obelisk-route,
22 obelisk-route,
20 obelisk-snap-extras,
23 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+36−20
@@ -19,6 +19,7 @@ module Obelisk.Backend
19 , runBackend
19 , runBackend
20 , staticRenderContentType
20 , staticRenderContentType
21 , mkRouteToUrl
21 , mkRouteToUrl
22+ , getPublicConfigs
22 ) where
23 ) where
23
24
24import Prelude hiding (id, (.))
25import Prelude hiding (id, (.))
@@ -32,11 +33,14 @@ import Data.Default (Default (..))
32import Data.Dependent.Sum
33import Data.Dependent.Sum
33import Data.Functor.Sum
34import Data.Functor.Sum
34import Data.Functor.Identity
35import Data.Functor.Identity
36+import Data.Map (Map)
37+import qualified Data.Map as Map
35import Data.Monoid ((<>))
38import Data.Monoid ((<>))
36import Data.Text (Text)
39import Data.Text (Text)
37import qualified Data.Text as T
40import qualified Data.Text as T
38import Data.Text.Encoding
41import Data.Text.Encoding
39import Obelisk.Asset.Serve.Snap (serveAsset)
42import Obelisk.Asset.Serve.Snap (serveAsset)
43+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
40import Obelisk.Frontend
44import Obelisk.Frontend
41import Obelisk.Route
45import Obelisk.Route
42import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
46import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
@@ -67,9 +71,10 @@ serveDefaultObeliskApp
67 -> ([Text]
71 -> ([Text]
68 -> m ())
72 -> m ())
69 -> Frontend (R appRoute)
73 -> Frontend (R appRoute)
74+ -> Map Text ByteString
70 -> R (ObeliskRoute appRoute)
75 -> R (ObeliskRoute appRoute)
71 -> m ()
76 -> m ()
72−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
77+serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
73 where frontendApp = GhcjsApp
78 where frontendApp = GhcjsApp
74 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
79 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
75 , _ghcjsApp_value = frontend
80 , _ghcjsApp_value = frontend
@@ -94,16 +99,16 @@ defaultFrontendGhcjsAssets = StaticAssets
94 , _staticAssets_unprocessed = "frontend.jsexe"
99 , _staticAssets_unprocessed = "frontend.jsexe"
95 }
100 }
96
101
97−runSnapWithCommandLineArgs :: Snap () -> IO ()
102+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
98runSnapWithCommandLineArgs a = do
103runSnapWithCommandLineArgs a = do
99 -- Get the web server configuration from the command line
104 -- Get the web server configuration from the command line
100− cmdLineConf <- commandLineConfig defaultConfig
105+ cmdLineConf <- liftIO $ commandLineConfig defaultConfig
101 let httpConf = cmdLineConf
106 let httpConf = cmdLineConf
102 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
107 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
103 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
108 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
104 }
109 }
105 -- Start the web server
110 -- Start the web server
106− httpServe httpConf a
111+ liftIO $ httpServe httpConf a
107
112
108getPageName :: (MonadSnap m) => m PageName
113getPageName :: (MonadSnap m) => m PageName
109getPageName = do
114getPageName = do
@@ -125,13 +130,14 @@ serveObeliskApp
125 => (R appRoute -> Text)
130 => (R appRoute -> Text)
126 -> ([Text] -> m ())
131 -> ([Text] -> m ())
127 -> GhcjsApp (R appRoute)
132 -> GhcjsApp (R appRoute)
133+ -> Map Text ByteString
128 -> R (ObeliskRoute appRoute)
134 -> R (ObeliskRoute appRoute)
129 -> m ()
135 -> m ()
130−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
136+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
131− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
137+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
132 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
138 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
133 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
139 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
134− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
140+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
135 ResourceRoute_JSaddleWarp :=> Identity _ -> do
141 ResourceRoute_JSaddleWarp :=> Identity _ -> do
136 let msg = "Error: Obelisk.Backend received jsaddle request"
142 let msg = "Error: Obelisk.Backend received jsaddle request"
137 liftIO $ putStrLn $ T.unpack msg
143 liftIO $ putStrLn $ T.unpack msg
@@ -159,24 +165,28 @@ serveGhcjsApp
159 :: (MonadSnap m, HasCookies m)
165 :: (MonadSnap m, HasCookies m)
160 => (R appRouteComponent -> Text)
166 => (R appRouteComponent -> Text)
161 -> GhcjsApp (R appRouteComponent)
167 -> GhcjsApp (R appRouteComponent)
168+ -> Map Text ByteString
162 -> R (GhcjsAppRoute appRouteComponent)
169 -> R (GhcjsAppRoute appRouteComponent)
163 -> m ()
170 -> m ()
164−serveGhcjsApp urlEnc app = \case
171+serveGhcjsApp urlEnc app config = \case
165 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
172 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
166 modifyResponse $ setContentType staticRenderContentType
173 modifyResponse $ setContentType staticRenderContentType
167− writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
174+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
168 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
175 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
169
176
170runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
177runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
171runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
178runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
172 Left e -> fail $ "backend error:\n" <> T.unpack e
179 Left e -> fail $ "backend error:\n" <> T.unpack e
180+ Right validFullEncoder -> do
181+ publicConfigs <- getPublicConfigs
173− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
182+ _backend_run backend $ \serveRoute -> do
174− runSnapWithCommandLineArgs $ do
183+ runSnapWithCommandLineArgs $ do
175− getRouteWith validFullEncoder >>= \case
184+ getRouteWith validFullEncoder >>= \case
176− Identity r -> case r of
185+ Identity r -> case r of
177− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
186+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
178− InR obeliskRoute :=> Identity a ->
187+ InR obeliskRoute :=> Identity a ->
179− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
188+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189+ obeliskRoute :/ a
180
190
181mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
191mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
182mkRouteToUrl validFullEncoder =
192mkRouteToUrl validFullEncoder =
@@ -187,15 +197,21 @@ renderGhcjsFrontend
187 :: (MonadSnap m, HasCookies m)
197 :: (MonadSnap m, HasCookies m)
188 => (route -> Text)
198 => (route -> Text)
189 -> route
199 -> route
200+ -> Map Text ByteString
190 -> Frontend route
201 -> Frontend route
191 -> m ByteString
202 -> m ByteString
192−renderGhcjsFrontend urlEnc route f = do
203+renderGhcjsFrontend urlEnc route configs f = do
193 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
204 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
194 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
205 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
195 cookies <- askCookies
206 cookies <- askCookies
196− renderFrontendHtml cookies urlEnc route
207+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
197− (_frontend_head f >> ghcjsPreload)
198− (_frontend_body f >> ghcjsScript)
199
208
200instance HasCookies Snap where
209instance HasCookies Snap where
201 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
210 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
211+
212+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
213+-- to frontend applications, and hence visible to end users).
214+getPublicConfigs :: IO (Map Text ByteString)
215+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
216+ where
217+ isMemberOf k = any (`T.isPrefixOf` k)
lib/command/src/Obelisk/Command.hs+27−5
@@ -92,6 +92,7 @@ data ObCommand
92 | ObCommand_Thunk ThunkCommand
92 | ObCommand_Thunk ThunkCommand
93 | ObCommand_Repl
93 | ObCommand_Repl
94 | ObCommand_Watch
94 | ObCommand_Watch
95+ | ObCommand_Shell ShellOpts
95 | ObCommand_Internal ObInternal
96 | ObCommand_Internal ObInternal
96 deriving Show
97 deriving Show
97
98
@@ -103,7 +104,7 @@ data ObInternal
103inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
104inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
104inNixShell' p = withProjectRoot "." $ \root -> do
105inNixShell' p = withProjectRoot "." $ \root -> do
105 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
106 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
106− projectShell root False "ghc" cmd
107+ projectShell root False "ghc" (Just cmd)
107 where
108 where
108 mkCmd = do
109 mkCmd = do
109 argsCfg <- getArgsConfig
110 argsCfg <- getArgsConfig
@@ -127,6 +128,7 @@ obCommand cfg = hsubparser
127 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
128 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
128 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
129 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
129 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
130 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
131+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
130 ])
132 ])
131 <|> subparser
133 <|> subparser
132 (mconcat
134 (mconcat
@@ -211,16 +213,34 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
211data ThunkCommand
213data ThunkCommand
212 = ThunkCommand_Update [FilePath] (Maybe String)
214 = ThunkCommand_Update [FilePath] (Maybe String)
213 | ThunkCommand_Unpack [FilePath]
215 | ThunkCommand_Unpack [FilePath]
214− | ThunkCommand_Pack [FilePath]
216+ | ThunkCommand_Pack [FilePath] Bool
215 deriving Show
217 deriving Show
216
218
219+forceFlag :: Parser Bool
220+forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
221+
217thunkCommand :: Parser ThunkCommand
222thunkCommand :: Parser ThunkCommand
218thunkCommand = hsubparser $ mconcat
223thunkCommand = hsubparser $ mconcat
219 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
224 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
220 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
225 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
221− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
226+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
222 ]
227 ]
223
228
229+data ShellOpts
230+ = ShellOpts
231+ { _shellOpts_shell :: String
232+ , _shellOpts_command :: Maybe String
233+ }
234+ deriving Show
235+
236+shellOpts :: Parser ShellOpts
237+shellOpts = ShellOpts
238+ <$> ( flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
239+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
240+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
241+ )
242+ <*> optional (strArgument (metavar "COMMAND"))
243+
224parserPrefs :: ParserPrefs
244parserPrefs :: ParserPrefs
225parserPrefs = defaultPrefs
245parserPrefs = defaultPrefs
226 { prefShowHelpOnEmpty = True
246 { prefShowHelpOnEmpty = True
@@ -325,7 +345,7 @@ ob = \case
325 Right (ThunkData_Packed ptr) -> return ptr
345 Right (ThunkData_Packed ptr) -> return ptr
326 Right (ThunkData_Checkout (Just ptr)) -> return ptr
346 Right (ThunkData_Checkout (Just ptr)) -> return ptr
327 Right (ThunkData_Checkout Nothing) ->
347 Right (ThunkData_Checkout Nothing) ->
328− getThunkPtr' False root
348+ getThunkPtr False root
329 let sshKeyPath = _deployInitOpts_sshKey deployOpts
349 let sshKeyPath = _deployInitOpts_sshKey deployOpts
330 hostname = _deployInitOpts_hostname deployOpts
350 hostname = _deployInitOpts_hostname deployOpts
331 route = _deployInitOpts_route deployOpts
351 route = _deployInitOpts_route deployOpts
@@ -344,9 +364,11 @@ ob = \case
344 ObCommand_Thunk tc -> case tc of
364 ObCommand_Thunk tc -> case tc of
345 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
365 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
346 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
366 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
347− ThunkCommand_Pack thunks -> forM_ thunks packThunk
367+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
348 ObCommand_Repl -> runRepl
368 ObCommand_Repl -> runRepl
349 ObCommand_Watch -> inNixShell' $ static runWatch
369 ObCommand_Watch -> inNixShell' $ static runWatch
370+ ObCommand_Shell so -> withProjectRoot "." $ \root ->
371+ projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
350 ObCommand_Internal icmd -> case icmd of
372 ObCommand_Internal icmd -> case icmd of
351 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
373 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
352 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
374 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
lib/command/src/Obelisk/Command/Deploy.hs+3−3
@@ -108,7 +108,7 @@ deployPush deployPath getNixBuilders = do
108 Right (ThunkData_Packed ptr) -> return ptr
108 Right (ThunkData_Packed ptr) -> return ptr
109 Right (ThunkData_Checkout _) -> do
109 Right (ThunkData_Checkout _) -> do
110 checkGitCleanStatus srcPath True >>= \case
110 checkGitCleanStatus srcPath True >>= \case
111− True -> packThunk srcPath
111+ True -> packThunk False srcPath
112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -234,7 +234,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
234 [ "with (import ", srcDir, " {});"
234 [ "with (import ", srcDir, " {});"
235 , "android.frontend.override (drv: { "
235 , "android.frontend.override (drv: { "
236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
237− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
237+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
238 , "})"
238 , "})"
239 ]
239 ]
240 return $ Target
240 return $ Target
@@ -245,7 +245,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
245 IOS -> do
245 IOS -> do
246 let expr = mconcat
246 let expr = mconcat
247 [ "with (import ", srcDir, " {});"
247 [ "with (import ", srcDir, " {});"
248− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
249 ]
249 ]
250 return $ Target
250 return $ Target
251 { _target_path = Nothing
251 { _target_path = Nothing
lib/command/src/Obelisk/Command/Project.hs+6−5
@@ -196,20 +196,21 @@ isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
196-- | Run a command in the given shell for the current project
196-- | Run a command in the given shell for the current project
197inProjectShell :: MonadObelisk m => String -> String -> m ()
197inProjectShell :: MonadObelisk m => String -> String -> m ()
198inProjectShell shellName command = withProjectRoot "." $ \root ->
198inProjectShell shellName command = withProjectRoot "." $ \root ->
199− projectShell root True shellName command
199+ projectShell root True shellName (Just command)
200
200
201inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
201inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
202inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
202inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
203− projectShell root False shellName command
203+ projectShell root False shellName (Just command)
204
204
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
205+projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206projectShell root isPure shellName command = do
206projectShell root isPure shellName command = do
207 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
207 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208 [ "--pure" | isPure ] <>
208 [ "--pure" | isPure ] <>
209 [ "-A"
209 [ "-A"
210 , "shells." <> shellName
210 , "shells." <> shellName
211− , "--run", command
211+ ] <> case command of
212− ]
212+ Nothing -> []
213+ Just c -> ["--run", c]
213 void $ liftIO $ waitForProcess ph
214 void $ liftIO $ waitForProcess ph
214
215
215setCtlc :: CreateProcess -> CreateProcess
216setCtlc :: CreateProcess -> CreateProcess
lib/command/src/Obelisk/Command/Run.hs+19−3
@@ -10,8 +10,8 @@ import Control.Monad
10import Control.Monad.IO.Class (liftIO)
10import Control.Monad.IO.Class (liftIO)
11import Control.Monad.Reader (MonadIO)
11import Control.Monad.Reader (MonadIO)
12import Data.Either
12import Data.Either
13−import Data.List
13+import qualified Data.List as L
14−import Data.List.NonEmpty as NE
14+import qualified Data.List.NonEmpty as NE
15import Data.Maybe
15import Data.Maybe
16import qualified Data.Text as T
16import qualified Data.Text as T
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
@@ -46,6 +46,8 @@ data CabalPackageInfo = CabalPackageInfo
46 -- ^ List of hs src dirs of the library component
46 -- ^ List of hs src dirs of the library component
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
48 -- ^ List of globally enable extensions of the library component
48 -- ^ List of globally enable extensions of the library component
49+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
50+ -- ^ List of globally set languages of the library component
49 }
51 }
50
52
51-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
53-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -71,7 +73,10 @@ run = do
71 -- Check whether the impure static files are a derivation (and so must be built)
73 -- Check whether the impure static files are a derivation (and so must be built)
72 if isDerivation == "1"
74 if isDerivation == "1"
73 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
75 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
76+ proc "nix-build"
77+ [ "--no-out-link"
74− proc "nix-build" ["-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"]
78+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
79+ ]
75 else readProcessAndLogStderr Debug $
80 else readProcessAndLogStderr Debug $
76 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
81 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
77 putLog Debug $ "Assets impurely loaded from: " <> assets
82 putLog Debug $ "Assets impurely loaded from: " <> assets
@@ -136,6 +141,8 @@ parseCabalPackage dir = do
136 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
141 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
137 , _cabalPackageInfo_defaultExtensions =
142 , _cabalPackageInfo_defaultExtensions =
138 defaultExtensions $ libBuildInfo lib
143 defaultExtensions $ libBuildInfo lib
144+ , _cabalPackageInfo_defaultLanguage =
145+ defaultLanguage $ libBuildInfo lib
139 }
146 }
140 Left (_, errors) -> do
147 Left (_, errors) -> do
141 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
148 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -165,17 +172,22 @@ withGhciScript pkgs f = do
165 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
172 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
166
173
167 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
174 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
175+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
176+ -- NOTE when no default-language is present cabal sets Haskell98
177+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
168 extensionsLine = if extensions == mempty
178 extensionsLine = if extensions == mempty
169 then ""
179 then ""
170 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
180 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
171 dotGhci = unlines $
181 dotGhci = unlines $
172 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
182 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
173 , extensionsLine
183 , extensionsLine
184+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
174 , ":load Backend Frontend"
185 , ":load Backend Frontend"
175 , "import Obelisk.Run"
186 , "import Obelisk.Run"
176 , "import qualified Frontend"
187 , "import qualified Frontend"
177 , "import qualified Backend"
188 , "import qualified Backend"
178 ]
189 ]
190+ warnDifferentLanguages language
179 withSystemTempDirectory "ob-ghci" $ \fp -> do
191 withSystemTempDirectory "ob-ghci" $ \fp -> do
180 let dotGhciPath = fp </> ".ghci"
192 let dotGhciPath = fp </> ".ghci"
181 liftIO $ writeFile dotGhciPath dotGhci
193 liftIO $ writeFile dotGhciPath dotGhci
@@ -185,6 +197,10 @@ withGhciScript pkgs f = do
185 rootedSourceDirs pkg = NE.toList $
197 rootedSourceDirs pkg = NE.toList $
186 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
198 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
187
199
200+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
201+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
202+warnDifferentLanguages _ = return ()
203+
188-- | Run ghci repl
204-- | Run ghci repl
189runGhciRepl
205runGhciRepl
190 :: MonadObelisk m
206 :: MonadObelisk m
lib/command/src/Obelisk/Command/Thunk.hs+7−12
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
26 , readThunk
26 , readThunk
27 , updateThunk
27 , updateThunk
28 , getThunkPtr
28 , getThunkPtr
29− , getThunkPtr'
30 , parseGitUri
29 , parseGitUri
31 , uriThunkPtr
30 , uriThunkPtr
32 ) where
31 ) where
@@ -573,7 +572,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
573 return tmpThunk
572 return tmpThunk
574 Right _ -> failWith $ "Thunk is not packed"
573 Right _ -> failWith $ "Thunk is not packed"
575 updateThunkFromTmp p' = do
574 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
575+ _ <- packThunk' True False p'
577 callProcessAndLogOutput (Notice, Error) $
576 callProcessAndLogOutput (Notice, Error) $
578 proc "cp" ["-r", "-T", p', p]
577 proc "cp" ["-r", "-T", p', p]
579
578
@@ -622,28 +621,24 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
622 callProcessAndLogOutput (Notice, Error) $
621 callProcessAndLogOutput (Notice, Error) $
623 proc "mv" ["-T", tmpRepo, thunkDir]
622 proc "mv" ["-T", tmpRepo, thunkDir]
624
623
625−--TODO: add force mode to pack even if changes are present
626--TODO: add a rollback mode to pack to the original thunk
624--TODO: add a rollback mode to pack to the original thunk
627−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
625+packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628packThunk = packThunk' False
626packThunk = packThunk' False
629
627
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628+packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
629+packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
630 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
631 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634 Right (ThunkData_Checkout _) -> do
632 Right (ThunkData_Checkout _) -> do
635 withSpinner' ("Packing thunk " <> T.pack thunkDir)
633 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
634 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
635+ thunkPtr <- getThunkPtr (not force) thunkDir
638 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
636 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639 liftIO $ createThunk thunkDir thunkPtr
637 liftIO $ createThunk thunkDir thunkPtr
640 pure thunkPtr
638 pure thunkPtr
641
639
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
643−getThunkPtr = getThunkPtr' True
644−
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
640+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
641+getThunkPtr checkClean thunkDir = do
647 when checkClean $ ensureCleanGitRepo thunkDir True $
642 when checkClean $ ensureCleanGitRepo thunkDir True $
648 "thunk pack: thunk checkout contains unsaved modifications"
643 "thunk pack: thunk checkout contains unsaved modifications"
649
644
lib/command/src/Obelisk/Command/VmBuilder.hs+2−1
@@ -156,7 +156,8 @@ testLinuxBuild stateDir
156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157 | otherwise = do
157 | otherwise = do
158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159+ [ "--no-out-link"
159− [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160+ , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160 , "--builders", nixBuildersArgString stateDir
161 , "--builders", nixBuildersArgString stateDir
161 ]
162 ]
162 unless (exitCode == ExitSuccess) $ do
163 unless (exitCode == ExitSuccess) $ do
lib/executable-config/default.nix+4−1
@@ -16,9 +16,12 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
16 exit 1
16 exit 1
17 fi
17 fi
18 cp -a "${config}"/* "$out/config"
18 cp -a "${config}"/* "$out/config"
19+ find "$out/config" -type f -printf '%P\0' >"$out/config.files"
19 '');
20 '');
20in with nixpkgs.haskell.lib; {
21in with nixpkgs.haskell.lib; {
22+ haskellOverlay = self: super: {
21− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
23+ obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
24+ };
22 platforms = {
25 platforms = {
23 android = {
26 android = {
24 # Inject the given config directory into an android assets folder
27 # Inject the given config directory into an android assets folder
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2
@@ -9,8 +9,8 @@ library
9 build-depends:
9 build-depends:
10 base,
10 base,
11 bytestring,
11 bytestring,
12− directory,
12+ base64-bytestring,
13− filepath,
13+ containers,
14 reflex-dom-core,
14 reflex-dom-core,
15 text
15 text
16
16
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40
@@ -2,50 +2,24 @@
2module Obelisk.ExecutableConfig.Inject where
2module Obelisk.ExecutableConfig.Inject where
3
3
4import Control.Monad (mapM_)
4import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
5+import Control.Monad.IO.Class (MonadIO)
6−import Data.Semigroup ((<>))
7import Data.ByteString (ByteString)
6import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Base64 as B64
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Semigroup ((<>))
8import Data.Text (Text)
11import Data.Text (Text)
9−import qualified Data.Text as T
12+import qualified Data.Text.Encoding as T
10−import qualified Data.Text.IO as T
11import Reflex.Dom.Core hiding (value)
13import Reflex.Dom.Core hiding (value)
12−import System.Directory
13−import System.FilePath ((</>))
14−
15−-- | Produces a @<script>@ tag containing the contents of the config item,
16−-- rendered as a 'ByteString'.
17−inject :: FilePath -> IO ByteString
18−inject key = do
19− value <- T.readFile key
20− fmap snd $ renderStatic $ injectPure (T.pack key) value
21
14
22-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
15-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23−-- whose value is the provided configuration
16+-- whose value is the provided configuration, encoded in base64.
24−injectPure :: DomBuilder t m => Text -> Text -> m ()
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
25injectPure key value =
18injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
27− in elAttr "script" attrs $ text value
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
28−
29−-- | Scans the "common" and "frontend" configuration folders and produces
30−-- injectable @<script>@ tags containing the configuration keys (filepaths) and
31−-- values.
32−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
33−injectExecutableConfigs = do
34− cfgC <- getConfigs "config/common"
35− cfgF <- getConfigs "config/frontend"
36− mapM_ (uncurry injectPure) (cfgC <> cfgF)
37
21
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
22+-- | Produces injectable @<script>@ tags containing the configuration keys
39−getConfigs fp = liftIO $ do
23+-- (filepaths) and values.
40− dir <- doesDirectoryExist fp
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
41− if dir
25+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
42− then do
43− ps <- listDirectory fp
44− fmap concat $ mapM (\p -> getConfigs $ fp </> p) ps
45− else do
46− file <- doesFileExist fp
47− if file
48− then do
49− f <- T.readFile fp
50− return [(T.pack fp, f)]
51− else return []
lib/executable-config/lookup/default.nix+5−3
@@ -1,16 +1,18 @@
1{ hostPlatform, ghc
1{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
2+, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3+, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4+, jsaddle
3, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
5, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
4}:
6}:
5let isAndroid = hostPlatform.libc == "bionic";
7let isAndroid = hostPlatform.libc == "bionic";
6 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
8 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
7 isGhcjs = ghc.isGhcjs or false;
9 isGhcjs = ghc.isGhcjs or false;
8in mkDerivation {
10in mkDerivation {
9− pname = "obelisk-executable-config";
11+ pname = "obelisk-executable-config-lookup";
10 version = "0.1";
12 version = "0.1";
11 src = ./.;
13 src = ./.;
12 libraryHaskellDepends = [
14 libraryHaskellDepends = [
13− base bytestring filepath text transformers
15+ base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
14 ] ++ (if isAndroid then [
16 ] ++ (if isAndroid then [
15 android-activity
17 android-activity
16 ] else if isIOS then [
18 ] else if isIOS then [
lib/executable-config/lookup/obelisk-executable-config-lookup.cabalnew file+45
@@ -0,0 +1,45 @@
1+name: obelisk-executable-config-lookup
2+version: 0.1.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
8+ if os(ios)
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
11+ build-depends: jsaddle-wkwebview
12+ build-depends: directory
13+ else
14+ if os(linux-android)
15+ hs-source-dirs: src-android, src
16+ c-sources: cbits/ExecutableConfig.c
17+ other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
18+ build-depends: android-activity
19+ ld-options: -landroid
20+ else
21+ if impl(ghcjs)
22+ hs-source-dirs: src-ghcjs, src
23+ build-depends: ghcjs-dom
24+ else
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
27+ build-depends: directory
28+
29+ build-depends:
30+ base,
31+ base64-bytestring,
32+ bytestring,
33+ containers,
34+ filepath,
35+ text,
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
44+
45+ ghc-options: -Wall
lib/executable-config/lookup/obelisk-executable-config.cabaldeleted−30
@@ -1,30 +0,0 @@
1−name: obelisk-executable-config
2−version: 0.1.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− exposed-modules: Obelisk.ExecutableConfig
8− if os(ios)
9− hs-source-dirs: src-ios
10− build-depends: jsaddle-wkwebview
11− else
12− if os(linux-android)
13− hs-source-dirs: src-android
14− c-sources: cbits/ExecutableConfig.c
15− other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
16− build-depends: android-activity
17− ld-options: -landroid
18− else
19− if impl(ghcjs)
20− hs-source-dirs: src-ghcjs
21− build-depends: ghcjs-dom
22− else
23− hs-source-dirs: src
24−
25− build-depends:
26− base,
27− bytestring,
28− filepath,
29− text,
30− transformers
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Control.Exception (bracket)
6+import Control.Monad (forM)
7+import Data.ByteString (ByteString)
8+import qualified Data.ByteString as BS
9+import Data.Map (Map)
10+import qualified Data.Map as Map
11+import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
13+import Foreign.C.String (withCString)
14+import Foreign.Ptr (nullPtr)
15+
16+import Obelisk.ExecutableConfig.Internal.AssetManager
17+
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
19+getFromMgr mgr name = do
20+ let open = do
21+ a <- BS.useAsCString name $ \fn ->
22+ assetManager_open mgr fn 3
23+ return $ if unAAsset a == nullPtr
24+ then Nothing
25+ else Just a
26+ close = mapM_ asset_close
27+ bracket open close $ mapM $ \asset -> do
28+ b <- asset_getBuffer asset
29+ l <- asset_getLength asset
30+ BS.packCStringLen (b, fromIntegral l)
31+
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
34+ mgr <- assetManagerFromJava mgrObj
35+ let openDir = do
36+ d <- withCString "config.files" $ \fn ->
37+ assetManager_open mgr fn 3
38+ return $ if unAAsset d == nullPtr
39+ then Nothing
40+ else Just d
41+ closeDir = mapM_ asset_close
42+ configPaths <- bracket openDir closeDir $ \case
43+ Just asset -> do
44+ b <- asset_getBuffer asset
45+ l <- asset_getLength asset
46+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
47+ Nothing -> error "could not open configuration manifest 'config.files'"
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
51+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
54+
55+lines0 :: BS.ByteString -> [BS.ByteString]
56+lines0 ps
57+ | BS.null ps = []
58+ | otherwise = case BS.elemIndex 0 ps of
59+ Nothing -> [ps]
60+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+42
@@ -0,0 +1,42 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Base64 as B64
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Maybe (catMaybes)
10+import Data.Text (Text)
11+import qualified Data.Text.Encoding as T
12+import Data.Traversable (for)
13+import GHCJS.DOM
14+import GHCJS.DOM.Document (getHead)
15+import GHCJS.DOM.DOMStringMap (get)
16+import GHCJS.DOM.Element (getInnerHTML)
17+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18+import GHCJS.DOM.NodeList (item, getLength)
19+import GHCJS.DOM.ParentNode (querySelectorAll)
20+import GHCJS.DOM.Types (Node(Node), castTo)
21+
22+getConfigs :: IO (Map Text ByteString)
23+getConfigs = do
24+ Just doc <- currentDocument
25+ Just hd <- getHead doc
26+ nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
27+ fmap Map.fromList $ for nodes $ \node -> do
28+ e <- castTo HTMLElement node >>= \case
29+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
30+ Just htmlE -> return htmlE
31+ dataset <- getDataset e
32+ (,)
33+ <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
34+ <*> (fmap decodeOrFail (getInnerHTML e))
35+ where
36+ collToList es = do
37+ len <- getLength es
38+ list <- traverse (item es) [0..len-1]
39+ pure $ catMaybes list
40+ decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
41+ Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
42+ Right x' -> x'
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Control.Monad
5−import Control.Monad.Trans.Maybe
6−import Data.ByteString as BS
7−import Data.Text (Text)
8−import qualified Data.Text as T
9−import qualified Data.Text.IO as T
10−import qualified Data.Text.Encoding as T
11−import Language.Javascript.JSaddle.WKWebView
12−import System.FilePath.Posix ((</>))
13−import System.IO.Error
14−
15−get :: Text -> IO (Maybe Text)
16−get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
18−
19−catchDoesNotExist :: IO a -> IO (Maybe a)
20−catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
21− where
22− doesNotExist e = if isDoesNotExistError e then Just () else Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hsnew file+17
@@ -0,0 +1,17 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.ByteString (ByteString)
5+import Data.Map (Map)
6+import Data.Text (Text)
7+import qualified Data.Text as T
8+import qualified Data.Text.Encoding as T
9+import Language.Javascript.JSaddle.WKWebView
10+import System.FilePath.Posix
11+
12+import Obelisk.Configs.Internal.Directory
13+
14+getConfigs :: IO (Map Text ByteString)
15+getConfigs = mainBundleResourcePath >>= \case
16+ Nothing -> error "Could not get bundle resource path."
17+ Just p -> getConfigsFromDirectory $ T.unpack (T.decodeUtf8 p) </> "config"
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hsnew file+18
@@ -0,0 +1,18 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.Map (Map)
6+import qualified Data.Map as Map
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Data.ByteString (ByteString)
10+import qualified Data.ByteString as BS
11+import System.Directory
12+import System.FilePath
13+import Control.Monad
14+
15+import Obelisk.Configs.Internal.Directory
16+
17+getConfigs :: IO (Map Text ByteString)
18+getConfigs = getConfigsFromDirectory "config"
lib/executable-config/lookup/src/Obelisk/Configs.hsnew file+124
@@ -0,0 +1,124 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.ByteString (ByteString)
31+import Data.Map (Map)
32+import qualified Data.Map as Map
33+import Data.Text (Text)
34+import Reflex
35+import Reflex.Host.Class
36+import Reflex.Dom
37+#ifndef ghcjs_HOST_OS
38+import Language.Javascript.JSaddle (MonadJSM)
39+#endif
40+
41+class Monad m => HasConfigs m where
42+ getConfigs :: m (Map Text ByteString)
43+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
44+ getConfigs = lift getConfigs
45+ getConfig :: Text -> m (Maybe ByteString)
46+ getConfig k = do
47+ configs <- getConfigs
48+ return $ Map.lookup k configs
49+
50+instance Monad m => HasConfigs (ConfigsT m) where
51+ getConfigs = ConfigsT ask
52+
53+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
54+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
55+instance HasConfigs m => HasConfigs (EventWriterT t w m)
56+instance HasConfigs m => HasConfigs (PostBuildT t m)
57+instance HasConfigs m => HasConfigs (QueryT t q m)
58+instance HasConfigs m => HasConfigs (ReaderT r m)
59+instance HasConfigs m => HasConfigs (RequesterT t request response m)
60+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
61+instance HasConfigs m => HasConfigs (TriggerEventT t m)
62+
63+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
64+ deriving
65+ ( Functor
66+ , Applicative
67+ , Monad
68+ , MonadPlus
69+ , Alternative
70+ , MonadFix
71+ , MonadIO
72+ , MonadBase m'
73+ , MonadBaseControl m'
74+ , MonadRef
75+ , MonadTrans
76+ , DomBuilder t
77+ , MonadHold t
78+ , MonadReflexCreateTrigger t
79+ , MonadSample t
80+ , NotReady t
81+ , PostBuild t
82+ , TriggerEvent t
83+ , HasDocument
84+ , DomRenderHook t
85+ , HasJSContext
86+ , HasJS js
87+#ifndef ghcjs_HOST_OS
88+ , MonadJSM
89+#endif
90+ )
91+
92+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
93+ type Performable (ConfigsT m) = ConfigsT (Performable m)
94+ performEvent e = ConfigsT $ ReaderT $ \configs ->
95+ performEvent $ runConfigsT configs <$> e
96+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
97+ performEvent_ $ runConfigsT configs <$> e
98+
99+instance Adjustable t m => Adjustable t (ConfigsT m) where
100+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
101+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
102+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
103+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
104+
105+instance Prerender js t m => Prerender js t (ConfigsT m) where
106+ type Client (ConfigsT m) = ConfigsT (Client m)
107+ prerender server client = ConfigsT $ ReaderT $ \configs ->
108+ prerender (runConfigsT configs server) (runConfigsT configs client)
109+
110+instance PrimMonad m => PrimMonad (ConfigsT m) where
111+ type PrimState (ConfigsT m) = PrimState m
112+ primitive = lift . primitive
113+
114+runConfigsT
115+ :: Map Text ByteString
116+ -> ConfigsT m a
117+ -> m a
118+runConfigsT cs child = runReaderT (unConfigsT child) cs
119+
120+mapConfigsT
121+ :: (forall x. m x -> n x)
122+ -> ConfigsT m a
123+ -> ConfigsT n a
124+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabal+2
@@ -7,6 +7,7 @@ library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10+ containers,
10 cookie,
11 cookie,
11 dependent-sum,
12 dependent-sum,
12 ghcjs-dom,
13 ghcjs-dom,
@@ -14,6 +15,7 @@ library
14 lens,
15 lens,
15 mtl,
16 mtl,
16 obelisk-executable-config-inject,
17 obelisk-executable-config-inject,
18+ obelisk-executable-config-lookup,
17 obelisk-route,
19 obelisk-route,
18 primitive,
20 primitive,
19 ref-tf,
21 ref-tf,
lib/frontend/src/Obelisk/Frontend.hs+148−24
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
@@ -14,7 +15,10 @@ module Obelisk.Frontend
14 ( ObeliskWidget
15 ( ObeliskWidget
15 , Frontend (..)
16 , Frontend (..)
16 , runFrontend
17 , runFrontend
18+ , runFrontendWithConfigsAndCurrentRoute
17 , renderFrontendHtml
19 , renderFrontendHtml
20+ , removeHTMLConfigs
21+ , FrontendMode (..)
18 , module Obelisk.Frontend.Cookie
22 , module Obelisk.Frontend.Cookie
19 ) where
23 ) where
20
24
@@ -27,18 +31,35 @@ import Control.Monad.IO.Class
27import Control.Monad.Primitive
31import Control.Monad.Primitive
28import Control.Monad.Reader
32import Control.Monad.Reader
29import Control.Monad.Ref
33import Control.Monad.Ref
34+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
30import Data.ByteString (ByteString)
35import Data.ByteString (ByteString)
36+import Data.Foldable (for_)
31import Data.Functor.Sum
37import Data.Functor.Sum
38+import Data.Map (Map)
39+import Data.Maybe (catMaybes)
32import Data.Monoid ((<>))
40import Data.Monoid ((<>))
33import Data.Text (Text)
41import Data.Text (Text)
42+import qualified GHCJS.DOM as DOM
43+import qualified GHCJS.DOM.Types as DOM
44+import qualified GHCJS.DOM.History as DOM
45+import qualified GHCJS.DOM.Window as DOM
34−import Language.Javascript.JSaddle (JSM)
46+import Language.Javascript.JSaddle (JSM, jsNull)
47+import GHCJS.DOM (currentDocument)
48+import GHCJS.DOM.Document (getHead)
49+import GHCJS.DOM.Node (removeChild_)
50+import GHCJS.DOM.NodeList (item, getLength)
51+import GHCJS.DOM.ParentNode (querySelectorAll)
35import Obelisk.Frontend.Cookie
52import Obelisk.Frontend.Cookie
36import Obelisk.Route.Frontend
53import Obelisk.Route.Frontend
37import Reflex.Dom.Core
54import Reflex.Dom.Core
38import Reflex.Host.Class
55import Reflex.Host.Class
56+import Obelisk.Configs
39import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
57import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
58+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
40import Web.Cookie
59import Web.Cookie
41
60
61+import Debug.Trace
62+
42makePrisms ''Sum
63makePrisms ''Sum
43
64
44type ObeliskWidget js t route m =
65type ObeliskWidget js t route m =
@@ -60,16 +81,16 @@ type ObeliskWidget js t route m =
60 , Prerender js t m
81 , Prerender js t m
61 , PrebuildAgnostic t route m
82 , PrebuildAgnostic t route m
62 , PrebuildAgnostic t route (Client m)
83 , PrebuildAgnostic t route (Client m)
84+ , HasConfigs m
63 , HasCookies m
85 , HasCookies m
64− -- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
65− , MonadIO m
66− , MonadIO (Performable m)
67 )
86 )
68
87
69type PrebuildAgnostic t route m =
88type PrebuildAgnostic t route m =
70 ( SetRoute t route m
89 ( SetRoute t route m
71 , RouteToUrl route m
90 , RouteToUrl route m
72 , MonadFix m
91 , MonadFix m
92+ , HasConfigs m
93+ , HasConfigs (Performable m)
73 )
94 )
74
95
75data Frontend route = Frontend
96data Frontend route = Frontend
@@ -77,46 +98,149 @@ data Frontend route = Frontend
77 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
98 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
78 }
99 }
79
100
101+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
102+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
103+
104+removeHTMLConfigs :: JSM ()
105+removeHTMLConfigs = void $ runMaybeT $ do
106+ doc <- MaybeT currentDocument
107+ hd <- MaybeT $ getHead doc
108+ es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109+ for_ es $ removeChild_ hd
110+ where
111+ collToList es = do
112+ len <- getLength es
113+ lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
114+ pure $ catMaybes lst
115+
116+setInitialRoute :: Bool -> JSM ()
117+setInitialRoute useHash = do
118+ traceM "setInitialRoute"
119+ window <- DOM.currentWindowUnchecked
120+ initialLocation <- DOM.getLocation window
121+ initialUri <- getLocationUri initialLocation
122+ history <- DOM.getHistory window
123+ DOM.replaceState history jsNull ("" :: Text) $ Just $
124+ show $ setAdaptedUriPath useHash "/" initialUri
125+
126+data FrontendMode = FrontendMode
127+ { _frontendMode_hydrate :: Bool
128+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
129+ -- than building new DOM
130+ , _frontendMode_adjustRoute :: Bool
131+ -- ^ The page can't use regular routes, so encode routes into the hash
132+ -- instead
133+ }
134+
135+-- | Run the frontend, setting the initial route to "/" on platforms where no
136+-- route exists ambiently in the context (e.g. anything but web).
137+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
80runFrontend
138runFrontend
81 :: forall backendRoute route
139 :: forall backendRoute route
82 . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
140 . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
83 -> Frontend (R route)
141 -> Frontend (R route)
84 -> JSM ()
142 -> JSM ()
85runFrontend validFullEncoder frontend = do
143runFrontend validFullEncoder frontend = do
144+ let mode = FrontendMode
145+ { _frontendMode_hydrate =
146+#ifdef ghcjs_HOST_OS
147+ True
148+#else
149+ False
150+#endif
151+ , _frontendMode_adjustRoute =
152+#ifdef ghcjs_HOST_OS
153+ False
154+#else
155+ True
156+#endif
157+ }
158+ configs <- liftIO Lookup.getConfigs
159+ when (_frontendMode_hydrate mode) removeHTMLConfigs
160+ -- There's no fundamental reason that adjustRoute needs to control setting the
161+ -- initial route and *also* the useHash parameter; that's why these are
162+ -- separate here. However, currently, they are always the same.
163+ when (_frontendMode_adjustRoute mode) $ do
164+ setInitialRoute $ _frontendMode_adjustRoute mode
165+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
166+
167+runFrontendWithConfigsAndCurrentRoute
168+ :: forall backendRoute route
169+ . FrontendMode
170+ -> Map Text ByteString
171+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
172+ -> Frontend (R route)
173+ -> JSM ()
174+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
86 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
175 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
87 errorLeft = \case
176 errorLeft = \case
88 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
177 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
89 Right x -> Identity x
178 Right x -> Identity x
179+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
180+ , Ref (Performable m) ~ Ref IO
181+ , Ref m ~ Ref IO
182+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
183+ , MonadHold DomTimeline m
184+ , MonadRef m
185+ , MonadRef (Performable m)
186+ , MonadReflexCreateTrigger DomTimeline m
187+ , PerformEvent DomTimeline m
188+ , PostBuild DomTimeline m
189+ , PrimMonad m
190+ , MonadSample DomTimeline (Performable m)
191+ , DOM.MonadJSM m
192+ , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
193+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
194+ , MonadFix (Performable m)
195+ , MonadFix m
196+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
197+ , MonadIO (Performable m)
198+ )
199+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
200+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
201+ -> FloatingWidget () ()
90− runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
202+ w appendHead appendBody = do
91− rec switchover <- runRouteViewT ve switchover $ do
203+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
92− (switchover'', fire) <- newTriggerEvent
204+ (switchover'', fire) <- newTriggerEvent
93− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
205+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
206+ -- The order here is important - baseTag has to be before headWidget!
207+ baseTag
94− _frontend_head frontend
208+ _frontend_head frontend
95− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
209+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
96− _frontend_body frontend
210+ _frontend_body frontend
211+ switchover' <- case _frontendMode_hydrate mode of
97− switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
212+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
213+ False -> getPostBuild
98− performEvent_ $ liftIO (fire ()) <$ switchover'
214+ performEvent_ $ liftIO (fire ()) <$ switchover'
99− pure switchover''
215+ pure switchover''
100− pure ()
216+ pure ()
217+ if _frontendMode_hydrate mode
218+ then runHydrationWidgetWithHeadAndBody (pure ()) w
219+ else runImmediateWidgetWithHeadAndBody w
101
220
102renderFrontendHtml
221renderFrontendHtml
103 :: ( t ~ DomTimeline
222 :: ( t ~ DomTimeline
104 , MonadIO m
223 , MonadIO m
105− , widget ~ RoutedT t r (SetRouteT t r' (RouteToUrlT r' (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))
224+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
106 )
225 )
226+ => Map Text ByteString
107− => Cookies
227+ -> Cookies
108− -> (r' -> Text)
228+ -> (r -> Text)
109 -> r
229 -> r
230+ -> Frontend r
110 -> widget ()
231 -> widget ()
111 -> widget ()
232 -> widget ()
112 -> m ByteString
233 -> m ByteString
113−renderFrontendHtml cookies urlEnc route headWidget bodyWidget = do
234+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
114 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
235 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
115− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
236+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
116 el "html" $ do
237 el "html" $ do
117 el "head" $ do
238 el "head" $ do
118− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
239+ baseTag
119− -- The order here is important - baseTag has to be before headWidget!
120− baseTag >> injectExecutableConfigs >> headWidget
240+ injectExecutableConfigs configs
241+ _frontend_head frontend
242+ headExtra
121− el "body" bodyWidget
243+ el "body" $ do
244+ _frontend_body frontend
245+ bodyExtra
122 return $ "<!DOCTYPE html>" <> html
246 return $ "<!DOCTYPE html>" <> html
lib/frontend/src/Obelisk/Frontend/Cookie.hs+3
@@ -26,6 +26,7 @@ import GHCJS.DOM.Document (getCookie, Document)
26import GHCJS.DOM.Types (MonadJSM)
26import GHCJS.DOM.Types (MonadJSM)
27import Web.Cookie
27import Web.Cookie
28
28
29+import Obelisk.Configs
29import Obelisk.Route.Frontend
30import Obelisk.Route.Frontend
30
31
31class Monad m => HasCookies m where
32class Monad m => HasCookies m where
@@ -45,6 +46,8 @@ instance HasCookies m => HasCookies (SetRouteT t r m)
45instance HasCookies m => HasCookies (StaticDomBuilderT t m)
46instance HasCookies m => HasCookies (StaticDomBuilderT t m)
46instance HasCookies m => HasCookies (TriggerEventT t m)
47instance HasCookies m => HasCookies (TriggerEventT t m)
47instance HasCookies m => HasCookies (RoutedT t r m)
48instance HasCookies m => HasCookies (RoutedT t r m)
49+instance HasCookies m => HasCookies (ConfigsT m)
50+instance HasConfigs m => HasConfigs (CookiesT m)
48
51
49newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
52newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
50 deriving
53 deriving
lib/route/obelisk-route.cabal+7−2
@@ -6,12 +6,14 @@ build-type: Simple
6library
6library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends: base,
8 build-depends: base,
9+ aeson,
10+ bytestring,
9 categories,
11 categories,
10 constraints,
12 constraints,
13+ constraints-extras,
11 containers,
14 containers,
12 dependent-map,
15 dependent-map,
13 dependent-sum,
16 dependent-sum,
14− dependent-sum-universe-orphans,
15 dependent-sum-template,
17 dependent-sum-template,
16 either,
18 either,
17 ghcjs-dom,
19 ghcjs-dom,
@@ -21,15 +23,18 @@ library
21 monad-control,
23 monad-control,
22 mtl,
24 mtl,
23 network-uri,
25 network-uri,
26+ obelisk-executable-config-lookup,
24 primitive,
27 primitive,
25 ref-tf,
28 ref-tf,
26 reflex,
29 reflex,
27 reflex-dom-core,
30 reflex-dom-core,
28 template-haskell,
31 template-haskell,
32+ tabulation,
29 text,
33 text,
34+ th-extras,
30 transformers,
35 transformers,
31 universe,
36 universe,
32− universe-template
37+ universe-dependent-sum
33 exposed-modules: Obelisk.Route
38 exposed-modules: Obelisk.Route
34 Obelisk.Route.TH
39 Obelisk.Route.TH
35 Obelisk.Route.Frontend
40 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+107−32
@@ -13,6 +13,7 @@
13{-# LANGUAGE RankNTypes #-}
13{-# LANGUAGE RankNTypes #-}
14{-# LANGUAGE ScopedTypeVariables #-}
14{-# LANGUAGE ScopedTypeVariables #-}
15{-# LANGUAGE TemplateHaskell #-}
15{-# LANGUAGE TemplateHaskell #-}
16+{-# LANGUAGE TypeApplications #-}
16{-# LANGUAGE TypeFamilies #-}
17{-# LANGUAGE TypeFamilies #-}
17{-# LANGUAGE TypeOperators #-}
18{-# LANGUAGE TypeOperators #-}
18{-# LANGUAGE ViewPatterns #-}
19{-# LANGUAGE ViewPatterns #-}
@@ -85,6 +86,11 @@ module Obelisk.Route
85 , readShowEncoder
86 , readShowEncoder
86 , integralEncoder
87 , integralEncoder
87 , pathSegmentEncoder
88 , pathSegmentEncoder
89+ , queryOnlyEncoder
90+ , Decoder(..)
91+ , dmapEncoder
92+ , fieldMapEncoder
93+ , jsonEncoder
88 ) where
94 ) where
89
95
90import Prelude hiding ((.), id)
96import Prelude hiding ((.), id)
@@ -97,6 +103,7 @@ import Control.Category.Associative
97import Control.Category.Monoidal
103import Control.Category.Monoidal
98import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
104import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
99import Control.Monad.Except
105import Control.Monad.Except
106+import qualified Data.ByteString.Lazy as BSL
100import Data.Dependent.Sum (DSum (..))
107import Data.Dependent.Sum (DSum (..))
101import Data.Dependent.Map (DMap)
108import Data.Dependent.Map (DMap)
102import qualified Data.Dependent.Map as DMap
109import qualified Data.Dependent.Map as DMap
@@ -113,17 +120,19 @@ import Data.Maybe
113import Data.Monoid ((<>))
120import Data.Monoid ((<>))
114import Data.Set (Set)
121import Data.Set (Set)
115import qualified Data.Set as Set
122import qualified Data.Set as Set
116−import Data.Some (Some)
123+import Data.Some (Some(Some))
117−import qualified Data.Some as Some
118−import Data.Some.Universe.Orphans ()
119import Data.Text (Text)
124import Data.Text (Text)
120import qualified Data.Text as T
125import qualified Data.Text as T
121−import Data.Text.Encoding
126+import qualified Data.Text.Encoding as T
122import Data.Universe
127import Data.Universe
128+import Data.Universe.Some
123import Network.HTTP.Types.URI
129import Network.HTTP.Types.URI
124import qualified Numeric.Lens
130import qualified Numeric.Lens
125import Obelisk.Route.TH
131import Obelisk.Route.TH
126import Text.Read (readMaybe)
132import Text.Read (readMaybe)
133+import Data.Tabulation
134+import qualified Data.Aeson as Aeson
135+import Data.Aeson (FromJSON, ToJSON)
127
136
128-- Design goals:
137-- Design goals:
129-- No start-up time on the frontend (not yet met)
138-- No start-up time on the frontend (not yet met)
@@ -149,13 +158,13 @@ import Text.Read (readMaybe)
149
158
150type R f = DSum f Identity --TODO: Better name
159type R f = DSum f Identity --TODO: Better name
151
160
152−--TODO: COMPLETE pragma
161+{-# COMPLETE (:/) #-}
153infixr 5 :/
162infixr 5 :/
154pattern (:/) :: f a -> a -> R f
163pattern (:/) :: f a -> a -> R f
155pattern a :/ b = a :=> Identity b
164pattern a :/ b = a :=> Identity b
156
165
157mapSome :: (forall a. f a -> g a) -> Some f -> Some g
166mapSome :: (forall a. f a -> g a) -> Some f -> Some g
158−mapSome f (Some.This a) = Some.This $ f a
167+mapSome f (Some a) = Some $ f a
159
168
160hoistR :: (forall x. f x -> g x) -> R f -> R g
169hoistR :: (forall x. f x -> g x) -> R f -> R g
161hoistR f (x :=> Identity y) = f x :/ y
170hoistR f (x :=> Identity y) = f x :/ y
@@ -343,14 +352,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
343justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
352justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
344justEncoder = prismEncoder _Just
353justEncoder = prismEncoder _Just
345
354
346−-- |
355+-- | Encode () to 'Nothing'.
347nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
356nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
348nothingEncoder = prismEncoder _Nothing
357nothingEncoder = prismEncoder _Nothing
349
358
350someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
359someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
351someConstEncoder = unsafeMkEncoder $ EncoderImpl
360someConstEncoder = unsafeMkEncoder $ EncoderImpl
352− { _encoderImpl_encode = \(Some.This (Const a)) -> a
361+ { _encoderImpl_encode = \(Some (Const a)) -> a
353− , _encoderImpl_decode = pure . Some.This . Const
362+ , _encoderImpl_decode = pure . Some . Const
354 }
363 }
355
364
356-- | WARNING: This is only safe if the Show and Read instances for 'a' are
365-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -378,7 +387,7 @@ checkEnum1EncoderFunc
378 -> check (EncoderFunc check' parse p r)
387 -> check (EncoderFunc check' parse p r)
379checkEnum1EncoderFunc f = do
388checkEnum1EncoderFunc f = do
380 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
389 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
381− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
390+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
382 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
391 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
383 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
392 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
384
393
@@ -433,11 +442,11 @@ chainEncoder cons this rest = Encoder $ do
433 pure $ EncoderImpl
442 pure $ EncoderImpl
434 { _encoderImpl_decode = \v -> do
443 { _encoderImpl_decode = \v -> do
435 (here, following) <- _encoderImpl_decode consValid v
444 (here, following) <- _encoderImpl_decode consValid v
436− Some.This r <- _encoderImpl_decode thisValid here
445+ Some r <- _encoderImpl_decode thisValid here
437 (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
446 (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
438 , _encoderImpl_encode = \(r :/ s) ->
447 , _encoderImpl_encode = \(r :/ s) ->
439 _encoderImpl_encode consValid
448 _encoderImpl_encode consValid
440− ( _encoderImpl_encode thisValid $ Some.This r
449+ ( _encoderImpl_encode thisValid $ Some r
441 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
450 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
442 }
451 }
443
452
@@ -504,7 +513,7 @@ enum1Encoder
504 , Show r
513 , Show r
505 )
514 )
506 => (forall a. p a -> r) -> Encoder check parse (Some p) r
515 => (forall a. p a -> r) -> Encoder check parse (Some p) r
507−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
516+enum1Encoder f = enumEncoder $ \(Some p) -> f p
508
517
509-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
518-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
510-- have a large number of values - it will use a lot of memory.
519-- have a large number of values - it will use a lot of memory.
@@ -547,13 +556,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
547 }
556 }
548
557
549pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
558pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
550−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
559+pathOnlyEncoder = second (unitEncoder mempty) . coidr
551− { _encoderImpl_decode = \(path, query) ->
560+
552− if query == mempty
561+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
553− then pure path
562+queryOnlyEncoder = first (unitEncoder []) . coidl
554− else throwError "pathOnlyEncoderImpl: query was provided"
555− , _encoderImpl_encode = \path -> (path, mempty)
556− }
557
563
558singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
564singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
559singletonListEncoder = unsafeMkEncoder $ EncoderImpl
565singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -596,10 +602,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
596 in (urlDecodeText True k, urlDecodeText True <$> mv)
602 in (urlDecodeText True k, urlDecodeText True <$> mv)
597
603
598urlEncodeText :: Bool -> Text -> Text
604urlEncodeText :: Bool -> Text -> Text
599−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
605+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
600
606
601urlDecodeText :: Bool -> Text -> Text
607urlDecodeText :: Bool -> Text -> Text
602−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
608+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
603
609
604listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
610listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
605listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
611listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -711,9 +717,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
711 ObeliskRoute_App :: f a -> ObeliskRoute f a
717 ObeliskRoute_App :: f a -> ObeliskRoute f a
712 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
718 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
713
719
714−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
720+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
721+ universeSome = concat
715− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
722+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
716− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
723+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
724+ ]
717
725
718instance GEq f => GEq (ObeliskRoute f) where
726instance GEq f => GEq (ObeliskRoute f) where
719 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
727 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -801,12 +809,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
801
809
802someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
810someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
803someSumEncoder = Encoder $ pure $ EncoderImpl
811someSumEncoder = Encoder $ pure $ EncoderImpl
804− { _encoderImpl_encode = \(Some.This t) -> case t of
812+ { _encoderImpl_encode = \(Some t) -> case t of
805− InL l -> Left $ Some.This l
813+ InL l -> Left $ Some l
806− InR r -> Right $ Some.This r
814+ InR r -> Right $ Some r
807 , _encoderImpl_decode = pure . \case
815 , _encoderImpl_decode = pure . \case
808− Left (Some.This l) -> Some.This (InL l)
816+ Left (Some l) -> Some (InL l)
809− Right (Some.This r) -> Some.This (InR r)
817+ Right (Some r) -> Some (InR r)
810 }
818 }
811
819
812data Void1 :: * -> * where {}
820data Void1 :: * -> * where {}
@@ -817,7 +825,7 @@ instance Universe (Some Void1) where
817void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
825void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
818void1Encoder = Encoder $ pure $ EncoderImpl
826void1Encoder = Encoder $ pure $ EncoderImpl
819 { _encoderImpl_encode = \case
827 { _encoderImpl_encode = \case
820− Some.This f -> case f of {}
828+ Some f -> case f of {}
821 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
829 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
822 }
830 }
823
831
@@ -866,7 +874,6 @@ renderObeliskRoute e r =
866readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
874readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
867readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
875readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
868
876
869−
870integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
877integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
871integralEncoder = prismEncoder (Numeric.Lens.integral)
878integralEncoder = prismEncoder (Numeric.Lens.integral)
872
879
@@ -879,4 +886,72 @@ pathSegmentEncoder = unsafeMkEncoder EncoderImpl
879 (x:xs) -> pure (x, (xs, y))
886 (x:xs) -> pure (x, (xs, y))
880 }
887 }
881
888
889+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
890+
891+dmapEncoder :: forall check parse k k' v.
892+ ( Monad check
893+ , MonadError Text parse
894+ , Universe (Some k')
895+ , Ord k
896+ , GCompare k'
897+ , GShow k'
898+ )
899+ => Encoder check parse (Some k') k
900+ -> (forall v'. k' v' -> Encoder check parse v' v)
901+ -> Encoder check parse (DMap k' Identity) (Map k v)
902+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
903+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
904+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
905+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
906+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
907+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
908+ return $ EncoderImpl
909+ { _encoderImpl_encode = \dm -> Map.fromList $ do
910+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
911+ return ( encode keyEncoder (Some k')
912+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
913+ )
914+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
915+ Some (k' :: k' t) <- tryDecode keyEncoder k
916+ case DMap.lookup k' valueDecoders of
917+ Nothing -> throwError . T.pack . keyError $ gshow k'
918+ Just (Decoder e) -> do
919+ v' <- tryDecode e v
920+ return (k' :=> Identity v')
921+ }
922+
923+fieldMapEncoder :: forall check parse r.
924+ ( Applicative check
925+ , MonadError Text parse
926+ , HasFields r
927+ , Universe (Some (Field r))
928+ , GShow (Field r)
929+ , GCompare (Field r)
930+ )
931+ => Encoder check parse r (DMap (Field r) Identity)
932+fieldMapEncoder = unsafeEncoder $ do
933+ pure $ EncoderImpl
934+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
935+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
936+ case DMap.lookup f dm of
937+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
938+ Just (Identity v) -> return v
939+ }
940+
941+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
942+jsonEncoder :: forall check parse r.
943+ ( ToJSON r
944+ , FromJSON r
945+ , Applicative check
946+ , MonadError Text parse
947+ )
948+ => Encoder check parse r Text
949+jsonEncoder = unsafeEncoder $ do
950+ pure $ EncoderImpl
951+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
952+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
953+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
954+ Right x -> return x
955+ }
956+
882--TODO: decodeURIComponent as appropriate
957--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+51−20
@@ -51,6 +51,9 @@ module Obelisk.Route.Frontend
51 , runRouteToUrlT
51 , runRouteToUrlT
52 , mapRouteToUrlT
52 , mapRouteToUrlT
53 , routeLink
53 , routeLink
54+ , dynRouteLink
55+ , adaptedUriPath
56+ , setAdaptedUriPath
54 ) where
57 ) where
55
58
56import Prelude hiding ((.), id)
59import Prelude hiding ((.), id)
@@ -88,13 +91,13 @@ import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can
88import Reflex.Dom.Core
91import Reflex.Dom.Core
89import qualified GHCJS.DOM.Types as DOM
92import qualified GHCJS.DOM.Types as DOM
90import Network.URI
93import Network.URI
91−#if defined(ios_HOST_OS)
92import Data.Maybe (fromMaybe)
94import Data.Maybe (fromMaybe)
93import qualified Data.List as L
95import qualified Data.List as L
94−#endif
95
96
96import Unsafe.Coerce
97import Unsafe.Coerce
97
98
99+import Obelisk.Configs
100+
98infixr 5 :~
101infixr 5 :~
99pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
102pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
100pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
103pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
@@ -176,6 +179,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
176 askQueryResult = lift askQueryResult
179 askQueryResult = lift askQueryResult
177 queryIncremental = lift . queryIncremental
180 queryIncremental = lift . queryIncremental
178
181
182+instance HasConfigs m => HasConfigs (RoutedT t r m)
183+
179instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
184instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
180
185
181instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
186instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
@@ -338,6 +343,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
338 type PrimState (SetRouteT t r m) = PrimState m
343 type PrimState (SetRouteT t r m) = PrimState m
339 primitive = lift . primitive
344 primitive = lift . primitive
340
345
346+instance HasConfigs m => HasConfigs (SetRouteT t r m)
347+
341instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
348instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
342 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
349 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
343 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
350 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -435,6 +442,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
435 askQueryResult = lift askQueryResult
442 askQueryResult = lift askQueryResult
436 queryIncremental = lift . queryIncremental
443 queryIncremental = lift . queryIncremental
437
444
445+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
446+
438runRouteViewT
447runRouteViewT
439 :: forall t m r a.
448 :: forall t m r a.
440 ( TriggerEvent t m
449 ( TriggerEvent t m
@@ -445,17 +454,20 @@ runRouteViewT
445 , MonadFix m
454 , MonadFix m
446 )
455 )
447 => (Encoder Identity Identity r PageName)
456 => (Encoder Identity Identity r PageName)
457+ --TODO: Get rid of the switchover and useHash arguments
458+ -- useHash can probably be baked into the encoder
448 -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
459 -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
460+ -> Bool
449 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
461 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
450 -> m a
462 -> m a
451−runRouteViewT routeEncoder switchover a = do
463+runRouteViewT routeEncoder switchover useHash a = do
452 rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
464 rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
453 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
465 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
454 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
466 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
455 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
467 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
456 -- this ever to happen, we'll just handle it by failing completely with 'error'.
468 -- this ever to happen, we'll just handle it by failing completely with 'error'.
457 route :: Dynamic t r
469 route :: Dynamic t r
458− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
470+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
459 where
471 where
460 errorLeft (Left e) = error (T.unpack e)
472 errorLeft (Left e) = error (T.unpack e)
461 errorLeft (Right x) = x
473 errorLeft (Right x) = x
@@ -476,7 +488,7 @@ runRouteViewT routeEncoder switchover a = do
476 -- we can change this function later to accommodate.
488 -- we can change this function later to accommodate.
477 -- See: https://github.com/whatwg/html/issues/2174
489 -- See: https://github.com/whatwg/html/issues/2174
478 , _historyStateUpdate_title = ""
490 , _historyStateUpdate_title = ""
479− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
491+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
480 { uriQuery = newQuery
492 { uriQuery = newQuery
481 }
493 }
482 }
494 }
@@ -503,25 +515,44 @@ routeLink r w = do
503 setRoute $ r <$ domEvent Click e
515 setRoute $ r <$ domEvent Click e
504 return a
516 return a
505
517
518+-- | A link widget that, when clicked, sets the route to current value of the
519+-- provided dynamic route. In non-javascript contexts the value of the dynamic post
520+-- build is used so the link still works like 'routeLink'.
521+dynRouteLink
522+ :: forall t m a route.
523+ ( DomBuilder t m
524+ , PostBuild t m
525+ , RouteToUrl (R route) m
526+ , SetRoute t (R route) m
527+ )
528+ => Dynamic t (R route) -- ^ Target route
529+ -> m a -- ^ Child widget
530+ -> m a
531+dynRouteLink dr w = do
532+ enc <- askRouteToUrl
533+ er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
534+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
535+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
536+ & elementConfig_modifyAttributes .~ er
537+ (e, a) <- element "a" cfg w
538+ setRoute $ tag (current dr) $ domEvent Click e
539+ return a
540+
506-- On ios due to sandboxing when loading the page from a file adapt the
541-- On ios due to sandboxing when loading the page from a file adapt the
507-- path to be based on the hash.
542-- path to be based on the hash.
508
543
509−adaptedUriPath :: URI -> String
544+adaptedUriPath :: Bool -> URI -> String
510−#if defined(ios_HOST_OS)
545+adaptedUriPath = \case
511−adaptedUriPath = hashToPath . uriFragment
546+ True -> hashToPath . uriFragment
547+ False -> uriPath
512
548
513−hashToPath :: String -> String
549+setAdaptedUriPath :: Bool -> String -> URI -> URI
514−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
550+setAdaptedUriPath useHash s u = case useHash of
515−#else
516−adaptedUriPath = uriPath
517−#endif
518−
519−setAdaptedUriPath :: String -> URI -> URI
520−#if defined(ios_HOST_OS)
521−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
551+ True -> u { uriFragment = pathToHash s }
552+ False -> u { uriPath = s }
522
553
523pathToHash :: String -> String
554pathToHash :: String -> String
524pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
555pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
525−#else
556+
526−setAdaptedUriPath s u = u { uriPath = s }
557+hashToPath :: String -> String
527−#endif
558+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
1module Obelisk.Route.TH (deriveRouteComponent) where
1module Obelisk.Route.TH (deriveRouteComponent) where
2
2
3+import Data.Constraint.Extras.TH
3import Data.GADT.Show.TH
4import Data.GADT.Show.TH
4import Data.GADT.Compare.TH
5import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
6import Language.Haskell.TH
7import Language.Haskell.TH
7
8
8-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
9-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
@@ -11,8 +12,6 @@ deriveRouteComponent x = concat <$> traverse ($ x)
11 [ deriveGShow
12 [ deriveGShow
12 , deriveGEq
13 , deriveGEq
13 , deriveGCompare
14 , deriveGCompare
14− , deriveShowTagIdentity
15+ , deriveUniverseSome
15− , deriveEqTagIdentity
16+ , deriveArgDict
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
18 ]
17 ]
lib/run/obelisk-run.cabal+2−1
@@ -10,6 +10,7 @@ library
10 attoparsec
10 attoparsec
11 , base
11 , base
12 , bytestring
12 , bytestring
13+ , containers
13 , cookie
14 , cookie
14 , dependent-sum
15 , dependent-sum
15 , dependent-sum-template
16 , dependent-sum-template
@@ -25,7 +26,7 @@ library
25 , network
26 , network
26 , obelisk-asset-serve-snap
27 , obelisk-asset-serve-snap
27 , obelisk-backend
28 , obelisk-backend
28− , obelisk-executable-config
29+ , obelisk-executable-config-lookup
29 , obelisk-frontend
30 , obelisk-frontend
30 , obelisk-route
31 , obelisk-route
31 , process
32 , process
lib/run/src/Obelisk/Run.hs+44−27
@@ -26,12 +26,15 @@ import Data.Dependent.Sum (DSum (..))
26import Data.Functor.Identity
26import Data.Functor.Identity
27import Data.Functor.Sum
27import Data.Functor.Sum
28import Data.List (uncons)
28import Data.List (uncons)
29+import Data.Map (Map)
30+import qualified Data.Map as Map
29import Data.Maybe
31import Data.Maybe
30import Data.Semigroup ((<>))
32import Data.Semigroup ((<>))
31import Data.Streaming.Network (bindPortTCP)
33import Data.Streaming.Network (bindPortTCP)
32import Data.String (fromString)
34import Data.String (fromString)
33import Data.Text (Text)
35import Data.Text (Text)
34import qualified Data.Text as T
36import qualified Data.Text as T
37+import qualified Data.Text.Encoding as T
35import Language.Javascript.JSaddle.Run (syncPoint)
38import Language.Javascript.JSaddle.Run (syncPoint)
36import Language.Javascript.JSaddle.WebSockets
39import Language.Javascript.JSaddle.WebSockets
37import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
40import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -46,7 +49,6 @@ import Network.WebSockets (ConnectionOptions)
46import Network.WebSockets.Connection (defaultConnectionOptions)
49import Network.WebSockets.Connection (defaultConnectionOptions)
47import qualified Obelisk.Asset.Serve.Snap as Snap
50import qualified Obelisk.Asset.Serve.Snap as Snap
48import Obelisk.Backend
51import Obelisk.Backend
49−import Obelisk.ExecutableConfig (get)
50import Obelisk.Frontend
52import Obelisk.Frontend
51import Obelisk.Route.Frontend
53import Obelisk.Route.Frontend
52import Reflex.Dom.Core
54import Reflex.Dom.Core
@@ -73,6 +75,7 @@ run port serveStaticAsset backend frontend = do
73 case checkEncoder $ _backend_routeEncoder backend of
75 case checkEncoder $ _backend_routeEncoder backend of
74 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
76 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
75 Right validFullEncoder -> do
77 Right validFullEncoder -> do
78+ publicConfigs <- getPublicConfigs
76 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
79 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
77 _backend_run backend $ \serveRoute -> do
80 _backend_run backend $ \serveRoute -> do
78 runSnapWithCommandLineArgs $ do
81 runSnapWithCommandLineArgs $ do
@@ -80,29 +83,31 @@ run port serveStaticAsset backend frontend = do
80 Identity r -> case r of
83 Identity r -> case r of
81 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
84 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
82 InR obeliskRoute :=> Identity a ->
85 InR obeliskRoute :=> Identity a ->
83− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
86+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
84 let conf = defRunConfig { _runConfig_redirectPort = port }
87 let conf = defRunConfig { _runConfig_redirectPort = port }
85− runWidget conf frontend validFullEncoder `finally` killThread backendTid
88+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
86
89
87-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
90-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
88runServeAsset :: FilePath -> [Text] -> Snap ()
91runServeAsset :: FilePath -> [Text] -> Snap ()
89runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
92runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
90
93
91−getConfigRoute :: IO (Maybe URI)
94+getConfigRoute :: Map Text ByteString -> Either Text URI
92−getConfigRoute = get "config/common/route" >>= \case
95+getConfigRoute configs = case Map.lookup "common/route" configs of
93− Just r -> case URI.mkURI $ T.strip r of
96+ Just r ->
97+ let stripped = T.strip (T.decodeUtf8 r)
98+ in case URI.mkURI stripped of
94− Just route -> pure $ Just route
99+ Just route -> Right route
95− Nothing -> do
100+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
96− putStrLn $ "Route is invalid: " <> show r
101+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
97− pure Nothing
102+
98− Nothing -> pure Nothing
103+runWidget
99−
104+ :: RunConfig
100−defAppUri :: URI
105+ -> Map Text ByteString
101−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
106+ -> Frontend (R route)
102−
103−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
107+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
108+ -> IO ()
104−runWidget conf frontend validFullEncoder = do
109+runWidget conf configs frontend validFullEncoder = do
105− uri <- fromMaybe defAppUri <$> getConfigRoute
110+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
106 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
111 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
107 redirectHost = _runConfig_redirectHost conf
112 redirectHost = _runConfig_redirectHost conf
108 redirectPort = _runConfig_redirectPort conf
113 redirectPort = _runConfig_redirectPort conf
@@ -114,19 +119,25 @@ runWidget conf frontend validFullEncoder = do
114 close
119 close
115 (\skt -> do
120 (\skt -> do
116 man <- newManager defaultManagerSettings
121 man <- newManager defaultManagerSettings
117− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
118 runSettingsSocket settings skt app)
123 runSettingsSocket settings skt app)
119
124
120obeliskApp
125obeliskApp
121− :: forall route backendRoute. ConnectionOptions
126+ :: forall route backendRoute
127+ . Map Text ByteString
128+ -> ConnectionOptions
122 -> Frontend (R route)
129 -> Frontend (R route)
123 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
130 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
124 -> URI
131 -> URI
125 -> Application
132 -> Application
126 -> IO Application
133 -> IO Application
127−obeliskApp opts frontend validFullEncoder uri backend = do
134+obeliskApp configs opts frontend validFullEncoder uri backend = do
135+ let mode = FrontendMode
136+ { _frontendMode_hydrate = True
137+ , _frontendMode_adjustRoute = False
138+ }
128− let entryPoint = do
139+ entryPoint = do
129− runFrontend validFullEncoder frontend
140+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
130 syncPoint
141 syncPoint
131 jsaddlePath <- URI.mkPathPiece "jsaddle"
142 jsaddlePath <- URI.mkPathPiece "jsaddle"
132 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
143 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
@@ -141,15 +152,21 @@ obeliskApp opts frontend validFullEncoder uri backend = do
141 }
152 }
142 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
153 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
143 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
154 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
144− html <- renderJsaddleFrontend cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
155+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
145 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
156 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
146 _ -> backend req sendResponse
157 _ -> backend req sendResponse
147
158
148−renderJsaddleFrontend :: Cookies -> (route -> Text) -> route -> Frontend route -> IO ByteString
159+renderJsaddleFrontend
160+ :: Map Text ByteString
161+ -> Cookies
162+ -> (route -> Text)
163+ -> route
164+ -> Frontend route
165+ -> IO ByteString
149−renderJsaddleFrontend cookies urlEnc r f =
166+renderJsaddleFrontend configs cookies urlEnc r f =
150 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
167 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
151 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
168 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
152− in renderFrontendHtml cookies urlEnc r (_frontend_head f >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
169+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
153
170
154-- | like 'bindPortTCP' but reconnects on exception
171-- | like 'bindPortTCP' but reconnects on exception
155bindPortTCPRetry :: Settings
172bindPortTCPRetry :: Settings
lib/selftest/obelisk-selftest.cabal+1
@@ -16,6 +16,7 @@ library
16 , http-types
16 , http-types
17 , HUnit
17 , HUnit
18 , obelisk-cliapp
18 , obelisk-cliapp
19+ , obelisk-executable-config-lookup
19 , obelisk-run
20 , obelisk-run
20 , process
21 , process
21 , network
22 , network
lib/selftest/src/Obelisk/SelfTest.hs+15−11
@@ -34,6 +34,7 @@ import Test.HUnit.Base
34
34
35import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
35import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
36import qualified Obelisk.CliApp as CliApp
36import qualified Obelisk.CliApp as CliApp
37+import Obelisk.ExecutableConfig.Lookup (getConfigs)
37import Obelisk.Run (getConfigRoute)
38import Obelisk.Run (getConfigRoute)
38
39
39data ObRunState
40data ObRunState
@@ -74,6 +75,7 @@ main = do
74 unless isVerbose $
75 unless isVerbose $
75 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
76 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
76 let verbosity = bool silently verbosely isVerbose
77 let verbosity = bool silently verbosely isVerbose
78+ nixBuild args = run "nix-build" ("--no-out-link" : args)
77 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
79 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
78 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
80 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
79 [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
81 [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
@@ -126,8 +128,10 @@ main = do
126 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
128 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127 ls tmp >>= liftIO . assertEqual "" []
129 ls tmp >>= liftIO . assertEqual "" []
128
130
129− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
131+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
132+ configs <- getConfigs
130− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
133+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
134+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
131
135
132 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
133 -- when not run by 'nix-build --attr selftest'
137 -- when not run by 'nix-build --attr selftest'
@@ -138,22 +142,22 @@ main = do
138 testObRunInDir p2 p3 (Just "frontend") httpManager
142 testObRunInDir p2 p3 (Just "frontend") httpManager
139
143
140 describe "obelisk project" $ parallel $ do
144 describe "obelisk project" $ parallel $ do
141− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
145+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
142− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
146+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
143− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
147+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
144 -- See https://github.com/obsidiansystems/obelisk/issues/101
148 -- See https://github.com/obsidiansystems/obelisk/issues/101
145− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
149+ -- it "can build everything" $ shelly_ $ nixBuild [obeliskImpl]
146
150
147 describe "blank initialized project" $ parallel $ do
151 describe "blank initialized project" $ parallel $ do
148
152
149 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
150− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
154+ nixBuild ["-A", "ghc.backend"]
151 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
152− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
156+ nixBuild ["-A", "ghcjs.frontend"]
153
157
154 if os == "darwin"
158 if os == "darwin"
155− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
159+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
156− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
160+ else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
157
161
158 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162 forM_ ["ghc", "ghcjs"] $ \compiler -> do
159 let
163 let
@@ -163,7 +167,7 @@ main = do
163 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
167 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
164
168
165 it "can build reflex project" $ inTmpObInit $ \_ -> do
169 it "can build reflex project" $ inTmpObInit $ \_ -> do
166− run "nix-build" []
170+ nixBuild []
167
171
168 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
172 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
169 u <- update
173 u <- update
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
release.nix+17−6
@@ -1,4 +1,5 @@
1+{ self-args ? {}
1−{ local-self ? import ./. {}
2+, local-self ? import ./. self-args
2}:
3}:
3
4
4let
5let
@@ -9,7 +10,7 @@ let
9 obeliskPackagesCommon = [
10 obeliskPackagesCommon = [
10 "obelisk-frontend"
11 "obelisk-frontend"
11 "obelisk-route"
12 "obelisk-route"
12− "obelisk-executable-config"
13+ "obelisk-executable-config-lookup"
13 ];
14 ];
14
15
15 obeliskPackagesBackend = obeliskPackagesCommon ++ [
16 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -59,17 +60,27 @@ let
59 (concatDepends ghcjs)
60 (concatDepends ghcjs)
60 ];
61 ];
61 command = local-self.command;
62 command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63+ serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
63− builtSkeleton = (import ./skeleton {}).all;
64+ androidObelisk = import ./. (self-args // {
65+ config.android_sdk.accept_license = true;
66+ });
67+ androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
68+ iosObelisk = import ./. (self-args // {
69+ system = "x86_64-darwin";
70+ iosSdkVersion = "10.2";
71+ });
72+ iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
64 in {
73 in {
65 inherit
74 inherit
66 command
75 command
67 ghc ghcjs
76 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
77+ serverExeSkeleton
78+ iosSkeleton
79+ androidSkeleton;
69 cache = reflex-platform.pinBuildInputs
80 cache = reflex-platform.pinBuildInputs
70 "obelisk-${system}"
81 "obelisk-${system}"
71 cachePackages
82 cachePackages
72− [command serverExeSkeleton builtSkeleton];
83+ [command serverExeSkeleton iosSkeleton androidSkeleton];
73 });
84 });
74
85
75 metaCache = local-self.pinBuildInputs
86 metaCache = local-self.pinBuildInputs
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
1module Common.Api where
1module Common.Api where
2
2
3commonStuff :: String
3commonStuff :: String
4−commonStuff = "Here is a string defined in code common to the frontend and backend."
4+commonStuff = "Here is a string defined in Common.Api"
skeleton/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
skeleton/default.nix+9−3
@@ -1,7 +1,13 @@
1+{ obelisk ? import ./.obelisk/impl {
1−{ system ? builtins.currentSystem # TODO: Get rid of this system cruft
2+ system = builtins.currentSystem;
2−, iosSdkVersion ? "10.2"
3+ iosSdkVersion = "10.2";
4+ # You must accept the Android Software Development Kit License Agreement at
5+ # https://developer.android.com/studio/terms in order to build Android apps.
6+ # Uncomment and set this to `true` to indicate your acceptance:
7+ # config.android_sdk.accept_license = false;
8+ }
3}:
9}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
10+with obelisk;
5project ./. ({ ... }: {
11project ./. ({ ... }: {
6 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
12 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
7 android.displayName = "Obelisk Minimal Example";
13 android.displayName = "Obelisk Minimal Example";
skeleton/frontend/frontend.cabal+1
@@ -10,6 +10,7 @@ library
10 , obelisk-frontend
10 , obelisk-frontend
11 , obelisk-route
11 , obelisk-route
12 , reflex-dom
12 , reflex-dom
13+ , obelisk-executable-config-lookup
13 , obelisk-generated-static
14 , obelisk-generated-static
14 , text
15 , text
15 exposed-modules:
16 exposed-modules:
skeleton/frontend/src/Frontend.hs+8−1
@@ -4,7 +4,9 @@
4module Frontend where
4module Frontend where
5
5
6import qualified Data.Text as T
6import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
7import Obelisk.Frontend
8import Obelisk.Frontend
9+import Obelisk.Configs
8import Obelisk.Route
10import Obelisk.Route
9import Reflex.Dom.Core
11import Reflex.Dom.Core
10
12
@@ -12,7 +14,6 @@ import Common.Api
12import Common.Route
14import Common.Route
13import Obelisk.Generated.Static
15import Obelisk.Generated.Static
14
16
15−
16frontend :: Frontend (R FrontendRoute)
17frontend :: Frontend (R FrontendRoute)
17frontend = Frontend
18frontend = Frontend
18 { _frontend_head = el "title" $ text "Obelisk Minimal Example"
19 { _frontend_head = el "title" $ text "Obelisk Minimal Example"
@@ -20,4 +21,10 @@ frontend = Frontend
20 text "Welcome to Obelisk!"
21 text "Welcome to Obelisk!"
21 el "p" $ text $ T.pack commonStuff
22 el "p" $ text $ T.pack commonStuff
22 elAttr "img" ("src" =: static @"obelisk.jpg") blank
23 elAttr "img" ("src" =: static @"obelisk.jpg") blank
24+ el "div" $ do
25+ exampleConfig <- getConfig "common/example"
26+ case exampleConfig of
27+ Nothing -> text "No config file found in config/common/example"
28+ Just s -> text (T.decodeUtf8 s)
29+ return ()
23 }
30 }