on master help sign in
Merge remote-tracking branch 'origin/develop' into cg-tabulation
commit44bef508507bce6cf3e6cc7fcc017ec6f01d09a9 authorCale Gibbard <cgibbard@gmail.com> dateTue 28 May 2019, 14:03 (7 years ago) parent60111f8de99fc6
49 files changed+1152−323
CONTRIBUTING.md+60−3 ChangeLog.md+24 FAQ.md+76 README.md+38−2 default.nix+17−15 dep/reflex-platform/github.json+3−3 jobsets.nix+5 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+2−1 lib/backend/obelisk-backend.cabal+4 lib/backend/src/Obelisk/Backend.hs+87−28 lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+26−2 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−17 lib/cliapp/src/Obelisk/CliApp/Theme.hs+29 lib/cliapp/src/Obelisk/CliApp/Types.hs+3 lib/command/src/Obelisk/Command.hs+7−4 lib/command/src/Obelisk/Command/Deploy.hs+4−4 lib/command/src/Obelisk/Command/Run.hs+15−2 lib/command/src/Obelisk/Command/Thunk.hs+7−12 lib/executable-config/backend/obelisk-executable-config-backend.cabal+20 lib/executable-config/backend/src/Obelisk/ExecutableConfig/Backend.hs+77 lib/executable-config/default.nix+6−1 lib/executable-config/frontend/obelisk-executable-config-frontend.cabal+21 lib/executable-config/frontend/src/Obelisk/ExecutableConfig/Frontend.hs+115 lib/executable-config/inject/obelisk-executable-config-inject.cabal+1−3 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+8−37 lib/executable-config/lookup/default.nix+1−1 lib/executable-config/lookup/obelisk-executable-config.cabal → lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+11−4 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs−27 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+56 lib/executable-config/lookup/src-directory/Obelisk/ExecutableConfig/Internal/ConfigDirectory.hs+24 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hs−17 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+36 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs−22 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+15 lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs−13 lib/executable-config/lookup/src/Obelisk/ExecutableConfig/Lookup.hs+9 lib/frontend/obelisk-frontend.cabal+7−1 lib/frontend/src/Obelisk/Frontend.hs+81−46 lib/frontend/src/Obelisk/Frontend/Cookie.hs+102 lib/route/obelisk-route.cabal+1 lib/route/src/Obelisk/Route.hs+1−1 lib/route/src/Obelisk/Route/Frontend.hs+57−17 lib/run/obelisk-run.cabal+3−1 lib/run/src/Obelisk/Run.hs+46−28 lib/selftest/obelisk-selftest.cabal+1 lib/selftest/src/Obelisk/SelfTest.hs+4−2 release.nix+14−6 skeleton/default.nix+9−3
CONTRIBUTING.md+60−3
@@ -1,9 +1,66 @@
1−## Contributing to Obelisk
1+# Contribution Guide
22
3−Thanks for taking the time to contribute!
3+Contributions and issue reports are encouraged and appreciated!
44
5−Before submitting a pull request, please run the tests:
5+- [Opening Issues](#opening-issues)
6+- [Submitting Changes](#submitting-changes)
7+ - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Code Quality](#code-quality)
9+ - [Documentation](#documentation)
610
11+## Opening Issues
12+
13+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
14+
15+* Describe the issue you're encountering or the suggestion you're making
16+* Include any relevant steps to reproduce or code samples you can. It's always easier for us to debug if we have something that demonstrates the error.
17+* Let us know what version of this project you were using. If you're using a github checkout, provide the git hash.
18+
19+## Submitting Changes
20+
21+Most pull requests should target the `develop` branch. `master` is the release branch. `develop` is periodically merged into master after a period of testing.
22+
23+### Guidelines for Commit Messages
24+
25+#### Summary Line
26+The summary line of your commit message should summarize the changes being made. Commit messages should be written in the imperative mood and should describe what happens when the commit is applied. If your commit modifies one of the in-tree haskell packages (found in `./lib`), please prefix your commit summary with the name of the package being modified.
27+
28+One way to think about it is that your commit message should be able to complete the sentence:
29+"When applied, this commit will..."
30+
31+##### Note on bumping dependencies
32+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
33+
34+#### Body
35+For 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.
36+
37+### Code Quality
38+
39+#### Warnings
40+
41+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
42+
43+#### Build and Test
44+
45+Make sure the project builds and that the tests pass! This will generally also be checked by CI before merge, but trying it yourself first means you'll catch problems earlier and your contribution can be merged that much sooner!
46+
47+You can run the tests like this:
748```bash
849$(nix-build -A selftest --no-out-link)
950```
51+
52+### Documentation
53+
54+#### In the code
55+We're always striving to improve documentation. Please include [haddock](https://haskell-haddock.readthedocs.io/en/latest/index.html) documentation for any added code, and update the documentation for any code you modify.
56+
57+#### In the [Changelog](ChangeLog.md)
58+Add an entry to the changelog when your PR:
59+* Adds a feature
60+* Deprecates something
61+* Includes a breaking change
62+* Makes any other change that will impact users
63+
64+#### In the [Readme](README.md)
65+The readme is the first place a lot of people look for information about the repository. Update any parts of the readme that are affected by your PR.
66+
ChangeLog.mdnew file+24
@@ -0,0 +1,24 @@
1+# Revision history for obelisk
2+
3+This 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+
5+## Unreleased
6+
7+
8+## v0.1.1.0 - 2019-05-17
9+
10+* Fix crashes of Android apps on 32-bit ARM devices.
11+* 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`.
12+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
13+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
14+* Add the `ob shell` command to enter a nix shell for an obelisk project
15+* 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.
16+* 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`.
17+* 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`.
18+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
19+
20+
21+## v0.1.0.0 - 2019-03-29
22+
23+* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
24+* Add `HasCookies` and `CookiesT` to allow `ObeliskWidget`s to access cookies during static and "hydrated" DOM rendering.
FAQ.md+76
@@ -2,6 +2,8 @@
22
331. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
441. [`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)
57
68### How do I fix invalid entitlements?
79
@@ -44,3 +46,77 @@ ob thunk pack $SOME_THUNK
4446Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
4547
4648(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+38−2
@@ -4,6 +4,8 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
44
55- [Installing Obelisk](#installing-obelisk)
66- [Developing an Obelisk project](#developing-an-obelisk-project)
7+ - [Hoogle](#hoogle)
8+ - [Adding Package Overrides](#adding-package-overrides)
79- [Deploying](#deploying)
810 - [Locally](#locally)
911 - [EC2](#ec2)
@@ -22,6 +24,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
2224 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
2325 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2426 ```
27+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2528 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)):
2629 ```
2730 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
@@ -79,10 +82,40 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
7982ob run
8083```
8184
82−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
85+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
8386
8487Every 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.
8588
89+### Hoogle
90+
91+To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
92+
93+`nix-shell -A shells.ghc --arg withHoogle true`
94+
95+### Adding package overrides
96+
97+To 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:
98+
99+```nix
100+# ...
101+project ./. ({ pkgs, ... }: {
102+# ...
103+ overrides = self: super: let
104+ aeson = pkgs.fetchFromGitHub {
105+ owner = "obsidiansystems";
106+ repo = "aeson-gadt-th";
107+ rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
108+ sha256 = "08q6rnz7w9pn76jkrafig6f50yd0f77z48rk2z5iyyl2jbhcbhx3";
109+ };
110+ in
111+ {
112+ aeson = self.callCabal2nix "aeson" aeson {};
113+ };
114+# ...
115+```
116+
117+For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
118+
86119## Deploying
87120
88121### Locally
@@ -106,7 +139,7 @@ In this section we will demonstrate how to deploy your Obelisk app to an Amazon
106139
107140First create a new EC2 instance:
108141
109−1. Launch a NixOS 17.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-40bee63a))
142+1. Launch a NixOS 18.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-009c9c3f1af480ff3))
1101431. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1111441. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1121451. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
@@ -237,7 +270,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237270
238271### Android
239272
273+NOTE: Currently Android builds are only supported on Linux.
274+
2402751. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
276+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.
2412771. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2422781. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243279
default.nix+17−15
@@ -1,7 +1,7 @@
11{ system ? builtins.currentSystem
22, profiling ? false
33, iosSdkVersion ? "10.2"
4−, __useLegacyCompilers ? false
4+, config ? {}
55}:
66let
77 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,9 +16,8 @@ let
1616 openssh
1717 ];
1818
19− getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
20− getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
21− inherit iosSdkVersion __useLegacyCompilers;
19+ getReflexPlatform = sys: import ./dep/reflex-platform {
20+ inherit iosSdkVersion config;
2221 system = sys;
2322 enableLibraryProfiling = profiling;
2423
@@ -50,11 +49,12 @@ let
5049 } + /template) {};
5150 })
5251
52+ pkgs.obeliskExecutableConfig.haskellOverlay
53+
5354 # Add obelisk packages
5455 (self: super: let
5556 pkgs = self.callPackage ({ pkgs }: pkgs) {};
5657 in {
57− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
5858 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5959
6060 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
@@ -113,7 +113,7 @@ let
113113 ];
114114 };
115115
116− reflex-platform = getReflexPlatform' false system;
116+ reflex-platform = getReflexPlatform system;
117117 inherit (reflex-platform) hackGet nixpkgs;
118118 pkgs = nixpkgs;
119119
@@ -301,20 +301,22 @@ in rec {
301301 injectableConfig = builtins.filterSource (path: _:
302302 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
303303 );
304− __android = configPath: {
304+ __androidWithConfig = configPath: {
305305 ${if android == null then null else frontendName} = {
306306 executableName = "frontend";
307307 ${if builtins.pathExists staticFiles then "assets" else null} =
308− (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath))
309− processedStatic.symlinked;
308+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
309+ (injectableConfig configPath)
310+ processedStatic.symlinked;
310311 } // android;
311312 };
312− __ios = configPath: {
313+ __iosWithConfig = configPath: {
313314 ${if ios == null then null else frontendName} = {
314315 executableName = "frontend";
315316 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
316− (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath))
317− processedStatic.symlinked;
317+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
318+ (injectableConfig configPath)
319+ processedStatic.symlinked;
318320 } // ios;
319321 };
320322 in {
@@ -336,9 +338,9 @@ in rec {
336338 commonName
337339 ];
338340 };
339− android = __android null;
340− ios = __ios null;
341− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
341+ android = __androidWithConfig (base + "/config");
342+ ios = __iosWithConfig (base + "/config");
343+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
342344 };
343345 in mkProject (projectDefinition args));
344346 serverOn = sys: version: serverExe
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "hydration",
5− "rev": "d4b1dcfa6d2795e612b9a4d89c40467afb9fad9d",
6− "sha256": "0wqcvmsivhk9h30p8fmwwp10qn8l8mwfxishpxpxwprc1kh4dbc4"
4+ "branch": "develop",
5+ "rev": "d68ab80e3a92425d4bcbfa2da7c685aded550c16",
6+ "sha256": "0d5hc4v56pldy65rl0pmdzx2jylh8rjqqg5n0qb48039yy3pp8cq"
77}
jobsets.nix+5
@@ -29,6 +29,11 @@ let
2929 emailresponsible = false;
3030 };
3131 };
32+ self-args = {
33+ type = "nix";
34+ value = "{ config.android_sdk.accept_license = true; }";
35+ emailresponsible = false;
36+ };
3237 };
3338 branchJobset = branch: defaults {
3439 description = "obelisk-${branch}";
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+2−1
@@ -49,7 +49,8 @@ toHashedPath root relativePath = do
4949 contents <- LBS.readFile path
5050 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
5151 (dir, filename) = splitFileName relativePath
52− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
5354
5455-- | Convert a ByteString to base 32 in the way that Nix does
5556toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/backend/obelisk-backend.cabal+4
@@ -8,12 +8,16 @@ library
88 build-depends: base,
99 bytestring,
1010 categories,
11+ containers,
12+ cookie,
1113 data-default,
1214 dependent-sum,
1315 dependent-sum-template,
1416 lens,
1517 mtl,
1618 obelisk-asset-serve-snap,
19+ obelisk-executable-config-backend,
20+ obelisk-executable-config-lookup,
1721 obelisk-frontend,
1822 obelisk-route,
1923 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+87−28
@@ -6,11 +6,12 @@
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
88{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
9+{-# OPTIONS_GHC -fno-warn-orphans #-}
1010module Obelisk.Backend
1111 ( Backend (..)
1212 -- * Re-exports
1313 , Default (def)
14+ , ComponentConfigs(..)
1415 , getPageName
1516 , getRouteWith
1617 , runSnapWithCommandLineArgs
@@ -19,6 +20,7 @@ module Obelisk.Backend
1920 , runBackend
2021 , staticRenderContentType
2122 , mkRouteToUrl
23+ , getComponentConfigs
2224 ) where
2325
2426import Prelude hiding (id, (.))
@@ -32,23 +34,35 @@ import Data.Default (Default (..))
3234import Data.Dependent.Sum
3335import Data.Functor.Sum
3436import Data.Functor.Identity
37+import Data.Map (Map)
38+import qualified Data.Map as Map
3539import Data.Monoid ((<>))
3640import Data.Text (Text)
3741import qualified Data.Text as T
3842import Data.Text.Encoding
3943import Obelisk.Asset.Serve.Snap (serveAsset)
44+import Obelisk.ExecutableConfig.Backend
45+import Obelisk.ExecutableConfig.Lookup
4046import Obelisk.Frontend
4147import Obelisk.Route
4248import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4349import Reflex.Dom
4450import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
45− , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
51+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
52+ , rqCookies, Cookie(..))
4653import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4754import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4855
4956data Backend backendRoute frontendRoute = Backend
5057 { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
51− , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
58+ , _backend_run :: ((R backendRoute -> BackendConfigsT Snap ()) -> BackendConfigsT IO ()) -> BackendConfigsT IO ()
59+ }
60+
61+-- | All the configs separated by which context they are accessible in. (Common
62+-- configs are present in both.)
63+data ComponentConfigs = ComponentConfigs
64+ { _componentConfigs_backend :: Map Text Text
65+ , _componentConfigs_frontend :: Map Text Text
5266 }
5367
5468-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
@@ -59,8 +73,17 @@ data GhcjsApp route = GhcjsApp
5973
6074-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
6175--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
62−serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
63−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
76+serveDefaultObeliskApp
77+ :: (MonadSnap m, HasCookies m)
78+ => (R appRoute
79+ -> Text)
80+ -> ([Text]
81+ -> m ())
82+ -> Frontend (R appRoute)
83+ -> Map Text Text
84+ -> R (ObeliskRoute appRoute)
85+ -> m ()
86+serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
6487 where frontendApp = GhcjsApp
6588 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
6689 , _ghcjsApp_value = frontend
@@ -85,16 +108,16 @@ defaultFrontendGhcjsAssets = StaticAssets
85108 , _staticAssets_unprocessed = "frontend.jsexe"
86109 }
87110
88−runSnapWithCommandLineArgs :: Snap () -> IO ()
111+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
89112runSnapWithCommandLineArgs a = do
90113 -- Get the web server configuration from the command line
91− cmdLineConf <- commandLineConfig defaultConfig
114+ cmdLineConf <- liftIO $ commandLineConfig defaultConfig
92115 let httpConf = cmdLineConf
93116 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
94117 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
95118 }
96119 -- Start the web server
97− httpServe httpConf a
120+ liftIO $ httpServe httpConf a
98121
99122getPageName :: (MonadSnap m) => m PageName
100123getPageName = do
@@ -111,12 +134,19 @@ getRouteWith e = do
111134 pageName <- getPageName
112135 return $ tryDecode e pageName
113136
114−serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
115−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
116− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
137+serveObeliskApp
138+ :: (MonadSnap m, HasCookies m)
139+ => (R appRoute -> Text)
140+ -> ([Text] -> m ())
141+ -> GhcjsApp (R appRoute)
142+ -> Map Text Text
143+ -> R (ObeliskRoute appRoute)
144+ -> m ()
145+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
146+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
117147 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
118148 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
119− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
149+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
120150 ResourceRoute_JSaddleWarp :=> Identity _ -> do
121151 let msg = "Error: Obelisk.Backend received jsaddle request"
122152 liftIO $ putStrLn $ T.unpack msg
@@ -140,34 +170,63 @@ staticRenderContentType :: ByteString
140170staticRenderContentType = "text/html; charset=utf-8"
141171
142172--TODO: Don't assume we're being served at "/"
143−serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
144−serveGhcjsApp urlEnc app = \case
173+serveGhcjsApp
174+ :: (MonadSnap m, HasCookies m)
175+ => (R appRouteComponent -> Text)
176+ -> GhcjsApp (R appRouteComponent)
177+ -> Map Text Text
178+ -> R (GhcjsAppRoute appRouteComponent)
179+ -> m ()
180+serveGhcjsApp urlEnc app config = \case
145181 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
146182 modifyResponse $ setContentType staticRenderContentType
147− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
183+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
148184 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
149185
150186runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
151187runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
152188 Left e -> fail $ "backend error:\n" <> T.unpack e
153− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
154− runSnapWithCommandLineArgs $ do
155− getRouteWith validFullEncoder >>= \case
156− Identity r -> case r of
157− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
158− InR obeliskRoute :=> Identity a ->
159− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
189+ Right validFullEncoder -> do
190+ configs <- getComponentConfigs
191+ runBackendConfigsT (_componentConfigs_backend configs) $
192+ _backend_run backend $ \serveRoute -> do
193+ runSnapWithCommandLineArgs $ do
194+ getRouteWith validFullEncoder >>= \case
195+ Identity r -> case r of
196+ InL backendRoute :=> Identity a ->
197+ runBackendConfigsT (_componentConfigs_backend configs) $
198+ serveRoute $ backendRoute :/ a
199+ InR obeliskRoute :=> Identity a ->
200+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
160201
161202mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
162203mkRouteToUrl validFullEncoder =
163204 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
164205 in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
165206
166−
167−renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
168−renderGhcjsFrontend urlEnc route f = do
207+renderGhcjsFrontend
208+ :: (MonadSnap m, HasCookies m)
209+ => (route -> Text)
210+ -> route
211+ -> Map Text Text
212+ -> Frontend route
213+ -> m ByteString
214+renderGhcjsFrontend urlEnc route configs f = do
169215 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
170216 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
171− liftIO $ renderFrontendHtml urlEnc route
172− (_frontend_head f >> ghcjsPreload)
173− (_frontend_body f >> ghcjsScript)
217+ cookies <- askCookies
218+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
219+
220+instance HasCookies Snap where
221+ askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
222+
223+getComponentConfigs :: IO ComponentConfigs
224+getComponentConfigs = do
225+ allConfigs <- getConfigs
226+ return $ ComponentConfigs
227+ { _componentConfigs_backend =
228+ Map.filterWithKey (\k _ -> isMemberOf k ["common", "backend"]) allConfigs
229+ , _componentConfigs_frontend =
230+ Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) allConfigs
231+ }
232+ where isMemberOf k dirs = or $ map (\dir -> ("config/" <> dir) `T.isPrefixOf` k) dirs
lib/cliapp/obelisk-cliapp.cabal+1
@@ -29,5 +29,6 @@ library
2929 Obelisk.CliApp.Process
3030 Obelisk.CliApp.Spinner
3131 Obelisk.CliApp.TerminalString
32+ Obelisk.CliApp.Theme
3233 Obelisk.CliApp.Types
3334 ghc-options: -Wall -fobject-code
lib/cliapp/src/Obelisk/CliApp/Logging.hs+26−2
@@ -39,17 +39,20 @@ import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggi
3939import Control.Monad.Loops (iterateUntil)
4040import Control.Monad.Reader (MonadIO, ReaderT (..))
4141import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
42+import Data.Maybe
4243import Data.Semigroup ((<>))
4344import Data.Text (Text)
4445import qualified Data.Text as T
4546import qualified Data.Text.IO as T
47+import GHC.IO.Encoding.Types
4648import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
4749 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
4850 SGR (SetColor, SetConsoleIntensity), clearLine)
4951import System.Exit (ExitCode (..))
50−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
52+import System.IO
5153
5254import qualified Obelisk.CliApp.TerminalString as TS
55+import Obelisk.CliApp.Theme
5356import Obelisk.CliApp.Types
5457
5558newCliConfig
@@ -63,7 +66,11 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
6366 lock <- newMVar False
6467 tipDisplayed <- newIORef False
6568 stack <- newIORef ([], [])
66− return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
69+ textEncoding <- hGetEncoding stdout
70+ let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
71+ then unicodeTheme
72+ else noUnicodeTheme
73+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
6774
6875runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
6976runCli c =
@@ -226,3 +233,20 @@ fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
226233fork f = do
227234 c <- getCliConfig
228235 liftIO $ forkIO $ runCli c f
236+
237+-- | Conservatively determines whether the encoding supports Unicode.
238+--
239+-- Currently this uses a whitelist of known-to-work encodings. In principle it
240+-- could test dynamically by opening a file with this encoding, but it doesn't
241+-- look like base exposes any way to determine this in a pure fashion.
242+supportsUnicode :: TextEncoding -> Bool
243+supportsUnicode enc = any ((textEncodingName enc ==) . textEncodingName)
244+ [ utf8
245+ , utf8_bom
246+ , utf16
247+ , utf16be
248+ , utf16le
249+ , utf32
250+ , utf32be
251+ , utf32le
252+ ]
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−17
@@ -24,6 +24,7 @@ import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
2424
2525import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
2626import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27+import Obelisk.CliApp.Theme
2728import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
2829
2930-- | Run an action with a CLI spinner.
@@ -51,7 +52,8 @@ withSpinner'
5152 -> m a
5253 -> m a
5354withSpinner' msg mkTrail action = do
54− noSpinner <- _cliConfig_noSpinner <$> getCliConfig
55+ cliConf <- getCliConfig
56+ let noSpinner = _cliConfig_noSpinner cliConf
5557 if noSpinner
5658 then putLog Notice msg >> action
5759 else bracket' run cleanup $ const action
@@ -61,8 +63,11 @@ withSpinner' msg mkTrail action = do
6163 modifyStack pushSpinner >>= \case
6264 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
6365 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
66+ cliConf <- getCliConfig
67+ let theme = _cliConfig_theme cliConf
68+ spinner = coloredSpinner $ _cliTheme_spinner theme
6469 spinnerThread <- fork $ runSpinner spinner $ \c -> do
65− logs <- renderSpinnerStack c . snd <$> readStack
70+ logs <- renderSpinnerStack theme c . snd <$> readStack
6671 logMessage $ Output_Overwrite logs
6772 pure [ctrleThread, spinnerThread]
6873 False -> -- Sub-spinner; nothing to do.
@@ -70,13 +75,15 @@ withSpinner' msg mkTrail action = do
7075 cleanup tids resultM = do
7176 liftIO $ mapM_ killThread tids
7277 logMessage Output_ClearLine
73− logsM <- modifyStack $ popSpinner $ case resultM of
78+ cliConf <- getCliConfig
79+ let theme = _cliConfig_theme cliConf
80+ logsM <- modifyStack $ (popSpinner theme) $ case resultM of
7481 Nothing ->
75− ( TerminalString_Colorized Red "✖"
82+ ( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
7683 , Just msg -- Always display final message if there was an exception.
7784 )
7885 Just result ->
79− ( TerminalString_Colorized Green "✔"
86+ ( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
8087 , mkTrail <*> pure result
8188 )
8289 -- Last message, finish off with newline.
@@ -87,10 +94,10 @@ withSpinner' msg mkTrail action = do
8794 )
8895 where
8996 isTemporary = isNothing mkTrail
90− popSpinner (mark, trailMsgM) (flag, old) =
97+ popSpinner theme (mark, trailMsgM) (flag, old) =
9198 ( (newFlag, new)
9299 -- With final trail spinner message to render
93− , renderSpinnerStack mark . (: new) . TerminalString_Normal <$> (
100+ , renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
94101 if inTemporarySpinner then Nothing else trailMsgM
95102 )
96103 )
@@ -102,19 +109,19 @@ withSpinner' msg mkTrail action = do
102109 =<< fmap _cliConfig_spinnerStack getCliConfig
103110 modifyStack f = liftIO . flip atomicModifyIORef' f
104111 =<< fmap _cliConfig_spinnerStack getCliConfig
105− spinner = coloredSpinner defaultSpinnerTheme
106112
107113-- | How nested spinner logs should be displayed
108114renderSpinnerStack
109− :: TerminalString -- ^ That which comes before the final element in stack
115+ :: CliTheme
116+ -> TerminalString -- ^ That which comes before the final element in stack
110117 -> [TerminalString] -- ^ Spinner elements in reverse order
111118 -> [TerminalString]
112−renderSpinnerStack mark = L.intersperse space . go . L.reverse
119+renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
113120 where
114121 go [] = []
115122 go (x:[]) = mark : [x]
116123 go (x:xs) = arrow : x : go xs
117− arrow = TerminalString_Colorized Blue "⇾"
124+ arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
118125 space = TerminalString_Normal " "
119126
120127-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
@@ -130,12 +137,6 @@ runSpinner spinner f = forM_ spinner $ f >=> const delay
130137 where
131138 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
132139
133−type SpinnerTheme = [Text]
134−
135−-- Find more spinners at https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
136−defaultSpinnerTheme :: SpinnerTheme
137−defaultSpinnerTheme = ["◐", "◓", "◑", "◒"]
138−
139140-- | Like `bracket` but the `release` function can know whether an exception was raised
140141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
141142bracket' acquire release use = mask $ \unmasked -> do
lib/cliapp/src/Obelisk/CliApp/Theme.hsnew file+29
@@ -0,0 +1,29 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Obelisk.CliApp.Theme where
3+
4+import Data.Text (Text)
5+
6+data CliTheme = CliTheme
7+ { _cliTheme_done :: Text
8+ , _cliTheme_failed :: Text
9+ , _cliTheme_arrow :: Text
10+ , _cliTheme_spinner :: SpinnerTheme
11+ }
12+
13+type SpinnerTheme = [Text]
14+
15+unicodeTheme :: CliTheme
16+unicodeTheme = CliTheme
17+ { _cliTheme_done = "✔"
18+ , _cliTheme_failed = "✖"
19+ , _cliTheme_arrow = "⇾"
20+ , _cliTheme_spinner = ["◐", "◓", "◑", "◒"]
21+ }
22+
23+noUnicodeTheme :: CliTheme
24+noUnicodeTheme = CliTheme
25+ { _cliTheme_done = "DONE"
26+ , _cliTheme_failed = "FAILED"
27+ , _cliTheme_arrow = "->"
28+ , _cliTheme_spinner = ["|", "/", "-", "\\"]
29+ }
lib/cliapp/src/Obelisk/CliApp/Types.hs+3
@@ -23,6 +23,7 @@ import Data.Text (Text)
2323import System.Exit (ExitCode (..), exitWith)
2424
2525import Obelisk.CliApp.TerminalString (TerminalString)
26+import Obelisk.CliApp.Theme (CliTheme)
2627
2728--------------------------------------------------------------------------------
2829
@@ -88,6 +89,8 @@ data CliConfig e = CliConfig
8889 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
8990 , -- | Failure handler. How to log error and what exit status to use.
9091 _cliConfig_errorLogExitCode :: e -> (Text, Int)
92+ , -- | Theme strings for spinners
93+ _cliConfig_theme :: CliTheme
9194 }
9295
9396class Monad m => HasCliConfig e m | m -> e where
lib/command/src/Obelisk/Command.hs+7−4
@@ -213,14 +213,17 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
213213data ThunkCommand
214214 = ThunkCommand_Update [FilePath] (Maybe String)
215215 | ThunkCommand_Unpack [FilePath]
216− | ThunkCommand_Pack [FilePath]
216+ | ThunkCommand_Pack [FilePath] Bool
217217 deriving Show
218218
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+
219222thunkCommand :: Parser ThunkCommand
220223thunkCommand = hsubparser $ mconcat
221224 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
222225 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
223− , 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"
224227 ]
225228
226229data ShellOpts
@@ -342,7 +345,7 @@ ob = \case
342345 Right (ThunkData_Packed ptr) -> return ptr
343346 Right (ThunkData_Checkout (Just ptr)) -> return ptr
344347 Right (ThunkData_Checkout Nothing) ->
345− getThunkPtr' False root
348+ getThunkPtr False root
346349 let sshKeyPath = _deployInitOpts_sshKey deployOpts
347350 hostname = _deployInitOpts_hostname deployOpts
348351 route = _deployInitOpts_route deployOpts
@@ -361,7 +364,7 @@ ob = \case
361364 ObCommand_Thunk tc -> case tc of
362365 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
363366 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
364− ThunkCommand_Pack thunks -> forM_ thunks packThunk
367+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
365368 ObCommand_Repl -> runRepl
366369 ObCommand_Watch -> inNixShell' $ static runWatch
367370 ObCommand_Shell so -> withProjectRoot "." $ \root ->
lib/command/src/Obelisk/Command/Deploy.hs+4−4
@@ -108,7 +108,7 @@ deployPush deployPath getNixBuilders = do
108108 Right (ThunkData_Packed ptr) -> return ptr
109109 Right (ThunkData_Checkout _) -> do
110110 checkGitCleanStatus srcPath True >>= \case
111− True -> packThunk srcPath
111+ True -> packThunk False srcPath
112112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
113113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -136,7 +136,7 @@ deployPush deployPath getNixBuilders = do
136136 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
137137 callProcess'
138138 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
139− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
139+ "nix-copy-closure" ["-v", "--to", "--use-substitutes", "root@" <> host, "--gzip", outputPath]
140140 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
141141 callProcessAndLogOutput (Notice, Warning) $
142142 proc "rsync"
@@ -234,7 +234,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
234234 [ "with (import ", srcDir, " {});"
235235 , "android.frontend.override (drv: { "
236236 , "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;"
238238 , "})"
239239 ]
240240 return $ Target
@@ -245,7 +245,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
245245 IOS -> do
246246 let expr = mconcat
247247 [ "with (import ", srcDir, " {});"
248− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
249249 ]
250250 return $ Target
251251 { _target_path = Nothing
lib/command/src/Obelisk/Command/Run.hs+15−2
@@ -10,8 +10,8 @@ import Control.Monad
1010import Control.Monad.IO.Class (liftIO)
1111import Control.Monad.Reader (MonadIO)
1212import Data.Either
13−import Data.List
14−import Data.List.NonEmpty as NE
13+import qualified Data.List as L
14+import qualified Data.List.NonEmpty as NE
1515import Data.Maybe
1616import qualified Data.Text as T
1717import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
@@ -46,6 +46,8 @@ data CabalPackageInfo = CabalPackageInfo
4646 -- ^ List of hs src dirs of the library component
4747 , _cabalPackageInfo_defaultExtensions :: [Extension]
4848 -- ^ List of globally enable extensions of the library component
49+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
50+ -- ^ List of globally set languages of the library component
4951 }
5052
5153-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -136,6 +138,8 @@ parseCabalPackage dir = do
136138 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
137139 , _cabalPackageInfo_defaultExtensions =
138140 defaultExtensions $ libBuildInfo lib
141+ , _cabalPackageInfo_defaultLanguage =
142+ defaultLanguage $ libBuildInfo lib
139143 }
140144 Left (_, errors) -> do
141145 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -165,17 +169,22 @@ withGhciScript pkgs f = do
165169 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
166170
167171 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
172+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
173+ -- NOTE when no default-language is present cabal sets Haskell98
174+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
168175 extensionsLine = if extensions == mempty
169176 then ""
170177 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
171178 dotGhci = unlines $
172179 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
173180 , extensionsLine
181+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
174182 , ":load Backend Frontend"
175183 , "import Obelisk.Run"
176184 , "import qualified Frontend"
177185 , "import qualified Backend"
178186 ]
187+ warnDifferentLanguages language
179188 withSystemTempDirectory "ob-ghci" $ \fp -> do
180189 let dotGhciPath = fp </> ".ghci"
181190 liftIO $ writeFile dotGhciPath dotGhci
@@ -185,6 +194,10 @@ withGhciScript pkgs f = do
185194 rootedSourceDirs pkg = NE.toList $
186195 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
187196
197+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
198+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
199+warnDifferentLanguages _ = return ()
200+
188201-- | Run ghci repl
189202runGhciRepl
190203 :: MonadObelisk m
lib/command/src/Obelisk/Command/Thunk.hs+7−12
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
2626 , readThunk
2727 , updateThunk
2828 , getThunkPtr
29− , getThunkPtr'
3029 , parseGitUri
3130 , uriThunkPtr
3231 ) where
@@ -573,7 +572,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
573572 return tmpThunk
574573 Right _ -> failWith $ "Thunk is not packed"
575574 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
575+ _ <- packThunk' True False p'
577576 callProcessAndLogOutput (Notice, Error) $
578577 proc "cp" ["-r", "-T", p', p]
579578
@@ -622,28 +621,24 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
622621 callProcessAndLogOutput (Notice, Error) $
623622 proc "mv" ["-T", tmpRepo, thunkDir]
624623
625−--TODO: add force mode to pack even if changes are present
626624--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
628626packThunk = packThunk' False
629627
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
628+packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
629+packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632630 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633631 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634632 Right (ThunkData_Checkout _) -> do
635633 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636634 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
635+ thunkPtr <- getThunkPtr (not force) thunkDir
638636 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639637 liftIO $ createThunk thunkDir thunkPtr
640638 pure thunkPtr
641639
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
643−getThunkPtr = getThunkPtr' True
644−
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
640+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641+getThunkPtr checkClean thunkDir = do
647642 when checkClean $ ensureCleanGitRepo thunkDir True $
648643 "thunk pack: thunk checkout contains unsaved modifications"
649644
lib/executable-config/backend/obelisk-executable-config-backend.cabalnew file+20
@@ -0,0 +1,20 @@
1+name: obelisk-executable-config-backend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ containers,
10+ monad-control,
11+ mtl,
12+ primitive,
13+ ref-tf,
14+ snap-core,
15+ text,
16+ transformers,
17+ transformers-base
18+ exposed-modules:
19+ Obelisk.ExecutableConfig.Backend
20+ ghc-options: -Wall
lib/executable-config/backend/src/Obelisk/ExecutableConfig/Backend.hsnew file+77
@@ -0,0 +1,77 @@
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.ExecutableConfig.Backend
14+ ( HasBackendConfigs(..)
15+ , BackendConfigsT
16+ , runBackendConfigsT
17+ , mapBackendConfigsT
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.Map (Map)
31+import qualified Data.Map as Map
32+import Data.Text (Text)
33+import Snap.Core (MonadSnap)
34+
35+class Monad m => HasBackendConfigs m where
36+ getBackendConfig :: Text -> m (Maybe Text)
37+ default getBackendConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
38+ getBackendConfig = lift . getBackendConfig
39+ getCommonConfig :: Text -> m (Maybe Text)
40+ default getCommonConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
41+ getCommonConfig = lift . getCommonConfig
42+
43+newtype BackendConfigsT m a = BackendConfigsT { unBackendConfigsT :: ReaderT (Map Text Text) m a }
44+ deriving
45+ ( Functor
46+ , Applicative
47+ , Monad
48+ , MonadPlus
49+ , Alternative
50+ , MonadFix
51+ , MonadIO
52+ , MonadBase m'
53+ , MonadBaseControl m'
54+ , MonadRef
55+ , MonadTrans
56+ , MonadSnap
57+ )
58+
59+instance PrimMonad m => PrimMonad (BackendConfigsT m) where
60+ type PrimState (BackendConfigsT m) = PrimState m
61+ primitive = lift . primitive
62+
63+runBackendConfigsT
64+ :: Map Text Text
65+ -> BackendConfigsT m a
66+ -> m a
67+runBackendConfigsT cs child = runReaderT (unBackendConfigsT child) cs
68+
69+instance Monad m => HasBackendConfigs (BackendConfigsT m) where
70+ getBackendConfig k = BackendConfigsT $ Map.lookup ("config/backend/" <> k) <$> ask
71+ getCommonConfig k = BackendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
72+
73+mapBackendConfigsT
74+ :: (forall x. m x -> n x)
75+ -> BackendConfigsT m a
76+ -> BackendConfigsT n a
77+mapBackendConfigsT f (BackendConfigsT x) = BackendConfigsT $ mapReaderT f x
lib/executable-config/default.nix+6−1
@@ -16,9 +16,14 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
1616 exit 1
1717 fi
1818 cp -a "${config}"/* "$out/config"
19+ (cd $out && find config -type f -print0 >config.files)
1920 '');
2021in with nixpkgs.haskell.lib; {
21− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
22+ haskellOverlay = self: super: {
23+ obelisk-executable-config-backend = self.callCabal2nix "obelisk-executable-config-backend" (filterGitSource ./backend) {};
24+ obelisk-executable-config-frontend = self.callCabal2nix "obelisk-executable-config-frontend" (filterGitSource ./frontend) {};
25+ obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
26+ };
2227 platforms = {
2328 android = {
2429 # Inject the given config directory into an android assets folder
lib/executable-config/frontend/obelisk-executable-config-frontend.cabalnew file+21
@@ -0,0 +1,21 @@
1+name: obelisk-executable-config-frontend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ containers,
10+ ghcjs-dom,
11+ jsaddle,
12+ mtl,
13+ primitive,
14+ ref-tf,
15+ reflex,
16+ reflex-dom-core,
17+ text,
18+ transformers
19+ exposed-modules:
20+ Obelisk.ExecutableConfig.Frontend
21+ ghc-options: -Wall
lib/executable-config/frontend/src/Obelisk/ExecutableConfig/Frontend.hsnew file+115
@@ -0,0 +1,115 @@
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.ExecutableConfig.Frontend
14+ ( HasFrontendConfigs(..)
15+ , FrontendConfigsT
16+ , runFrontendConfigsT
17+ , mapFrontendConfigsT
18+ ) where
19+
20+import Control.Monad.Fix
21+import Control.Monad.Primitive
22+import Control.Monad.Ref
23+import Control.Monad.IO.Class
24+import Control.Monad.Trans.Class
25+import Control.Monad.Trans.Reader
26+import Data.Map (Map)
27+import qualified Data.Map as Map
28+import Data.Text (Text)
29+import Reflex
30+import Reflex.Host.Class
31+import Reflex.Dom.Core
32+#ifndef ghcjs_HOST_OS
33+import GHCJS.DOM.Types (MonadJSM)
34+#endif
35+
36+class Monad m => HasFrontendConfigs m where
37+ getFrontendConfig :: Text -> m (Maybe Text)
38+ default getFrontendConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
39+ getFrontendConfig = lift . getFrontendConfig
40+ getCommonConfig :: Text -> m (Maybe Text)
41+ default getCommonConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
42+ getCommonConfig = lift . getCommonConfig
43+
44+instance HasFrontendConfigs m => HasFrontendConfigs (BehaviorWriterT t w m)
45+instance HasFrontendConfigs m => HasFrontendConfigs (DynamicWriterT t w m)
46+instance HasFrontendConfigs m => HasFrontendConfigs (EventWriterT t w m)
47+instance HasFrontendConfigs m => HasFrontendConfigs (PostBuildT t m)
48+instance HasFrontendConfigs m => HasFrontendConfigs (QueryT t q m)
49+instance HasFrontendConfigs m => HasFrontendConfigs (ReaderT r m)
50+instance HasFrontendConfigs m => HasFrontendConfigs (RequesterT t request response m)
51+instance HasFrontendConfigs m => HasFrontendConfigs (StaticDomBuilderT t m)
52+instance HasFrontendConfigs m => HasFrontendConfigs (TriggerEventT t m)
53+
54+newtype FrontendConfigsT m a = FrontendConfigsT { unFrontendConfigsT :: ReaderT (Map Text Text) m a }
55+ deriving
56+ ( Functor
57+ , Applicative
58+ , DomBuilder t
59+ , Monad
60+ , MonadFix
61+ , MonadHold t
62+ , MonadIO
63+#ifndef ghcjs_HOST_OS
64+ , MonadJSM
65+#endif
66+ , MonadRef
67+ , MonadReflexCreateTrigger t
68+ , MonadSample t
69+ , MonadTrans
70+ , NotReady t
71+ , PostBuild t
72+ , TriggerEvent t
73+ , HasDocument
74+ , DomRenderHook t
75+ , HasJSContext
76+ , HasJS js
77+ )
78+
79+instance PerformEvent t m => PerformEvent t (FrontendConfigsT m) where
80+ type Performable (FrontendConfigsT m) = FrontendConfigsT (Performable m)
81+ performEvent e = FrontendConfigsT $ ReaderT $ \configs ->
82+ performEvent $ runFrontendConfigsT configs <$> e
83+ performEvent_ e = FrontendConfigsT $ ReaderT $ \configs ->
84+ performEvent_ $ runFrontendConfigsT configs <$> e
85+
86+instance Adjustable t m => Adjustable t (FrontendConfigsT m) where
87+ runWithReplace a e = FrontendConfigsT $ runWithReplace (unFrontendConfigsT a) (unFrontendConfigsT <$> e)
88+ traverseDMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
89+ traverseIntMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
90+ traverseDMapWithKeyWithAdjustWithMove f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFrontendConfigsT $ f k v) m e
91+
92+instance Prerender js t m => Prerender js t (FrontendConfigsT m) where
93+ type Client (FrontendConfigsT m) = FrontendConfigsT (Client m)
94+ prerender server client = FrontendConfigsT $ ReaderT $ \configs ->
95+ prerender (runFrontendConfigsT configs server) (runFrontendConfigsT configs client)
96+
97+instance PrimMonad m => PrimMonad (FrontendConfigsT m) where
98+ type PrimState (FrontendConfigsT m) = PrimState m
99+ primitive = lift . primitive
100+
101+runFrontendConfigsT
102+ :: Map Text Text
103+ -> FrontendConfigsT m a
104+ -> m a
105+runFrontendConfigsT cs child = runReaderT (unFrontendConfigsT child) cs
106+
107+instance Monad m => HasFrontendConfigs (FrontendConfigsT m) where
108+ getFrontendConfig k = FrontendConfigsT $ Map.lookup ("config/frontend/" <> k) <$> ask
109+ getCommonConfig k = FrontendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
110+
111+mapFrontendConfigsT
112+ :: (forall x. m x -> n x)
113+ -> FrontendConfigsT m a
114+ -> FrontendConfigsT n a
115+mapFrontendConfigsT f (FrontendConfigsT x) = FrontendConfigsT $ mapReaderT f x
lib/executable-config/inject/obelisk-executable-config-inject.cabal+1−3
@@ -8,9 +8,7 @@ library
88
99 build-depends:
1010 base,
11− bytestring,
12− directory,
13− filepath,
11+ containers,
1412 reflex-dom-core,
1513 text
1614
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+8−37
@@ -2,50 +2,21 @@
22module Obelisk.ExecutableConfig.Inject where
33
44import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
5+import Control.Monad.IO.Class (MonadIO)
6+import Data.Map (Map)
7+import qualified Data.Map as Map
68import Data.Semigroup ((<>))
7−import Data.ByteString (ByteString)
89import Data.Text (Text)
9−import qualified Data.Text as T
10−import qualified Data.Text.IO as T
1110import 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
2111
2212-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
2313-- whose value is the provided configuration
2414injectPure :: DomBuilder t m => Text -> Text -> m ()
2515injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
16+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key)
2717 in elAttr "script" attrs $ text value
2818
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−
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
39−getConfigs fp = liftIO $ do
40− dir <- doesDirectoryExist fp
41− if dir
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 []
19+-- | Produces injectable @<script>@ tags containing the configuration keys
20+-- (filepaths) and values.
21+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text Text -> m ()
22+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
lib/executable-config/lookup/default.nix+1−1
@@ -6,7 +6,7 @@ let isAndroid = hostPlatform.libc == "bionic";
66 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
77 isGhcjs = ghc.isGhcjs or false;
88in mkDerivation {
9− pname = "obelisk-executable-config";
9+ pname = "obelisk-executable-config-lookup";
1010 version = "0.1";
1111 src = ./.;
1212 libraryHaskellDepends = [
lib/executable-config/lookup/obelisk-executable-config.cabal → lib/executable-config/lookup/obelisk-executable-config-lookup.cabalrenamed+11−4
@@ -1,13 +1,15 @@
1−name: obelisk-executable-config
1+name: obelisk-executable-config-lookup
22version: 0.1.1
33cabal-version: >= 1.2
44build-type: Simple
55
66library
7− exposed-modules: Obelisk.ExecutableConfig
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup
88 if os(ios)
9− hs-source-dirs: src-ios
9+ hs-source-dirs: src-ios, src-directory
10+ other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
1011 build-depends: jsaddle-wkwebview
12+ build-depends: directory
1113 else
1214 if os(linux-android)
1315 hs-source-dirs: src-android
@@ -20,11 +22,16 @@ library
2022 hs-source-dirs: src-ghcjs
2123 build-depends: ghcjs-dom
2224 else
23− hs-source-dirs: src
25+ hs-source-dirs: src, src-directory
26+ other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
27+ build-depends: directory
2428
2529 build-depends:
2630 base,
2731 bytestring,
32+ containers,
2833 filepath,
2934 text,
3035 transformers
36+
37+ ghc-options: -Wall
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+56
@@ -0,0 +1,56 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Control.Exception (bracket)
5+import Control.Monad (forM)
6+import qualified Data.ByteString as BS
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Text (Text)
10+import qualified Data.Text.Encoding as T
11+import Foreign.C.String (withCString)
12+import Foreign.Ptr (nullPtr)
13+
14+import Obelisk.ExecutableConfig.Internal.AssetManager
15+
16+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe Text)
17+getFromMgr mgr name = do
18+ let open = do
19+ a <- BS.useAsCString name $ \fn ->
20+ assetManager_open mgr fn 3
21+ return $ if unAAsset a == nullPtr
22+ then Nothing
23+ else Just a
24+ close = mapM_ asset_close
25+ bracket open close $ mapM $ \asset -> do
26+ b <- asset_getBuffer asset
27+ l <- asset_getLength asset
28+ fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
29+
30+getConfigs :: IO (Map Text Text)
31+getConfigs = bracket getAssets freeAssetManager $ \mgrObj -> do
32+ mgr <- assetManagerFromJava mgrObj
33+ let openDir = do
34+ d <- withCString "config.files" $ \fn ->
35+ assetManager_open mgr fn 3
36+ return $ if unAAsset d == nullPtr
37+ then Nothing
38+ else Just d
39+ closeDir = mapM_ asset_close
40+ configPaths <- bracket openDir closeDir $ \case
41+ Just asset -> do
42+ b <- asset_getBuffer asset
43+ l <- asset_getLength asset
44+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
45+ Nothing -> error "could not open configuration manifest 'config.files'"
46+ fmap Map.fromList $ forM configPaths $ \fp ->
47+ getFromMgr mgr fp >>= \case
48+ Just v -> return (T.decodeUtf8 fp, v)
49+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
50+
51+lines0 :: BS.ByteString -> [BS.ByteString]
52+lines0 ps
53+ | BS.null ps = []
54+ | otherwise = case BS.elemIndex 0 ps of
55+ Nothing -> [ps]
56+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-directory/Obelisk/ExecutableConfig/Internal/ConfigDirectory.hsnew file+24
@@ -0,0 +1,24 @@
1+module Obelisk.ExecutableConfig.Internal.ConfigDirectory where
2+
3+import Data.Map (Map)
4+import qualified Data.Map as Map
5+import Data.Text (Text)
6+import Data.Text as T
7+import Data.Text.IO as T
8+import System.Directory
9+import System.FilePath.Posix ((</>))
10+
11+getConfigsFromDirectory :: FilePath -> FilePath -> IO (Map Text Text)
12+getConfigsFromDirectory base fp = do
13+ dir <- doesDirectoryExist (base </> fp)
14+ if dir
15+ then do
16+ ps <- listDirectory (base </> fp)
17+ fmap mconcat $ mapM (\p -> getConfigsFromDirectory base $ fp </> p) ps
18+ else do
19+ file <- doesFileExist (base </> fp)
20+ if file
21+ then do
22+ f <- T.readFile (base </> fp)
23+ return $ Map.singleton (T.pack fp) f
24+ else return mempty
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+36
@@ -0,0 +1,36 @@
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.Maybe (catMaybes)
8+import Data.Text (Text)
9+import Data.Traversable (for)
10+import GHCJS.DOM
11+import GHCJS.DOM.Document (getHead)
12+import GHCJS.DOM.DOMStringMap (get)
13+import GHCJS.DOM.Element (getInnerHTML)
14+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
15+import GHCJS.DOM.NodeList (item, getLength)
16+import GHCJS.DOM.ParentNode (querySelectorAll)
17+import GHCJS.DOM.Types (Node(Node), castTo)
18+
19+getConfigs :: IO (Map Text Text)
20+getConfigs = do
21+ Just doc <- currentDocument
22+ Just hd <- getHead doc
23+ nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
24+ fmap Map.fromList $ for nodes $ \node -> do
25+ e <- castTo HTMLElement node >>= \case
26+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
27+ Just htmlE -> return htmlE
28+ dataset <- getDataset e
29+ (,)
30+ <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
31+ <*> getInnerHTML e
32+ where
33+ collToList es = do
34+ len <- getLength es
35+ list <- traverse (item es) [0..len-1]
36+ pure $ catMaybes list
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+15
@@ -0,0 +1,15 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.Map (Map)
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
8+import Language.Javascript.JSaddle.WKWebView
9+
10+import Obelisk.ExecutableConfig.Internal.ConfigDirectory
11+
12+getConfigs :: IO (Map Text Text)
13+getConfigs = mainBundleResourcePath >>= \case
14+ Nothing -> error "Could not get bundle resource path."
15+ Just p -> getConfigsFromDirectory (T.unpack (T.decodeUtf8 p)) "config"
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/executable-config/lookup/src/Obelisk/ExecutableConfig/Lookup.hsnew file+9
@@ -0,0 +1,9 @@
1+module Obelisk.ExecutableConfig.Lookup where
2+
3+import Data.Map (Map)
4+import Data.Text (Text)
5+
6+import Obelisk.ExecutableConfig.Internal.ConfigDirectory
7+
8+getConfigs :: IO (Map Text Text)
9+getConfigs = getConfigsFromDirectory "." "config"
lib/frontend/obelisk-frontend.cabal+7−1
@@ -7,12 +7,16 @@ library
77 hs-source-dirs: src
88 build-depends: base,
99 bytestring,
10+ containers,
11+ cookie,
1012 dependent-sum,
1113 ghcjs-dom,
1214 jsaddle,
1315 lens,
1416 mtl,
17+ obelisk-executable-config-frontend,
1518 obelisk-executable-config-inject,
19+ obelisk-executable-config-lookup,
1620 obelisk-route,
1721 primitive,
1822 ref-tf,
@@ -20,5 +24,7 @@ library
2024 reflex-dom-core,
2125 text,
2226 transformers
23− exposed-modules: Obelisk.Frontend
27+ exposed-modules:
28+ Obelisk.Frontend
29+ Obelisk.Frontend.Cookie
2430 ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hs+81−46
@@ -1,4 +1,5 @@
11{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE CPP #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
45{-# LANGUAGE LambdaCase #-}
@@ -14,40 +15,43 @@ module Obelisk.Frontend
1415 ( ObeliskWidget
1516 , Frontend (..)
1617 , runFrontend
18+ , runFrontendWithConfigs
1719 , renderFrontendHtml
20+ , removeHTMLConfigs
21+ , module Obelisk.Frontend.Cookie
1822 ) where
1923
2024import Prelude hiding ((.))
2125
2226import Control.Category
23−import Control.Concurrent
2427import Control.Lens
25−import Control.Monad hiding (sequence, sequence_)
2628import Control.Monad.Fix
2729import Control.Monad.IO.Class
2830import Control.Monad.Primitive
2931import Control.Monad.Reader
3032import Control.Monad.Ref
31−import Control.Monad.Trans.Class
33+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
3234import Data.ByteString (ByteString)
33−import Data.Dependent.Sum (DSum (..))
3435import Data.Foldable (for_)
3536import Data.Functor.Sum
36−import Data.IORef
37+import Data.Map (Map)
38+import Data.Maybe (catMaybes)
3739import Data.Monoid ((<>))
3840import Data.Text (Text)
39−import Data.Foldable (sequence_)
40−import Data.Traversable (sequence)
41−import GHCJS.DOM hiding (bracket, catch)
42−import GHCJS.DOM.Document
43−import GHCJS.DOM.Node
44−import qualified GHCJS.DOM.Types as DOM
41+import GHCJS.DOM (currentDocument)
42+import GHCJS.DOM.Document (getHead)
43+import GHCJS.DOM.Node (removeChild_)
44+import GHCJS.DOM.NodeList (item, getLength)
45+import GHCJS.DOM.ParentNode (querySelectorAll)
4546import Language.Javascript.JSaddle (JSM)
47+import Obelisk.Frontend.Cookie
4648import Obelisk.Route.Frontend
4749import Reflex.Dom.Core
4850import Reflex.Host.Class
49−import qualified Reflex.TriggerEvent.Base as TriggerEvent
51+import Obelisk.ExecutableConfig.Frontend
5052import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
53+import Obelisk.ExecutableConfig.Lookup (getConfigs)
54+import Web.Cookie
5155
5256makePrisms ''Sum
5357
@@ -70,15 +74,16 @@ type ObeliskWidget js t route m =
7074 , Prerender js t m
7175 , PrebuildAgnostic t route m
7276 , PrebuildAgnostic t route (Client m)
73− -- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
74− , MonadIO m
75− , MonadIO (Performable m)
77+ , HasFrontendConfigs m
78+ , HasCookies m
7679 )
7780
7881type PrebuildAgnostic t route m =
7982 ( SetRoute t route m
8083 , RouteToUrl route m
8184 , MonadFix m
85+ , HasFrontendConfigs m
86+ , HasFrontendConfigs (Performable m)
8287 )
8388
8489data Frontend route = Frontend
@@ -86,51 +91,81 @@ data Frontend route = Frontend
8691 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
8792 }
8893
89−runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
94+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
95+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
96+
97+removeHTMLConfigs :: JSM ()
98+removeHTMLConfigs = void $ runMaybeT $ do
99+ doc <- MaybeT currentDocument
100+ hd <- MaybeT $ getHead doc
101+ es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
102+ for_ es $ removeChild_ hd
103+ where
104+ collToList es = do
105+ len <- getLength es
106+ lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
107+ pure $ catMaybes lst
108+
109+runFrontend
110+ :: forall backendRoute route
111+ . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
112+ -> Frontend (R route)
113+ -> JSM ()
90114runFrontend validFullEncoder frontend = do
115+ configs <- liftIO getConfigs
116+#ifdef ghcjs_HOST_OS
117+ removeHTMLConfigs
118+#endif
119+ runFrontendWithConfigs configs validFullEncoder frontend
120+
121+runFrontendWithConfigs
122+ :: forall backendRoute route
123+ . Map Text Text
124+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
125+ -> Frontend (R route)
126+ -> JSM ()
127+runFrontendWithConfigs configs validFullEncoder frontend = do
91128 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
92129 errorLeft = \case
93130 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
94131 Right x -> Identity x
95− runMyRouteViewT
96− :: ( TriggerEvent t m
97− , PerformEvent t m
98− , MonadHold t m
99− , DOM.MonadJSM m
100− , DOM.MonadJSM (Performable m)
101− , MonadFix m
102− , MonadFix (Performable m)
103− )
104− => Event t ()
105− -> RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
106− -> m a
107− runMyRouteViewT = runRouteViewT ve
108132 runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
109− rec switchover <- runMyRouteViewT switchover $ do
110− (switchover, fire) <- newTriggerEvent
111− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
133+ rec switchover <- runRouteViewT ve switchover $ do
134+ (switchover'', fire) <- newTriggerEvent
135+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runFrontendConfigsT configs))) $ do
136+ -- The order here is important - baseTag has to be before headWidget!
137+ baseTag
112138 _frontend_head frontend
113− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
139+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runFrontendConfigsT configs))) $ do
114140 _frontend_body frontend
115− switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
141+ switchover' <- lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
116142 performEvent_ $ liftIO (fire ()) <$ switchover'
117− pure switchover
143+ pure switchover''
118144 pure ()
119145
120146renderFrontendHtml
121− :: (t ~ DomTimeline)
122− => (r' -> Text)
147+ :: ( t ~ DomTimeline
148+ , MonadIO m
149+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (FrontendConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
150+ )
151+ => Map Text Text
152+ -> Cookies
153+ -> (r -> Text)
123154 -> r
124− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
125− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
126− -> IO ByteString
127−renderFrontendHtml urlEnc route headWidget bodyWidget = do
155+ -> Frontend r
156+ -> widget ()
157+ -> widget ()
158+ -> m ByteString
159+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
128160 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
129− html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
161+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runFrontendConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
130162 el "html" $ do
131163 el "head" $ do
132− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
133− -- The order here is important - baseTag has to be before headWidget!
134− baseTag >> injectExecutableConfigs >> headWidget
135− el "body" bodyWidget
164+ baseTag
165+ injectExecutableConfigs configs
166+ _frontend_head frontend
167+ headExtra
168+ el "body" $ do
169+ _frontend_body frontend
170+ bodyExtra
136171 return $ "<!DOCTYPE html>" <> html
lib/frontend/src/Obelisk/Frontend/Cookie.hsnew file+102
@@ -0,0 +1,102 @@
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 RankNTypes #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+
13+module Obelisk.Frontend.Cookie where
14+
15+import Control.Monad.Fix
16+import Control.Monad.Primitive
17+import Control.Monad.Ref
18+import Control.Monad.IO.Class
19+import Control.Monad.Trans.Class
20+import Control.Monad.Trans.Reader
21+import Data.Text.Encoding (encodeUtf8)
22+import Reflex
23+import Reflex.Host.Class
24+import Reflex.Dom.Core
25+import GHCJS.DOM.Document (getCookie, Document)
26+import GHCJS.DOM.Types (MonadJSM)
27+import Web.Cookie
28+
29+import Obelisk.ExecutableConfig.Frontend
30+import Obelisk.Route.Frontend
31+
32+class Monad m => HasCookies m where
33+ askCookies :: m Cookies
34+ default askCookies :: (HasCookies m', m ~ t m', MonadTrans t) => m Cookies
35+ askCookies = lift askCookies
36+
37+instance HasCookies m => HasCookies (BehaviorWriterT t w m)
38+instance HasCookies m => HasCookies (DynamicWriterT t w m)
39+instance HasCookies m => HasCookies (EventWriterT t w m)
40+instance HasCookies m => HasCookies (PostBuildT t m)
41+instance HasCookies m => HasCookies (QueryT t q m)
42+instance HasCookies m => HasCookies (ReaderT r m)
43+instance HasCookies m => HasCookies (RequesterT t request response m)
44+instance HasCookies m => HasCookies (RouteToUrlT t m)
45+instance HasCookies m => HasCookies (SetRouteT t r m)
46+instance HasCookies m => HasCookies (StaticDomBuilderT t m)
47+instance HasCookies m => HasCookies (TriggerEventT t m)
48+instance HasCookies m => HasCookies (RoutedT t r m)
49+instance HasCookies m => HasCookies (FrontendConfigsT m)
50+instance HasFrontendConfigs m => HasFrontendConfigs (CookiesT m)
51+
52+newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
53+ deriving
54+ ( Functor
55+ , Applicative
56+ , DomBuilder t
57+ , Monad
58+ , MonadFix
59+ , MonadHold t
60+ , MonadIO
61+#ifndef ghcjs_HOST_OS
62+ , MonadJSM
63+#endif
64+ , MonadRef
65+ , MonadReflexCreateTrigger t
66+ , MonadSample t
67+ , MonadTrans
68+ , NotReady t
69+ , PerformEvent t
70+ , PostBuild t
71+ , Prerender js t
72+ , TriggerEvent t
73+ , HasDocument
74+ )
75+
76+instance Adjustable t m => Adjustable t (CookiesT m) where
77+ runWithReplace a e = CookiesT $ runWithReplace (unCookiesT a) (unCookiesT <$> e)
78+ traverseDMapWithKeyWithAdjust f m e = CookiesT $ traverseDMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
79+ traverseIntMapWithKeyWithAdjust f m e = CookiesT $ traverseIntMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
80+ traverseDMapWithKeyWithAdjustWithMove f m e = CookiesT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unCookiesT $ f k v) m e
81+
82+instance PrimMonad m => PrimMonad (CookiesT m) where
83+ type PrimState (CookiesT m) = PrimState m
84+ primitive = lift . primitive
85+
86+runCookiesT
87+ :: Cookies
88+ -> CookiesT m a
89+ -> m a
90+runCookiesT cs child = runReaderT (unCookiesT child) cs
91+
92+instance Monad m => HasCookies (CookiesT m) where
93+ askCookies = CookiesT ask
94+
95+mapCookiesT
96+ :: (forall x. m x -> n x)
97+ -> CookiesT m a
98+ -> CookiesT n a
99+mapCookiesT f (CookiesT x) = CookiesT $ mapReaderT f x
100+
101+instance (MonadJSM m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => HasCookies (HydrationDomBuilderT s t m) where
102+ askCookies = fmap (parseCookies . encodeUtf8) $ getCookie =<< askDocument
lib/route/obelisk-route.cabal+1
@@ -22,6 +22,7 @@ library
2222 monad-control,
2323 mtl,
2424 network-uri,
25+ obelisk-executable-config-frontend,
2526 primitive,
2627 ref-tf,
2728 reflex,
lib/route/src/Obelisk/Route.hs+1−1
@@ -155,7 +155,7 @@ import Data.Tabulation
155155
156156type R f = DSum f Identity --TODO: Better name
157157
158−--TODO: COMPLETE pragma
158+{-# COMPLETE (:/) #-}
159159infixr 5 :/
160160pattern (:/) :: f a -> a -> R f
161161pattern a :/ b = a :=> Identity b
lib/route/src/Obelisk/Route/Frontend.hs+57−17
@@ -3,6 +3,7 @@
33{-# LANGUAGE RecursiveDo #-}
44{-# LANGUAGE StandaloneDeriving #-}
55{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE DefaultSignatures #-}
67{-# LANGUAGE FlexibleContexts #-}
78{-# LANGUAGE FlexibleInstances #-}
89{-# LANGUAGE GADTs #-}
@@ -37,6 +38,9 @@ module Obelisk.Route.Frontend
3738 , maybeRoute
3839 , maybeRoute_
3940 , maybeRouted
41+ , eitherRoute
42+ , eitherRoute_
43+ , eitherRouted
4044 , runRouteViewT
4145 , SetRouteT(..)
4246 , SetRoute(..)
@@ -52,17 +56,18 @@ module Obelisk.Route.Frontend
5256
5357import Prelude hiding ((.), id)
5458
59+import Obelisk.ExecutableConfig.Frontend
5560import Obelisk.Route
5661
5762import Control.Category (Category (..), (.))
5863import Control.Category.Cartesian
5964import Control.Lens hiding (Bifunctor, bimap, universe, element)
65+import Control.Monad ((<=<))
6066import Control.Monad.Fix
6167import Control.Monad.Primitive
68+import Control.Monad.Reader
6269import Control.Monad.Ref
63−import Control.Monad.Trans
6470import Control.Monad.Trans.Control
65−import Control.Monad.Trans.Reader
6671import Data.Coerce
6772import Data.Dependent.Sum (DSum (..))
6873import Data.GADT.Compare
@@ -98,13 +103,21 @@ pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
98103
99104class Routed t r m | m -> t r where
100105 askRoute :: m (Dynamic t r)
106+ default askRoute :: (Monad m', MonadTrans f, Routed t r m', m ~ f m') => m (Dynamic t r)
107+ askRoute = lift askRoute
101108
102109instance Monad m => Routed t r (RoutedT t r m) where
103110 askRoute = RoutedT ask
104111
112+instance (Monad m, Routed t r m) => Routed t r (ReaderT r' m)
113+
105114newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
106115 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
107116
117+instance MonadReader r' m => MonadReader r' (RoutedT t r m) where
118+ ask = lift ask
119+ local = mapRoutedT . local
120+
108121instance HasJSContext m => HasJSContext (RoutedT t r m) where
109122 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
110123 askJSContext = lift askJSContext
@@ -165,17 +178,20 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
165178 askQueryResult = lift askQueryResult
166179 queryIncremental = lift . queryIncremental
167180
168−instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m) where
169− askRouteToUrl = lift askRouteToUrl
181+instance HasFrontendConfigs m => HasFrontendConfigs (RoutedT t r m)
170182
171−instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m) where
172− setRoute = lift . setRoute
173− modifyRoute = lift . modifyRoute
183+instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
184+
185+instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
186+
187+instance (Monad m, RouteToUrl r m) => RouteToUrl r (EventWriterT t w m)
188+
189+instance (Monad m, SetRoute t r m) => SetRoute t r (EventWriterT t w m)
174190
175191runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
176192runRoutedT = runReaderT . unRoutedT
177193
178−mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
194+mapRoutedT :: (m a -> n b) -> RoutedT t r m a -> RoutedT t r n b
179195mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
180196
181197withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
@@ -205,6 +221,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
205221 runRoutedT (f c) r'
206222-}
207223
224+eitherRoute_
225+ :: (MonadFix m, MonadHold t m, Adjustable t m)
226+ => RoutedT t l m ()
227+ -> RoutedT t r m ()
228+ -> RoutedT t (Either l r) m ()
229+eitherRoute_ l r = eitherRouted $ strictDynWidget_ $ either (runRoutedT l) (runRoutedT r)
230+
231+eitherRoute
232+ :: (MonadFix m, MonadHold t m, Adjustable t m)
233+ => RoutedT t l m a
234+ -> RoutedT t r m a
235+ -> RoutedT t (Either l r) m (Dynamic t a)
236+eitherRoute l r = eitherRouted $ strictDynWidget $ either (runRoutedT l) (runRoutedT r)
237+
208238dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
209239dsumValueCoercion Coercion = Coercion
210240
@@ -221,6 +251,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
221251 d' <- maybeDyn d
222252 runRoutedT r d'
223253
254+eitherRouted :: (Reflex t, MonadFix m, MonadHold t m) => RoutedT t (Either (Dynamic t a) (Dynamic t b)) m c -> RoutedT t (Either a b) m c
255+eitherRouted r = RoutedT $ ReaderT $ runRoutedT r <=< eitherDyn
256+
224257-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
225258strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
226259strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -257,13 +290,17 @@ runSetRouteT = runEventWriterT . unSetRouteT
257290class Reflex t => SetRoute t r m | m -> t r where
258291 setRoute :: Event t r -> m ()
259292 modifyRoute :: Event t (r -> r) -> m ()
293+ default modifyRoute :: (Monad m', MonadTrans f, SetRoute t r m', m ~ f m') => Event t (r -> r) -> m ()
294+ modifyRoute = lift . modifyRoute
295+
260296 setRoute = modifyRoute . fmap const
261297
262298instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
263299 modifyRoute = SetRouteT . tellEvent . fmap Endo
264300
265−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
266− modifyRoute = lift . modifyRoute
301+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
302+
303+instance (Monad m, SetRoute t r m) => SetRoute t r (ReaderT r' m)
267304
268305instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
269306 type Client (SetRouteT t r m) = SetRouteT t r (Client m)
@@ -280,9 +317,7 @@ instance Requester t m => Requester t (SetRouteT t r m) where
280317 requesting = SetRouteT . requesting
281318 requesting_ = SetRouteT . requesting_
282319
283−instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m) where
284− setRoute = lift . setRoute
285− modifyRoute = lift . modifyRoute
320+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
286321
287322#ifndef ghcjs_HOST_OS
288323deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
@@ -307,6 +342,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
307342 type PrimState (SetRouteT t r m) = PrimState m
308343 primitive = lift . primitive
309344
345+instance HasFrontendConfigs m => HasFrontendConfigs (SetRouteT t r m)
346+
310347instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
311348 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
312349 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -320,6 +357,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
320357
321358class RouteToUrl r m | m -> r where
322359 askRouteToUrl :: m (r -> Text)
360+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
361+ askRouteToUrl = lift askRouteToUrl
323362
324363newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
325364 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
@@ -337,13 +376,12 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
337376 askRouteToUrl = RouteToUrlT ask
338377
339378instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
340− askRouteToUrl = lift askRouteToUrl
341379
342380instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
343− askRouteToUrl = lift askRouteToUrl
344381
345−instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m) where
346− askRouteToUrl = lift askRouteToUrl
382+instance (Monad m, RouteToUrl r m) => RouteToUrl r (ReaderT r' m) where
383+
384+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m)
347385
348386instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
349387 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
@@ -403,6 +441,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
403441 askQueryResult = lift askQueryResult
404442 queryIncremental = lift . queryIncremental
405443
444+instance HasFrontendConfigs m => HasFrontendConfigs (RouteToUrlT t m)
445+
406446runRouteViewT
407447 :: forall t m r a.
408448 ( TriggerEvent t m
lib/run/obelisk-run.cabal+3−1
@@ -10,6 +10,8 @@ library
1010 attoparsec
1111 , base
1212 , bytestring
13+ , containers
14+ , cookie
1315 , dependent-sum
1416 , dependent-sum-template
1517 , ghcjs-dom
@@ -24,7 +26,7 @@ library
2426 , network
2527 , obelisk-asset-serve-snap
2628 , obelisk-backend
27− , obelisk-executable-config
29+ , obelisk-executable-config-backend
2830 , obelisk-frontend
2931 , obelisk-route
3032 , process
lib/run/src/Obelisk/Run.hs+46−28
@@ -1,17 +1,12 @@
11{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyCase #-}
3−{-# LANGUAGE EmptyDataDecls #-}
42{-# LANGUAGE FlexibleContexts #-}
53{-# LANGUAGE FlexibleInstances #-}
64{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE KindSignatures #-}
85{-# LANGUAGE LambdaCase #-}
96{-# LANGUAGE MultiParamTypeClasses #-}
107{-# LANGUAGE OverloadedStrings #-}
118{-# LANGUAGE Rank2Types #-}
12−{-# LANGUAGE RecursiveDo #-}
139{-# LANGUAGE ScopedTypeVariables #-}
14−{-# LANGUAGE TemplateHaskell #-}
1510{-# LANGUAGE TypeFamilies #-}
1611{-# LANGUAGE UndecidableInstances #-}
1712{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
@@ -31,9 +26,12 @@ import Data.Dependent.Sum (DSum (..))
3126import Data.Functor.Identity
3227import Data.Functor.Sum
3328import Data.List (uncons)
29+import Data.Map (Map)
30+import qualified Data.Map as Map
3431import Data.Maybe
3532import Data.Semigroup ((<>))
3633import Data.Streaming.Network (bindPortTCP)
34+import Data.String (fromString)
3735import Data.Text (Text)
3836import qualified Data.Text as T
3937import Language.Javascript.JSaddle.Run (syncPoint)
@@ -49,7 +47,8 @@ import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4947import Network.WebSockets (ConnectionOptions)
5048import Network.WebSockets.Connection (defaultConnectionOptions)
5149import qualified Obelisk.Asset.Serve.Snap as Snap
52−import Obelisk.ExecutableConfig (get)
50+import Obelisk.Backend
51+import Obelisk.ExecutableConfig.Backend
5352import Obelisk.Frontend
5453import Obelisk.Route.Frontend
5554import Reflex.Dom.Core
@@ -61,7 +60,7 @@ import System.Exit (ExitCode(..))
6160import Text.URI (URI)
6261import qualified Text.URI as URI
6362import Text.URI.Lens
64−import Obelisk.Backend
63+import Web.Cookie
6564
6665run
6766 :: Int -- ^ Port to run the backend
@@ -76,23 +75,27 @@ run port serveStaticAsset backend frontend = do
7675 case checkEncoder $ _backend_routeEncoder backend of
7776 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7877 Right validFullEncoder -> do
78+ configs <- getComponentConfigs
7979 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
80− _backend_run backend $ \serveRoute -> do
81− runSnapWithCommandLineArgs $ do
82− getRouteWith validFullEncoder >>= \case
83− Identity r -> case r of
84− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a ->
86− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
80+ runBackendConfigsT (_componentConfigs_backend configs) $
81+ _backend_run backend $ \serveRoute -> do
82+ runSnapWithCommandLineArgs $ do
83+ getRouteWith validFullEncoder >>= \case
84+ Identity r -> case r of
85+ InL backendRoute :=> Identity a ->
86+ runBackendConfigsT (_componentConfigs_backend configs) $
87+ serveRoute $ backendRoute :/ a
88+ InR obeliskRoute :=> Identity a ->
89+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
8790 let conf = defRunConfig { _runConfig_redirectPort = port }
88− runWidget conf frontend validFullEncoder `finally` killThread backendTid
91+ runWidget conf (_componentConfigs_frontend configs) frontend validFullEncoder `finally` killThread backendTid
8992
9093-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
9194runServeAsset :: FilePath -> [Text] -> Snap ()
9295runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
9396
94−getConfigRoute :: IO (Maybe URI)
95−getConfigRoute = get "config/common/route" >>= \case
97+getConfigRoute :: Map Text Text -> IO (Maybe URI)
98+getConfigRoute configs = case Map.lookup "common/route" configs of
9699 Just r -> case URI.mkURI $ T.strip r of
97100 Just route -> pure $ Just route
98101 Nothing -> do
@@ -103,9 +106,14 @@ getConfigRoute = get "config/common/route" >>= \case
103106defAppUri :: URI
104107defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
105108
106−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
107−runWidget conf frontend validFullEncoder = do
108− uri <- fromMaybe defAppUri <$> getConfigRoute
109+runWidget
110+ :: RunConfig
111+ -> Map Text Text
112+ -> Frontend (R route)
113+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
114+ -> IO ()
115+runWidget conf configs frontend validFullEncoder = do
116+ uri <- fromMaybe defAppUri <$> getConfigRoute configs
109117 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
110118 redirectHost = _runConfig_redirectHost conf
111119 redirectPort = _runConfig_redirectPort conf
@@ -117,19 +125,22 @@ runWidget conf frontend validFullEncoder = do
117125 close
118126 (\skt -> do
119127 man <- newManager defaultManagerSettings
120− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
128+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
121129 runSettingsSocket settings skt app)
122130
123131obeliskApp
124− :: forall route backendRoute. ConnectionOptions
132+ :: forall route backendRoute
133+ . Map Text Text
134+ -> ConnectionOptions
125135 -> Frontend (R route)
126136 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
127137 -> URI
128138 -> Application
129139 -> IO Application
130−obeliskApp opts frontend validFullEncoder uri backend = do
140+obeliskApp configs opts frontend validFullEncoder uri backend = do
131141 let entryPoint = do
132− runFrontend validFullEncoder frontend
142+ removeHTMLConfigs
143+ runFrontendWithConfigs configs validFullEncoder frontend
133144 syncPoint
134145 jsaddlePath <- URI.mkPathPiece "jsaddle"
135146 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
@@ -143,15 +154,22 @@ obeliskApp opts frontend validFullEncoder uri backend = do
143154 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
144155 }
145156 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
146− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
157+ let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
158+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
147159 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
148160 _ -> backend req sendResponse
149161
150−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
151−renderJsaddleFrontend urlEnc r f =
162+renderJsaddleFrontend
163+ :: Map Text Text
164+ -> Cookies
165+ -> (route -> Text)
166+ -> route
167+ -> Frontend route
168+ -> IO ByteString
169+renderJsaddleFrontend configs cookies urlEnc r f =
152170 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
153171 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
154− in renderFrontendHtml urlEnc r (_frontend_head f >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
172+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
155173
156174-- | like 'bindPortTCP' but reconnects on exception
157175bindPortTCPRetry :: Settings
lib/selftest/obelisk-selftest.cabal+1
@@ -16,6 +16,7 @@ library
1616 , http-types
1717 , HUnit
1818 , obelisk-cliapp
19+ , obelisk-executable-config-lookup
1920 , obelisk-run
2021 , process
2122 , network
lib/selftest/src/Obelisk/SelfTest.hs+4−2
@@ -34,6 +34,7 @@ import Test.HUnit.Base
3434
3535import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
3636import qualified Obelisk.CliApp as CliApp
37+import Obelisk.ExecutableConfig.Lookup (getConfigs)
3738import Obelisk.Run (getConfigRoute)
3839
3940data ObRunState
@@ -126,8 +127,9 @@ main = do
126127 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127128 ls tmp >>= liftIO . assertEqual "" []
128129
129− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
130− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
130+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
131+ configs <- getConfigs
132+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute configs `shouldNotReturn` Nothing
131133
132134 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
133135 -- when not run by 'nix-build --attr selftest'
release.nix+14−6
@@ -1,4 +1,5 @@
1−{ local-self ? import ./. {}
1+{ self-args ? {}
2+, local-self ? import ./. self-args
23}:
34
45let
@@ -9,7 +10,7 @@ let
910 obeliskPackagesCommon = [
1011 "obelisk-frontend"
1112 "obelisk-route"
12− "obelisk-executable-config"
13+ "obelisk-executable-config-lookup"
1314 ];
1415
1516 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -59,17 +60,24 @@ let
5960 (concatDepends ghcjs)
6061 ];
6162 command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63− builtSkeleton = (import ./skeleton {}).all;
63+ serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
64+ androidSkeleton = (import ./skeleton { obelisk = local-self; }).android.frontend;
65+ iosObelisk = import ./. (self-args // {
66+ system = "x86_64-darwin";
67+ iosSdkVersion = "10.2";
68+ });
69+ iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
6470 in {
6571 inherit
6672 command
6773 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
74+ serverExeSkeleton
75+ iosSkeleton
76+ androidSkeleton;
6977 cache = reflex-platform.pinBuildInputs
7078 "obelisk-${system}"
7179 cachePackages
72− [command serverExeSkeleton builtSkeleton];
80+ [command serverExeSkeleton iosSkeleton androidSkeleton];
7381 });
7482
7583 metaCache = local-self.pinBuildInputs
skeleton/default.nix+9−3
@@ -1,7 +1,13 @@
1−{ system ? builtins.currentSystem # TODO: Get rid of this system cruft
2−, iosSdkVersion ? "10.2"
1+{ obelisk ? import ./.obelisk/impl {
2+ system = builtins.currentSystem;
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+ }
39}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
10+with obelisk;
511project ./. ({ ... }: {
612 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
713 android.displayName = "Obelisk Minimal Example";