on master help sign in
Merge remote-tracking branch 'origin/develop' into avoid-pointless-thunk
commit5634f4ae947f3a19c57e876bda24fd78ffa28237 authorJohn Ericson <John.Ericson@Obsidian.Systems> dateTue 8 Oct 2019, 14:46 (7 years ago) parentc065831c1010c7
72 files changed+2056−669
.gitignore+1−3 CONTRIBUTING.md+73−3 ChangeLog.md+38 FAQ.md+107 README.md+80−8 default.nix+37−34 dep/reflex-platform/github.json+2−2 jobsets.nix+5 lib/asset/manifest/obelisk-asset-manifest.cabal+1 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+7−6 lib/backend/obelisk-backend.cabal+3 lib/backend/src/Obelisk/Backend.hs+82−43 lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp/Demo.hs+2−1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+26−2 lib/cliapp/src/Obelisk/CliApp/Process.hs+9−7 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−23 lib/cliapp/src/Obelisk/CliApp/Theme.hs+29 lib/cliapp/src/Obelisk/CliApp/Types.hs+10−3 lib/command/obelisk-command.cabal+1−1 lib/command/src/Obelisk/App.hs+5−2 lib/command/src/Obelisk/Command.hs+53−8 lib/command/src/Obelisk/Command/Deploy.hs+35−30 lib/command/src/Obelisk/Command/Project.hs+7−5 lib/command/src/Obelisk/Command/Run.hs+50−10 lib/command/src/Obelisk/Command/Thunk.hs+9−14 lib/command/src/Obelisk/Command/Utils.hs+1−1 lib/command/src/Obelisk/Command/VmBuilder.hs+2−1 lib/executable-config/default.nix+4−1 lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40 lib/executable-config/lookup/default.nix−24 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+45 lib/executable-config/lookup/obelisk-executable-config.cabal−30 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs−27 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+60 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hs−17 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+51 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs−22 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+17 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+18 lib/executable-config/lookup/src/Obelisk/Configs.hs+128 lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hs+24 lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs−13 lib/frontend/obelisk-frontend.cabal+8−1 lib/frontend/src/Obelisk/Frontend.hs+178−92 lib/frontend/src/Obelisk/Frontend/Cookie.hs+102 lib/route/obelisk-route.cabal+7−2 lib/route/src/Obelisk/Route.hs+340−39 lib/route/src/Obelisk/Route/Frontend.hs+127−43 lib/route/src/Obelisk/Route/TH.hs+4−5 lib/run/obelisk-run.cabal+3−2 lib/run/src/Obelisk/Run.hs+56−43 lib/selftest/obelisk-selftest.cabal+1 lib/selftest/src/Obelisk/SelfTest.hs+15−15 lib/tabulation/CHANGELOG.md+5 lib/tabulation/LICENSE+30 lib/tabulation/Setup.hs+2 lib/tabulation/src/Data/Tabulation.hs+21 lib/tabulation/tabulation.cabal+26 release.nix+31−14 skeleton/backend/src/Backend.hs+1−3 skeleton/common/common.cabal+3−1 skeleton/common/src/Common/Api.hs+1−1 skeleton/common/src/Common/Route.hs+12−18 skeleton/config/common/example+1 skeleton/default.nix+9−3 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src-bin/main.hs+1−1 skeleton/frontend/src/Frontend.hs+8−1 skeleton/shell.nix+1
.gitignore+1−3
@@ -4,12 +4,10 @@
44result
55*.o
66*.hi
7−
87TAGS
98tags
109ctags
1110dist-newstyle
1211cabal.project.local
1312.ghc.environment.*
14−
15−**/ghcid-output.txt
13+**/ghcid-output.txt
CONTRIBUTING.md+73−3
@@ -1,9 +1,79 @@
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+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
9+ - [Code Quality](#code-quality)
10+ - [Documentation](#documentation)
611
12+## Opening Issues
13+
14+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
15+
16+* Describe the issue you're encountering or the suggestion you're making
17+* 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.
18+* Let us know what version of this project you were using. If you're using a github checkout, provide the git hash.
19+
20+## Submitting Changes
21+
22+Most pull requests should target the `develop` branch. `master` is the release branch. `develop` is periodically merged into master after a period of testing.
23+
24+### Guidelines for Commit Messages
25+
26+#### Summary Line
27+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.
28+
29+One way to think about it is that your commit message should be able to complete the sentence:
30+"When applied, this commit will..."
31+
32+##### Note on bumping dependencies
33+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
34+
35+#### Body
36+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.
37+
38+### Guidelines for Pull Requests
39+
40+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
41+
42+### Code Quality
43+
44+#### Warnings
45+
46+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
47+
48+#### Build and Test
49+
50+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!
51+
52+You can run the tests like this:
753```bash
854$(nix-build -A selftest --no-out-link)
955```
56+
57+To test that your changes build across platforms, you can also try to build release.nix, like this:
58+```bash
59+nix-build release.nix
60+```
61+
62+Note, however, that to build release.nix you must accept the android license agreement and your machine must be configured to build both ios and android executables (usually via remote builders).
63+
64+
65+### Documentation
66+
67+#### In the code
68+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.
69+
70+#### In the [Changelog](ChangeLog.md)
71+Add an entry to the changelog when your PR:
72+* Adds a feature
73+* Deprecates something
74+* Includes a breaking change
75+* Makes any other change that will impact users
76+
77+#### In the [Readme](README.md)
78+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.
79+
ChangeLog.mdnew file+38
@@ -0,0 +1,38 @@
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+* Generalised pathSegmentEncoder, added pathFieldEncoder.
8+* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
9+* Add "ob doc" command, which lists paths to haddock documentation for specified packages
10+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
11+
12+## v0.2.0.0 - 2019-8-17
13+
14+* Configs become ByteStrings.
15+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
16+ * The frontend will still only have access to configs that are placed in config/frontend and config/common, while the backend has access to the entire contents of the config directory via `Obelisk.ExecutableConfig.Lookup.getConfigs`.
17+* The backend no longer runs in BackendConfigsT.
18+* Add tabulation package. See Data.Tabulation for details.
19+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
20+* Use IP address for nginx proxy pass instead of localhost
21+
22+## v0.1.1.0 - 2019-05-17
23+
24+* Fix crashes of Android apps on 32-bit ARM devices.
25+* 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`.
26+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
27+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
28+* Add the `ob shell` command to enter a nix shell for an obelisk project
29+* 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.
30+* 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`.
31+* 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`.
32+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
33+
34+
35+## v0.1.0.0 - 2019-03-29
36+
37+* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
38+* Add `HasCookies` and `CookiesT` to allow `ObeliskWidget`s to access cookies during static and "hydrated" DOM rendering.
FAQ.md+107
@@ -1,6 +1,9 @@
11# Frequently Asked Questions
22
331. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
4+1. [`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)
47
58### How do I fix invalid entitlements?
69
@@ -13,3 +16,107 @@ When this happens you'll see an error something like this:
1316
1417Fixing the value of `ios.bundleIdentifier` should fix the error.
1518
19+### `ob thunk update` or `ob deploy update` fails
20+Whenever an `ob` command fails, try re-running it with `-v`.
21+
22+If you're using a private repo, and you get a failure in nix-prefetch-url, you may need to unpack and repack the thunk. Here's some example output that shows this issue:
23+
24+```
25+Starting Obelisk </nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/.ob-wrapped> args=["deploy","update","-v"] logging-level=Debug
26+Creating process: 'nix-build' './.obelisk/impl' '-A' 'command' '--no-out-link'
27+✔ Built on ./.obelisk/impl [command]
28+Handing off to /nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/ob
29+Starting Obelisk </nix/store/j8wls8a89xr6s1a47lg6g83gnbdrfd0l-obelisk-command-0.1/bin/.ob-wrapped> args=["--no-handoff","deploy","update","-v"] logging-level=Debug
30+Creating process: 'git' 'ls-remote' '--exit-code' '--symref' 'https://github.com/obsidiansystems/some-private-repo.git' 'refs/heads/master'
31+git ls-remote maps: (fromList [],fromList [(GitRef_Branch "master","8546dfc8be9653f20bb26214e0991dbb957cb290")])
32+Latest commit in branch master from remote repo https://github.com/obsidiansystems/some-private-repo.git is 8546dfc8be9653f20bb26214e0991dbb957cb290
33+Creating process: 'nix-prefetch-url' '--unpack' '--type' 'sha256' 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz'
34+error: unable to download 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz': HTTP error 404
35+Process exited with code 1; 'nix-prefetch-url' '--unpack' '--type' 'sha256' 'https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz'
36+nix-prefetch-url: Failed to determine sha256 hash of URL https://github.com/obsidiansystems/some-private-repo/archive/8546dfc8be9653f20bb26214e0991dbb957cb290.tar.gz
37+✖ Updating thunk ./src to latest
38+```
39+
40+And here's how you can fix it:
41+
42+```
43+ob thunk unpack $SOME_THUNK
44+ob thunk pack $SOME_THUNK
45+```
46+Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
47+
48+(Based on issue #351).
49+
50+### How do I fix `Ambiguous module name` errors?
51+Since obelisk places the common/backend/frontend modules packages into the same ghci, and ghci doesn't "sandbox" them, it is possible to have conflicting module errors inside `ob repl/watch/run` that do not appear when doing a cabal build.
52+
53+You can disambiguate this via [PackageImports](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#package-qualified-imports). For instance, if you see
54+```
55+error:
56+ Ambiguous module name ‘Crypto.Hash’:
57+ it was found in multiple packages:
58+ cryptohash-0.11.9 cryptonite-0.25
59+```
60+then specify the package you want in the import, e.g:
61+`import "cryptonite" Crypto.Hash`
62+
63+
64+### What does `ob run` actually do?
65+
66+
67+#### Short version:
68+
69+`ob run` starts a [`ghcid`](https://github.com/ndmitchell/ghcid) process which
70+tries to build your project within a carefully crafted `nix-shell` with all the
71+project's dependencies and,
72+
73+* either displays compilation errors/warnings,
74+* or starts the Obelisk server, which serves:
75+ * your backend's route handlers,
76+ * the static assets,
77+ * the [JSaddle](https://github.com/ghcjs/jsaddle) frontend code (while opening its websocket).
78+
79+#### Longer version:
80+
81+Assuming we are in a project created with `ob init`, `ob run` calls (see
82+`lib/command/src/Obelisk/Command.hs`):
83+
84+ nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
85+
86+where
87+
88+* `shells.ghc` is defined in `./default.nix` by importing `./.obelisk/impl/default.nix` which
89+ is `default.nix` in the present (Obelisk) repository,
90+* `run-static-io` is a logging-enabled command wrapper
91+ (cf. `runObelisk` in `lib/command/src/Obelisk/App.hs`).
92+
93+In this case, it runs the function `Obelisk.Command.Command.run` (defined in
94+`lib/command/src/Obelisk/Command/Run.hs`).
95+
96+* It creates a GHCi config from a Nix expression which:
97+ * loads three packages: `backend`, `common`, `frontend`,
98+ * obtains a free port number.
99+* Then runs `ghcid`
100+ * with a command that reruns `Obelisk.Run.run` at each restart (option
101+ `--test`).
102+
103+It is defined in `lib/run/src/Obelisk/Run.hs`:
104+
105+* It creates a thread which starts the main backend.
106+ This runs `runSnapWithCommandLineArgs` and passes routes:
107+ * to the backend (result of the `_backend_run` field of your
108+ implementation of the `Backend fullRoute frontendRoute` record),
109+ * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110+ serve static assets.
111+* Starts `runWidget`
112+ which itself runs
113+ [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114+ (the *“TCP listen loop”*):
115+ * it binds to TCP socket, and
116+ * creates the HTTP connection manager
117+ ( [`newManager`](https://hackage.haskell.org/package/http-client-0.6.3/docs/Network-HTTP-Client.html#v:newManager))
118+ * calls `obeliskApp` which
119+ * has a route to serve the [JSaddle](https://github.com/ghcjs/jsaddle) frontend,
120+ * runs the frontend (*on* the server), to produce pre-rendered pages,
121+ * starts the JSaddle web-socket,
122+ * “falls back” to proxying the backend (`fallbackProxy`).
README.md+80−8
@@ -4,6 +4,9 @@ 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 Packages](#adding-packages)
9+ - [Adding Package Overrides](#adding-package-overrides)
710- [Deploying](#deploying)
811 - [Locally](#locally)
912 - [EC2](#ec2)
@@ -22,6 +25,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
2225 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
2326 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2427 ```
28+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2529 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)):
2630 ```
2731 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
@@ -56,6 +60,28 @@ Or to launch ghcid for `lib/command` project:
5660nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
5761```
5862
63+To re-install `ob` from source do
64+```
65+nix-env -f /path/to/obelisk -iA command
66+```
67+
68+Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
69+
70+```shell
71+ob thunk unpack ./obelisk/impl
72+cd ./obelisk/impl
73+# apply your changes
74+```
75+
76+If you want to commit your changes, first push them to your fork of obelisk and then
77+
78+```shell
79+cd /your/project/root
80+ob thunk pack .obelisk/impl
81+git add .obelisk/impl
82+git commit -m "Bump obelisk"
83+```
84+
5985### Accessing private repositories
6086
6187To allow the Nix builder to access private git repositories, you must be set up
@@ -79,10 +105,49 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
79105ob run
80106```
81107
82−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
108+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
83109
84110Every 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.
85111
112+### Hoogle
113+
114+To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
115+
116+`nix-shell -A shells.ghc --arg withHoogle true`
117+
118+### Adding packages
119+
120+In order to add package dependencies, declare them under the build-depends field in the appropriate cabal files (backend, common, and frontend each have their own). The corresponding Nix packages will automatically be selected when building.
121+
122+### Adding package overrides
123+
124+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 fetched from GitHub and a specific version of the `waargonaut` package fetched from Hackage, your `default.nix` will look like:
125+
126+```nix
127+# ...
128+project ./. ({ pkgs, ... }: {
129+# ...
130+ overrides = self: super: let
131+ aesonSrc = pkgs.fetchFromGitHub {
132+ owner = "obsidiansystems";
133+ repo = "aeson-gadt-th";
134+ rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
135+ sha256 = "08q6rnz7w9pn76jkrafig6f50yd0f77z48rk2z5iyyl2jbhcbhx3";
136+ };
137+ in
138+ {
139+ aeson = self.callCabal2nix "aeson" aesonSrc {};
140+ waargonaut = self.callHackageDirect {
141+ pkg = "waargonaut";
142+ ver = "0.8.0.1";
143+ sha256 = "1zv28np3k3hg378vqm89v802xr0g8cwk7gy3mr77xrzy5jbgpa39";
144+ } {};
145+ };
146+# ...
147+```
148+
149+For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
150+
86151## Deploying
87152
88153### Locally
@@ -90,14 +155,16 @@ Every time you change the Haskell source files in frontend, common or backend, `
90155Build everything:
91156
92157```
93−nix-build -A exe -o result-exe
158+nix-build -A exe --no-out-link
94159```
95160
96−Run the server:
161+Copy the result to a new directory, add configuration, and run!
97162
98163```
99−cd result-exe
100−./backend
164+mkdir test-app
165+ln -s $(nix-build -A exe --no-out-link)/* test-app/
166+cp -r config test-app
167+(cd test-app && ./backend)
101168```
102169
103170### EC2
@@ -106,7 +173,7 @@ In this section we will demonstrate how to deploy your Obelisk app to an Amazon
106173
107174First create a new EC2 instance:
108175
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))
176+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))
1101771. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1111781. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1121791. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
@@ -129,9 +196,11 @@ ob deploy init \
129196 ~/code/myapp-deploy
130197```
131198
132−NOTE: HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
199+HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
200+
201+This step will also require that you manually verify the authenticity of the host `$SERVER`. Obelisk will save the fingerprint in a deployment-specific configuration. **Obelisk deployments do *not* rely on the `known_hosts` of your local machine.** This is because, in the event that you need to switch from one deploy machine / bastion host to another, you want to be absolutely sure that you're still connecting to the machines you think you are, even if that deploy machine / bastion host has never connected to them before. Obelisk explicitly avoids a workflow that encourages people to accept host keys without checking them, since that could result in leaking production secrets to anyone who manages to MITM you, e.g. via DNS spoofing or cache poisoning. (Note that an active attack is a circumstance where you may need to quickly switch bastion hosts, e.g. because the attacker has taken one down or you have taken it down in case it was compromised. In this circumstance you might need to deploy to production to fix an exploit or rotate keys, etc.) When you run `ob deploy` later it will rely on the saved verification in this step.
133202
134−Then go to that created deployment configuration directory, and initiate the deployment:
203+Next, go to the deployment directory that you just initialized and deploy!
135204
136205```
137206cd ~/code/myapp-deploy
@@ -237,7 +306,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237306
238307### Android
239308
309+NOTE: Currently Android builds are only supported on Linux.
310+
2403111. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
312+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.
2413131. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2423141. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243315
default.nix+37−34
@@ -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
@@ -33,28 +32,21 @@ let
3332 # Fix misc upstream packages
3433 (self: super: let
3534 pkgs = self.callPackage ({ pkgs }: pkgs) {};
35+ haskellLib = pkgs.haskell.lib;
3636 in {
37− hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
38− owner = "haskell-nix";
39− repo = "hnix";
40− rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
41− sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
42− }) {});
43− # Need 8.0.2 build support
44− # PR: https://github.com/dmwit/universe/pull/33
45− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
46− owner = "obsidiansystems";
47− repo = "universe";
48− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
49− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
50− } + /template) {};
37+ hnix = haskellLib.overrideCabal (self.callHackage "hnix" "0.6.1" { these = self.these_0_8; }) (_: {
38+ jailbreak = true;
39+ doCheck = false;
40+ });
41+ hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
5142 })
5243
44+ pkgs.obeliskExecutableConfig.haskellOverlay
45+
5346 # Add obelisk packages
5447 (self: super: let
5548 pkgs = self.callPackage ({ pkgs }: pkgs) {};
5649 in {
57− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
5850 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5951
6052 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
@@ -67,6 +59,7 @@ let
6759 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
6860 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
6961 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
62+ tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
7063 })
7164
7265 (self: super: let
@@ -112,7 +105,7 @@ let
112105 ];
113106 };
114107
115− reflex-platform = getReflexPlatform' false system;
108+ reflex-platform = getReflexPlatform system;
116109 inherit (reflex-platform) hackGet nixpkgs;
117110 pkgs = nixpkgs;
118111
@@ -135,7 +128,7 @@ in rec {
135128 obelisk = ghcObelisk;
136129 obeliskEnvs = ghcObeliskEnvs;
137130 command = ghcObelisk.obelisk-command;
138− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
131+ shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
139132
140133 selftest = pkgs.writeScript "selftest" ''
141134 #!/usr/bin/env bash
@@ -164,12 +157,16 @@ in rec {
164157 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
165158 '';
166159
167− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
160+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
168161 mkdir $out
169162 cd $out
170163 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
171− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
172− echo "//# sourceMappingURL=all.js.map" >> all.js
164+ ${if optimizationLevel == null then ''
165+ ln -s all.unminified.js all.js
166+ '' else ''
167+ ${pkgs.closurecompiler}/bin/closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
168+ echo "//# sourceMappingURL=all.js.map" >> all.js
169+ ''}
173170 '';
174171
175172 serverModules = {
@@ -205,7 +202,8 @@ in rec {
205202 enableACME = enableHttps;
206203 forceSSL = enableHttps;
207204 locations.${baseUrl} = {
208− proxyPass = "http://localhost:" + toString internalPort;
205+ proxyPass = "http://127.0.0.1:" + toString internalPort;
206+ proxyWebsockets = true;
209207 };
210208 };
211209 };
@@ -213,10 +211,11 @@ in rec {
213211 wantedBy = [ "multi-user.target" ];
214212 after = [ "network.target" ];
215213 restartIfChanged = true;
214+ path = [ pkgs.gnutar ];
216215 script = ''
217216 ln -sft . '${exe}'/*
218217 mkdir -p log
219− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
218+ exec ./backend ${backendArgs} </dev/null
220219 '';
221220 serviceConfig = {
222221 User = user;
@@ -274,7 +273,7 @@ in rec {
274273 , tools ? _: []
275274 , shellToolOverrides ? _: _: {}
276275 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
277− , __closureCompilerOptimizationLevel ? "ADVANCED"
276+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
278277 }:
279278 let frontendName = "frontend";
280279 backendName = "backend";
@@ -299,18 +298,22 @@ in rec {
299298 injectableConfig = builtins.filterSource (path: _:
300299 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
301300 );
302− __android = configPath: {
301+ __androidWithConfig = configPath: {
303302 ${if android == null then null else frontendName} = {
304303 executableName = "frontend";
305304 ${if builtins.pathExists staticFiles then "assets" else null} =
306− nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath) processedStatic.symlinked;
305+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
306+ (injectableConfig configPath)
307+ processedStatic.symlinked;
307308 } // android;
308309 };
309− __ios = configPath: {
310+ __iosWithConfig = configPath: {
310311 ${if ios == null then null else frontendName} = {
311312 executableName = "frontend";
312313 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
313− nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath) processedStatic.symlinked;
314+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
315+ (injectableConfig configPath)
316+ processedStatic.symlinked;
314317 } // ios;
315318 };
316319 in {
@@ -332,9 +335,9 @@ in rec {
332335 commonName
333336 ];
334337 };
335− android = __android null;
336− ios = __ios null;
337− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
338+ android = __androidWithConfig (base + "/config");
339+ ios = __iosWithConfig (base + "/config");
340+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
338341 };
339342 in mkProject (projectDefinition args));
340343 serverOn = sys: version: serverExe
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "a7cd9a23e7faa9c2545a4c111229e28208e42351",
6− "sha256": "0y52rfrhk4zszgprpyni51l0pgq18dg695k5bmpd62c3zxar5mvm"
5+ "rev": "9e306f72ed0dbcdccce30a4ba0eb37aa03cf91e3",
6+ "sha256": "1crlwfw6zsx2izwsd57i5651p5pfyrpkfjwp6fc1yd4q4d4n7g2m"
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/obelisk-asset-manifest.cabal+1
@@ -17,6 +17,7 @@ library
1717 base
1818 , bytestring
1919 , containers
20+ , deepseq
2021 , directory
2122 , SHA
2223 , filepath
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2
@@ -1,10 +1,12 @@
11{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
23-- | Functions for collecting up files to be processed by the asset pipeline
34module Obelisk.Asset.Gather
45 ( gatherHashedPaths
56 , toHashedPath
67 ) where
78
9+import Control.DeepSeq
810import Control.Monad (forM)
911import Data.Bits
1012import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +35,7 @@ gatherHashedPaths root = go ""
3335 let relativePath = subdir </> sub
3436 isFile <- doesFileExist $ root </> relativePath
3537 if isFile
36− then do hashedRelativePath <- toHashedPath root relativePath
38+ then do !hashedRelativePath <- force <$> toHashedPath root relativePath
3739 return $ Map.singleton relativePath hashedRelativePath
3840 else go relativePath
3941
@@ -47,7 +49,8 @@ toHashedPath root relativePath = do
4749 contents <- LBS.readFile path
4850 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
4951 (dir, filename) = splitFileName relativePath
50− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
5154
5255-- | Convert a ByteString to base 32 in the way that Nix does
5356toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+7−6
@@ -19,6 +19,7 @@ import Control.Applicative ((<|>))
1919import Control.Exception (handleJust, try, throwIO)
2020import Control.Monad (forM, liftM)
2121import Control.Monad.IO.Class (liftIO)
22+import Control.Monad.Fail (MonadFail)
2223import Data.Attoparsec.ByteString (parseOnly, endOfInput)
2324import Data.ByteString (ByteString)
2425import qualified Data.ByteString as BS
@@ -38,7 +39,7 @@ import System.Posix (getFileStatus, fileSize)
3839-- current version of the asset
3940-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
4041-- asset names will be sent with permanent caching headers.
41−serveAssets :: MonadSnap m => FilePath -> FilePath -> m ()
42+serveAssets :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
4243serveAssets = serveAssets' True
4344
4445-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -47,15 +48,15 @@ serveAssets = serveAssets' True
4748-- browser for the logical asset name (e.g. @test.png@) but instead the current
4849-- version of the asset with to point the browser at the unique filename for the
4950-- current version of the asset.
50−serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
51+serveAssetsInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
5152serveAssetsInPlace = serveAssets' False
5253
5354-- | Like 'serveAssets', but only serves a single specified asset
54−serveAsset :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
55+serveAsset :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5556serveAsset = serveAsset' True
5657
5758-- | Like 'serveAssetsInPlace', but only serves a single specified asset
58−serveAssetInPlace :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
59+serveAssetInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5960serveAssetInPlace = serveAsset' False
6061
6162-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -63,13 +64,13 @@ serveAssetInPlace = serveAsset' False
6364-- For assets generated from @assets.nix@, the @Bool@ argument @doRedirect@ controls whether redirects will be sent to the browser if a request is made for
6465-- an unhashed asset name, e.g. @test.png@. For @True@, a redirect will be sent, yielding more round trips to the server but better caching behavior if the
6566-- asset doesn't change often. Conversely for @False@, the asset will be served "in place" but made uncacheable.
66−serveAssets' :: MonadSnap m => Bool -> FilePath -> FilePath -> m ()
67+serveAssets' :: (MonadSnap m, MonadFail m) => Bool -> FilePath -> FilePath -> m ()
6768serveAssets' doRedirect base fallback = do
6869 pRaw <- getSafePath
6970 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
7071
7172-- | Serve a single static asset from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
72−serveAsset' :: MonadSnap m => Bool -> FilePath -> FilePath -> FilePath -> m ()
73+serveAsset' :: (MonadFail m, MonadSnap m) => Bool -> FilePath -> FilePath -> FilePath -> m ()
7374serveAsset' doRedirect base fallback p = do
7475 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
7576 case assetType of
lib/backend/obelisk-backend.cabal+3
@@ -8,6 +8,8 @@ library
88 build-depends: base,
99 bytestring,
1010 categories,
11+ containers,
12+ cookie,
1113 data-default,
1214 dependent-sum,
1315 dependent-sum-template,
@@ -15,6 +17,7 @@ library
1517 mtl,
1618 obelisk-asset-serve-snap,
1719 obelisk-executable-config-inject,
20+ obelisk-executable-config-lookup,
1821 obelisk-frontend,
1922 obelisk-route,
2023 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+82−43
@@ -6,7 +6,7 @@
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
@@ -19,36 +19,40 @@ module Obelisk.Backend
1919 , runBackend
2020 , staticRenderContentType
2121 , mkRouteToUrl
22+ , getPublicConfigs
2223 ) where
2324
2425import Prelude hiding (id, (.))
2526import Control.Category
2627import Control.Monad
2728import Control.Monad.Except
29+import Control.Monad.Fail (MonadFail)
2830import Control.Categorical.Bifunctor
2931import Data.ByteString (ByteString)
3032import qualified Data.ByteString.Char8 as BSC8
3133import Data.Default (Default (..))
3234import Data.Dependent.Sum
33−import Data.Functor.Sum
3435import Data.Functor.Identity
36+import Data.Map (Map)
37+import qualified Data.Map as Map
3538import Data.Monoid ((<>))
3639import Data.Text (Text)
3740import qualified Data.Text as T
3841import Data.Text.Encoding
3942import Obelisk.Asset.Serve.Snap (serveAsset)
40−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
43+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4144import Obelisk.Frontend
4245import Obelisk.Route
4346import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4447import Reflex.Dom
4548import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
46− , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
49+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
50+ , rqCookies, Cookie(..) , setHeader)
4751import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4852import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4953
5054data Backend backendRoute frontendRoute = Backend
51− { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
55+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (FullRoute backendRoute frontendRoute)) PageName
5256 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
5357 }
5458
@@ -60,8 +64,17 @@ data GhcjsApp route = GhcjsApp
6064
6165-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
6266--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
63−serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
64−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
67+serveDefaultObeliskApp
68+ :: (MonadSnap m, HasCookies m, MonadFail m)
69+ => (R appRoute
70+ -> Text)
71+ -> ([Text]
72+ -> m ())
73+ -> Frontend (R appRoute)
74+ -> Map Text ByteString
75+ -> R (ObeliskRoute appRoute)
76+ -> m ()
77+serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
6578 where frontendApp = GhcjsApp
6679 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
6780 , _ghcjsApp_value = frontend
@@ -86,45 +99,48 @@ defaultFrontendGhcjsAssets = StaticAssets
8699 , _staticAssets_unprocessed = "frontend.jsexe"
87100 }
88101
89−runSnapWithCommandLineArgs :: Snap () -> IO ()
102+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
90103runSnapWithCommandLineArgs a = do
91104 -- Get the web server configuration from the command line
92− cmdLineConf <- commandLineConfig defaultConfig
105+ cmdLineConf <- liftIO $ commandLineConfig defaultConfig
93106 let httpConf = cmdLineConf
94107 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
95108 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
96109 }
97110 -- Start the web server
98− httpServe httpConf a
111+ liftIO $ httpServe httpConf a
99112
100113getPageName :: (MonadSnap m) => m PageName
101114getPageName = do
102115 p <- getsRequest rqPathInfo
103116 q <- getsRequest rqQueryString
104− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
105− pageNameEncoder' = bimap
106− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
107− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
108− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
117+ return $ byteStringsToPageName p q
109118
110119getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
111120getRouteWith e = do
112121 pageName <- getPageName
113122 return $ tryDecode e pageName
114123
115−serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
116−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
117− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
124+serveObeliskApp
125+ :: (MonadSnap m, HasCookies m, MonadFail m)
126+ => (R appRoute -> Text)
127+ -> ([Text] -> m ())
128+ -> GhcjsApp (R appRoute)
129+ -> Map Text ByteString
130+ -> R (ObeliskRoute appRoute)
131+ -> m ()
132+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
133+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
118134 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
119135 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
120− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
136+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
121137 ResourceRoute_JSaddleWarp :=> Identity _ -> do
122138 let msg = "Error: Obelisk.Backend received jsaddle request"
123139 liftIO $ putStrLn $ T.unpack msg
124140 writeText msg
125141 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
126142
127−serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
143+serveStaticAssets :: (MonadSnap m, MonadFail m) => StaticAssets -> [Text] -> m ()
128144serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
129145
130146data StaticAssets = StaticAssets
@@ -141,35 +157,58 @@ staticRenderContentType :: ByteString
141157staticRenderContentType = "text/html; charset=utf-8"
142158
143159--TODO: Don't assume we're being served at "/"
144−serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
145−serveGhcjsApp urlEnc app = \case
160+serveGhcjsApp
161+ :: (MonadSnap m, HasCookies m, MonadFail m)
162+ => (R appRouteComponent -> Text)
163+ -> GhcjsApp (R appRouteComponent)
164+ -> Map Text ByteString
165+ -> R (GhcjsAppRoute appRouteComponent)
166+ -> m ()
167+serveGhcjsApp urlEnc app config = \case
146168 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147169 modifyResponse $ setContentType staticRenderContentType
148− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
170+ modifyResponse $ setHeader "Cache-Control" "no-store private"
171+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
149172 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
150173
151−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
174+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
152175runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
153176 Left e -> fail $ "backend error:\n" <> T.unpack e
154− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
155− runSnapWithCommandLineArgs $ do
156− getRouteWith validFullEncoder >>= \case
157− Identity r -> case r of
158− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
159− InR obeliskRoute :=> Identity a ->
160− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
161−
162−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
177+ Right validFullEncoder -> do
178+ publicConfigs <- getPublicConfigs
179+ _backend_run backend $ \serveRoute -> do
180+ runSnapWithCommandLineArgs $ do
181+ getRouteWith validFullEncoder >>= \case
182+ Identity r -> case r of
183+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
184+ FullRoute_Frontend obeliskRoute :/ a ->
185+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
186+ obeliskRoute :/ a
187+
188+mkRouteToUrl :: Encoder Identity parse (R (FullRoute br fr)) PageName -> R fr -> Text
163189mkRouteToUrl validFullEncoder =
164190 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
165− in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
166−
167−
168−renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
169−renderGhcjsFrontend urlEnc route f = do
170− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
171− ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
191+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (FullRoute_Frontend $ ObeliskRoute_App k) :/ v
192+
193+renderGhcjsFrontend
194+ :: (MonadSnap m, HasCookies m)
195+ => (route -> Text)
196+ -> route
197+ -> Map Text ByteString
198+ -> Frontend route
199+ -> m ByteString
200+renderGhcjsFrontend urlEnc route configs f = do
201+ let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
172202 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
173− liftIO $ renderFrontendHtml urlEnc route
174− (_frontend_head f >> injectExecutableConfigs >> baseTag >> ghcjsPreload)
175− (_frontend_body f >> ghcjsScript)
203+ cookies <- askCookies
204+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
205+
206+instance HasCookies Snap where
207+ askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
208+
209+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
210+-- to frontend applications, and hence visible to end users).
211+getPublicConfigs :: IO (Map Text ByteString)
212+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
213+ where
214+ isMemberOf k = any (`T.isPrefixOf` k)
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/Demo.hs+2−1
@@ -4,6 +4,7 @@
44module Obelisk.CliApp.Demo where
55
66import Control.Concurrent (threadDelay)
7+import Control.Monad.Fail
78import Control.Monad.IO.Class
89import Data.Semigroup ((<>))
910import qualified Data.Text as T
@@ -14,7 +15,7 @@ import Control.Monad.Catch (MonadMask)
1415import Obelisk.CliApp
1516
1617cliDemo
17− :: ( MonadIO m, MonadMask m
18+ :: ( MonadIO m, MonadMask m, MonadFail m
1819 , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
1920 => m ()
2021cliDemo = withSpinner "CLI Demo" $ do
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/Process.hs+9−7
@@ -24,6 +24,7 @@ module Obelisk.CliApp.Process
2424
2525import Control.Monad ((<=<), join, void)
2626import Control.Monad.Except (throwError)
27+import Control.Monad.Fail
2728import Control.Monad.IO.Class (MonadIO, liftIO)
2829import Control.Lens (Prism', review)
2930import qualified Data.ByteString as BS
@@ -33,6 +34,7 @@ import Data.Monoid ((<>))
3334import Data.Text (Text)
3435import qualified Data.Text as T
3536import qualified Data.Text.Encoding as T
37+import Data.Text.Encoding.Error (lenientDecode)
3638import System.Exit (ExitCode (..))
3739import System.IO (Handle)
3840import System.IO.Streams (InputStream, handleToInputStream)
@@ -59,12 +61,12 @@ instance AsProcessFailure ProcessFailure where
5961 asProcessFailure = id
6062
6163readProcessAndLogStderr
62− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
64+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
6365 => Severity -> CreateProcess -> m Text
6466readProcessAndLogStderr sev process = do
6567 (out, _err) <- withProcess process $ \_out err -> do
6668 streamToLog =<< liftIO (streamHandle sev err)
67− liftIO $ T.decodeUtf8 <$> BS.hGetContents out
69+ liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
6870
6971readCreateProcessWithExitCode
7072 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
@@ -81,7 +83,7 @@ readCreateProcessWithExitCode process = do
8183-- which case it is advisable to call it with a non-Error severity for stderr, like
8284-- `callProcessAndLogOutput (Debug, Debug)`.
8385readProcessAndLogOutput
84− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
86+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
8587 => (Severity, Severity) -> CreateProcess -> m Text
8688readProcessAndLogOutput (sev_out, sev_err) process = do
8789 (_, Just out, Just err, p) <- createProcess $ process
@@ -89,7 +91,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
8991
9092 -- TODO interleave stdout and stderr in log correctly
9193 streamToLog =<< liftIO (streamHandle sev_err err)
92− outText <- liftIO $ T.decodeUtf8 <$> BS.hGetContents out
94+ outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
9395 putLogRaw sev_out outText
9496
9597 liftIO (waitForProcess p) >>= \case
@@ -105,7 +107,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
105107-- `callProcessAndLogOutput (Debug, Debug)`.
106108callProcessAndLogOutput
107109
108− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
110+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
109111 => (Severity, Severity) -> CreateProcess -> m ()
110112callProcessAndLogOutput (sev_out, sev_err) process =
111113 void $ withProcess process $ \out err -> do
@@ -149,7 +151,7 @@ callCommand cmd = do
149151 liftIO $ Process.callCommand cmd
150152
151153withProcess
152− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
154+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
153155 => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
154156withProcess process f = do -- TODO: Use bracket.
155157 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
@@ -173,7 +175,7 @@ streamToLog
173175streamToLog stream = fix $ \loop -> do
174176 liftIO (Streams.read stream) >>= \case
175177 Nothing -> return ()
176− Just (sev, line) -> putLogRaw sev (T.decodeUtf8 line) >> loop
178+ Just (sev, line) -> putLogRaw sev (T.decodeUtf8With lenientDecode line) >> loop
177179
178180-- | Pretty print a 'CmdSpec'
179181reconstructCommand :: Process.CmdSpec -> Text
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−23
@@ -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,18 +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−_spinnerCircleHalves :: SpinnerTheme
137−_spinnerCircleHalves = ["◐", "◓", "◑", "◒"]
138−
139−spinnerMoon :: SpinnerTheme
140−spinnerMoon = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"]
141−
142−defaultSpinnerTheme :: SpinnerTheme
143−defaultSpinnerTheme = spinnerMoon
144−
145140-- | Like `bracket` but the `release` function can know whether an exception was raised
146141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
147142bracket' 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+10−3
@@ -6,12 +6,14 @@
66{-# LANGUAGE GeneralizedNewtypeDeriving #-}
77{-# LANGUAGE MultiParamTypeClasses #-}
88{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE StandaloneDeriving #-}
910{-# LANGUAGE UndecidableInstances #-}
1011module Obelisk.CliApp.Types where
1112
1213import Control.Concurrent.MVar (MVar)
1314import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
15+import Control.Monad.Fail (MonadFail)
16+import Control.Monad.Log (LoggingT(..), MonadLog, Severity (..), WithSeverity (..), logMessage)
1517import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
1618import Control.Monad.Writer (WriterT)
1719import Control.Monad.State (StateT)
@@ -23,6 +25,7 @@ import Data.Text (Text)
2325import System.Exit (ExitCode (..), exitWith)
2426
2527import Obelisk.CliApp.TerminalString (TerminalString)
28+import Obelisk.CliApp.Theme (CliTheme)
2629
2730--------------------------------------------------------------------------------
2831
@@ -44,9 +47,11 @@ type CliThrow e m = MonadError e m
4447putLog :: CliLog m => Severity -> Text -> m ()
4548putLog sev = logMessage . Output_Log . WithSeverity sev
4649
50+deriving instance MonadFail m => MonadFail (LoggingT Output m)
51+
4752newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
4853 deriving
49− ( Functor, Applicative, Monad, MonadIO
54+ ( Functor, Applicative, Monad, MonadIO, MonadFail
5055 , MonadThrow, MonadCatch, MonadMask
5156 , MonadLog Output
5257 )
@@ -88,6 +93,8 @@ data CliConfig e = CliConfig
8893 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
8994 , -- | Failure handler. How to log error and what exit status to use.
9095 _cliConfig_errorLogExitCode :: e -> (Text, Int)
96+ , -- | Theme strings for spinners
97+ _cliConfig_theme :: CliTheme
9198 }
9299
93100class Monad m => HasCliConfig e m | m -> e where
@@ -111,7 +118,7 @@ newtype CliT e m a = CliT
111118 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
112119 }
113120 deriving
114− ( Functor, Applicative, Monad, MonadIO
121+ ( Functor, Applicative, Monad, MonadIO, MonadFail
115122 , MonadThrow, MonadCatch, MonadMask
116123 , MonadLog Output -- CliLog
117124 , MonadError e -- CliThrow
lib/command/obelisk-command.cabal+1−1
@@ -23,7 +23,7 @@ library
2323 , filepath
2424 , github
2525 , here
26− , hit
26+ , git
2727 , hnix
2828 , hpack
2929 , io-streams
lib/command/src/Obelisk/App.hs+5−2
@@ -12,6 +12,7 @@ module Obelisk.App where
1212
1313import Control.Lens
1414import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
1516import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1617import Control.Monad.Writer (WriterT)
1718import Control.Monad.State (StateT)
@@ -61,10 +62,11 @@ newtype ObeliskT m a = ObeliskT
6162 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
6263 }
6364 deriving
64− ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
65+ ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail
6566 , MonadLog Output -- CliLog
6667 , MonadError ObeliskError -- CliThrow ObeliskError
67− , HasCliConfig ObeliskError)
68+ , HasCliConfig ObeliskError
69+ )
6870
6971instance MonadTrans ObeliskT where
7072 lift = ObeliskT . lift . lift
@@ -104,6 +106,7 @@ type MonadInfallibleObelisk m =
104106type MonadObelisk m =
105107 ( MonadInfallibleObelisk m
106108 , CliThrow ObeliskError m
109+ , MonadFail m
107110 )
108111
109112getObeliskUserStateDir :: IO FilePath
lib/command/src/Obelisk/Command.hs+53−8
@@ -17,8 +17,10 @@ import Data.List
1717import Data.Maybe (catMaybes)
1818import qualified Data.Text as T
1919import Data.Text.Encoding
20+import Data.Text.Encoding.Error (lenientDecode)
2021import GHC.StaticPtr
2122import Options.Applicative
23+import Options.Applicative.Help.Pretty (text, (<$$>))
2224import System.Directory
2325import System.Environment
2426import System.FilePath
@@ -91,6 +93,8 @@ data ObCommand
9193 | ObCommand_Thunk ThunkCommand
9294 | ObCommand_Repl
9395 | ObCommand_Watch
96+ | ObCommand_Shell ShellOpts
97+ | ObCommand_Doc String [String] -- shell and list of packages
9498 | ObCommand_Internal ObInternal
9599 deriving Show
96100
@@ -102,7 +106,7 @@ data ObInternal
102106inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
103107inNixShell' p = withProjectRoot "." $ \root -> do
104108 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
105− projectShell root False "ghc" cmd
109+ projectShell root False "ghc" (Just cmd)
106110 where
107111 mkCmd = do
108112 argsCfg <- getArgsConfig
@@ -126,6 +130,12 @@ obCommand cfg = hsubparser
126130 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
127131 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
128132 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
133+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
134+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
135+ progDesc "List paths to haddock documentation for specified packages"
136+ <> footerDoc (Just $
137+ text "Hint: To open the documentation you can pipe the output of this command like"
138+ <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
129139 ])
130140 <|> subparser
131141 (mconcat
@@ -133,6 +143,9 @@ obCommand cfg = hsubparser
133143 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
134144 ])
135145
146+packageNames :: Parser [String]
147+packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
148+
136149deployCommand :: ArgsConfig -> Parser DeployCommand
137150deployCommand cfg = hsubparser $ mconcat
138151 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
@@ -210,16 +223,37 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
210223data ThunkCommand
211224 = ThunkCommand_Update [FilePath] (Maybe String)
212225 | ThunkCommand_Unpack [FilePath]
213− | ThunkCommand_Pack [FilePath]
226+ | ThunkCommand_Pack [FilePath] Bool
214227 deriving Show
215228
229+forceFlag :: Parser Bool
230+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."
231+
216232thunkCommand :: Parser ThunkCommand
217233thunkCommand = hsubparser $ mconcat
218234 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
219235 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
220− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
236+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
221237 ]
222238
239+data ShellOpts
240+ = ShellOpts
241+ { _shellOpts_shell :: String
242+ , _shellOpts_command :: Maybe String
243+ }
244+ deriving Show
245+
246+shellFlags :: Parser String
247+shellFlags =
248+ flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
249+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
250+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
251+
252+shellOpts :: Parser ShellOpts
253+shellOpts = ShellOpts
254+ <$> shellFlags
255+ <*> optional (strArgument (metavar "COMMAND"))
256+
223257parserPrefs :: ParserPrefs
224258parserPrefs = defaultPrefs
225259 { prefShowHelpOnEmpty = True
@@ -324,14 +358,13 @@ ob = \case
324358 Right (ThunkData_Packed ptr) -> return ptr
325359 Right (ThunkData_Checkout (Just ptr)) -> return ptr
326360 Right (ThunkData_Checkout Nothing) ->
327− getThunkPtr' False root
361+ getThunkPtr False root
328362 let sshKeyPath = _deployInitOpts_sshKey deployOpts
329363 hostname = _deployInitOpts_hostname deployOpts
330364 route = _deployInitOpts_route deployOpts
331365 adminEmail = _deployInitOpts_adminEmail deployOpts
332366 enableHttps = _deployInitOpts_enableHttps deployOpts
333− deployInit thunkPtr (root </> "config") deployDir
334− sshKeyPath hostname route adminEmail enableHttps
367+ deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
335368 DeployCommand_Push remoteBuilder -> do
336369 deployPath <- liftIO $ canonicalizePath "."
337370 deployPush deployPath $ case remoteBuilder of
@@ -344,9 +377,13 @@ ob = \case
344377 ObCommand_Thunk tc -> case tc of
345378 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
346379 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
347− ThunkCommand_Pack thunks -> forM_ thunks packThunk
380+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
348381 ObCommand_Repl -> runRepl
349382 ObCommand_Watch -> inNixShell' $ static runWatch
383+ ObCommand_Shell so -> withProjectRoot "." $ \root ->
384+ projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
385+ ObCommand_Doc shell pkgs -> withProjectRoot "." $ \root ->
386+ projectShell root False shell (Just $ haddockCommand pkgs)
350387 ObCommand_Internal icmd -> case icmd of
351388 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
352389 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
@@ -355,13 +392,21 @@ ob = \case
355392 liftIO $ runObelisk c $ deRefStaticPtr p
356393 ObInternal_CLIDemo -> cliDemo
357394
395+haddockCommand :: [String] -> String
396+haddockCommand pkgs = unwords
397+ [ "for p in"
398+ , unwords [getHaddockPath p ++ "/index.html" | p <- pkgs]
399+ , "; do echo $p; done"
400+ ]
401+ where getHaddockPath p = "$(ghc-pkg field " ++ p ++ " haddock-html --simple-output)"
402+
358403--TODO: Clean up all the magic strings throughout this codebase
359404
360405getArgsConfig :: IO ArgsConfig
361406getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
362407
363408encodeStaticKey :: StaticKey -> String
364−encodeStaticKey = T.unpack . decodeUtf8 . Base16.encode . LBS.toStrict . Binary.encode
409+encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
365410
366411-- TODO: Use failWith in place of fail to be consistent.
367412decodeStaticKey :: String -> Either String StaticKey
lib/command/src/Obelisk/Command/Deploy.hs+35−30
@@ -4,7 +4,6 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
66{-# LANGUAGE DeriveGeneric #-}
7−{-# LANGUAGE TypeApplications #-}
87module Obelisk.Command.Deploy where
98
109import Control.Lens
@@ -22,8 +21,8 @@ import qualified Data.Set as Set
2221import qualified Data.Text as T
2322import qualified Data.Text.IO as T
2423import GHC.Generics
25−import Nix.Convert
2624import Nix.Pretty
25+import Nix.String (principledMakeNixStringWithoutContext)
2726import Nix.Value
2827import System.Directory
2928import System.Environment (getEnvironment)
@@ -47,33 +46,38 @@ deployInit
4746 => ThunkPtr
4847 -> FilePath
4948 -> FilePath
50− -> FilePath
5149 -> [String] -- ^ hostnames
5250 -> String -- ^ route
5351 -> String -- ^ admin email
5452 -> Bool -- ^ enable https
5553 -> m ()
56−deployInit thunkPtr configDir deployDir sshKeyPath hostnames route adminEmail enableHttps = do
57− (hasConfigDir, localKey) <- withSpinner ("Preparing " <> T.pack deployDir) $ do
58− hasConfigDir <- liftIO $ do
59− createDirectoryIfMissing True deployDir
60− doesDirectoryExist configDir
54+deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
55+ localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
6156 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
6257 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
6358 True -> pure $ deployDir </> "ssh_key"
6459 callProcessAndLogOutput (Notice, Error) $
6560 proc "cp" [sshKeyPath, localKey]
6661 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
67− return $ (hasConfigDir, localKey)
62+ return localKey
6863 withSpinner "Validating configuration" $ do
6964 void $ getHostFromRoute enableHttps route -- make sure that hostname is present
7065 forM_ hostnames $ \hostname -> do
7166 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
7267 -- Note: we can't use a spinner here as this function will prompt the user.
7368 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
74− when hasConfigDir $ withSpinner "Importing project configuration" $ do
69+ --IMPORTANT: We cannot copy config directory from the development project to
70+ --the deployment directory. If we do, it's very likely someone will
71+ --accidentally create a production deployment that uses development
72+ --credentials to connect to some resources. This could result in, e.g.,
73+ --production data backed up to a dev environment.
74+ withSpinner "Creating project configuration directories" $ do
7575 callProcessAndLogOutput (Notice, Error) $
76− proc "cp" [ "-r" , "-T" , configDir , deployDir </> "config"]
76+ proc "mkdir" [ "-p"
77+ , deployDir </> "config" </> "backend"
78+ , deployDir </> "config" </> "common"
79+ , deployDir </> "config" </> "frontend"
80+ ]
7781 withSpinner "Writing deployment configuration" $ do
7882 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
7983 writeDeployConfig deployDir "enable_https" $ show enableHttps
@@ -103,7 +107,7 @@ deployPush deployPath getNixBuilders = do
103107 Right (ThunkData_Packed ptr) -> return ptr
104108 Right (ThunkData_Checkout _) -> do
105109 checkGitCleanStatus srcPath True >>= \case
106− True -> packThunk srcPath
110+ True -> packThunk False srcPath
107111 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
108112 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
109113 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -131,7 +135,7 @@ deployPush deployPath getNixBuilders = do
131135 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
132136 callProcess'
133137 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
134− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
138+ "nix-copy-closure" ["-v", "--to", "--use-substitutes", "root@" <> host, "--gzip", outputPath]
135139 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
136140 callProcessAndLogOutput (Notice, Warning) $
137141 proc "rsync"
@@ -168,13 +172,14 @@ deployPush deployPath getNixBuilders = do
168172deployUpdate :: MonadObelisk m => FilePath -> m ()
169173deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
170174
171−keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
172−keytoolToAndroidConfig conf = runIdentity $ do
173− path <- toValue $ Path $ _keytoolConfig_keystore conf
174− storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
175− alias <- toValue $ T.pack $ _keytoolConfig_alias conf
176− keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
177− return $ HM.fromList
175+keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176+keytoolToAndroidConfig conf =
177+ let path = nvPath $ _keytoolConfig_keystore conf
178+ fromStr = nvStr . principledMakeNixStringWithoutContext . T.pack
179+ storepass = fromStr $ _keytoolConfig_storepass conf
180+ alias = fromStr $ _keytoolConfig_alias conf
181+ keypass = fromStr $ _keytoolConfig_keypass conf
182+ in HM.fromList
178183 [ ("storeFile", path)
179184 , ("storePassword", storepass)
180185 , ("keyAlias", alias)
@@ -189,7 +194,7 @@ renderPlatformDeployment = \case
189194 Android -> "android"
190195 IOS -> "ios"
191196
192−deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
197+deployMobile :: forall m. MonadObelisk m => PlatformDeployment -> [String] -> m ()
193198deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
194199 let srcDir = root </> "src"
195200 configDir = root </> "config"
@@ -219,17 +224,17 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
219224 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
220225 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
221226 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
222− liftIO $ putStrLn $ show keytoolConfContents
223− releaseKey <- case eitherDecode keytoolConfContents of
227+ liftIO $ print keytoolConfContents
228+ releaseKey :: String <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
224229 Left err -> failWith $ T.pack err
225− Right conf -> do
226− let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
227− return $ printNix $ runIdentity nvset
230+ Right conf -> return $
231+ (printNix :: MonadObelisk m => NValue t Identity m -> String) $
232+ nvSet (keytoolToAndroidConfig conf) HM.empty
228233 let expr = mconcat
229234 [ "with (import ", srcDir, " {});"
230235 , "android.frontend.override (drv: { "
231236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
232− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
237+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
233238 , "})"
234239 ]
235240 return $ Target
@@ -240,7 +245,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
240245 IOS -> do
241246 let expr = mconcat
242247 [ "with (import ", srcDir, " {});"
243− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
244249 ]
245250 return $ Target
246251 { _target_path = Nothing
@@ -250,7 +255,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
250255 result <- nixCmd $ NixCmd_Build $ def
251256 & nixBuildConfig_outLink .~ OutLink_None
252257 & nixCmdConfig_target .~ nixBuildTarget
253− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
258+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> show result
254259 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
255260 where
256261 withEcho showEcho f = do
@@ -270,7 +275,7 @@ instance ToJSON KeytoolConfig
270275createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
271276createKeystore root config = do
272277 let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
273− callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
278+ callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
274279 where
275280 keytoolCmd = processToShellString "keytool"
276281 [ "-genkeypair", "-noprompt"
lib/command/src/Obelisk/Command/Project.hs+7−5
@@ -196,20 +196,22 @@ isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
196196-- | Run a command in the given shell for the current project
197197inProjectShell :: MonadObelisk m => String -> String -> m ()
198198inProjectShell shellName command = withProjectRoot "." $ \root ->
199− projectShell root True shellName command
199+ projectShell root True shellName (Just command)
200200
201201inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
202202inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
203− projectShell root False shellName command
203+ projectShell root False shellName (Just command)
204204
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
205+projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206206projectShell root isPure shellName command = do
207207 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208+ [ "default.nix"] <>
208209 [ "--pure" | isPure ] <>
209210 [ "-A"
210211 , "shells." <> shellName
211− , "--run", command
212− ]
212+ ] <> case command of
213+ Nothing -> []
214+ Just c -> ["--run", c]
213215 void $ liftIO $ waitForProcess ph
214216
215217setCtlc :: CreateProcess -> CreateProcess
lib/command/src/Obelisk/Command/Run.hs+50−10
@@ -10,10 +10,11 @@ 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
17+import Distribution.Compiler (CompilerFlavor(..))
1718import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
1819import Distribution.Parsec.ParseResult (runParseResult)
1920import Distribution.Pretty
@@ -46,6 +47,10 @@ data CabalPackageInfo = CabalPackageInfo
4647 -- ^ List of hs src dirs of the library component
4748 , _cabalPackageInfo_defaultExtensions :: [Extension]
4849 -- ^ List of globally enable extensions of the library component
50+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
51+ -- ^ List of globally set languages of the library component
52+ , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
53+ -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
4954 }
5055
5156-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -54,14 +59,32 @@ run = do
5459 pkgs <- getLocalPkgs
5560 withGhciScript pkgs $ \dotGhciPath -> do
5661 freePort <- getFreePort
57− assets <- withProjectRoot "." $ \root ->
58− -- `--raw` is not available with old nix-instantiate. It drops quotation
59− -- marks and trailing newline, so is very convenient for shelling out.
60− readProcessAndLogStderr Debug $
61− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
62+ assets <- withProjectRoot "." $ \root -> do
63+ let importableRoot = if "/" `isInfixOf` root
64+ then root
65+ else "./" <> root
66+ isDerivation <- readProcessAndLogStderr Debug $
67+ proc "nix"
68+ [ "eval"
69+ , "-f"
70+ , root
71+ , "(let a = import " <> importableRoot <> " {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
72+ , "--raw"
73+ -- `--raw` is not available with old nix-instantiate. It drops quotation
74+ -- marks and trailing newline, so is very convenient for shelling out.
75+ ]
76+ -- Check whether the impure static files are a derivation (and so must be built)
77+ if isDerivation == "1"
78+ then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
79+ proc "nix-build"
80+ [ "--no-out-link"
81+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
82+ ]
83+ else readProcessAndLogStderr Debug $
84+ proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
6285 putLog Debug $ "Assets impurely loaded from: " <> assets
6386 runGhcid dotGhciPath $ Just $ unwords
64− [ "run"
87+ [ "Obelisk.Run.run"
6588 , show freePort
6689 , "(runServeAsset " ++ show assets ++ ")"
6790 , "Backend.backend"
@@ -98,12 +121,12 @@ parseCabalPackage dir = do
98121 then Just <$> liftIO (readUTF8File cabalFp)
99122 else if hasHpack
100123 then do
101− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
124+ let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
102125 liftIO (readPackageConfig decodeOptions) >>= \case
103126 Left err -> do
104127 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
105128 return Nothing
106− Right (DecodeResult hpackPackage _ _) -> do
129+ Right (DecodeResult hpackPackage _ _ _) -> do
107130 return $ Just $ renderPackage [] hpackPackage
108131 else return Nothing
109132
@@ -121,6 +144,9 @@ parseCabalPackage dir = do
121144 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
122145 , _cabalPackageInfo_defaultExtensions =
123146 defaultExtensions $ libBuildInfo lib
147+ , _cabalPackageInfo_defaultLanguage =
148+ defaultLanguage $ libBuildInfo lib
149+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
124150 }
125151 Left (_, errors) -> do
126152 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -150,17 +176,27 @@ withGhciScript pkgs f = do
150176 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
151177
152178 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
179+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
180+ -- NOTE when no default-language is present cabal sets Haskell98
181+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
153182 extensionsLine = if extensions == mempty
154183 then ""
155184 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
185+ ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
186+ packageInfos >>= _cabalPackageInfo_compilerOptions
156187 dotGhci = unlines $
157188 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
189+ , case ghcOptions of
190+ [] -> ""
191+ xs -> ":set " <> intercalate " " xs
158192 , extensionsLine
193+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
159194 , ":load Backend Frontend"
160195 , "import Obelisk.Run"
161196 , "import qualified Frontend"
162197 , "import qualified Backend"
163198 ]
199+ warnDifferentLanguages language
164200 withSystemTempDirectory "ob-ghci" $ \fp -> do
165201 let dotGhciPath = fp </> ".ghci"
166202 liftIO $ writeFile dotGhciPath dotGhci
@@ -170,6 +206,10 @@ withGhciScript pkgs f = do
170206 rootedSourceDirs pkg = NE.toList $
171207 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
172208
209+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
210+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
211+warnDifferentLanguages _ = return ()
212+
173213-- | Run ghci repl
174214runGhciRepl
175215 :: MonadObelisk m
lib/command/src/Obelisk/Command/Thunk.hs+9−14
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
2626 , readThunk
2727 , updateThunk
2828 , getThunkPtr
29− , getThunkPtr'
3029 , parseGitUri
3130 , uriThunkPtr
3231 ) where
@@ -95,7 +94,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
9594
9695-- | A specific revision of data; it may be available from multiple sources
9796data ThunkRev = ThunkRev
98− { _thunkRev_commit :: Ref
97+ { _thunkRev_commit :: Ref Ref.SHA1
9998 , _thunkRev_nixSha256 :: NixSha256
10099 }
101100 deriving (Show, Eq, Ord)
@@ -150,7 +149,7 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
150149 ThunkSource_GitHub s -> _gitHubSource_branch s
151150 ThunkSource_Git s -> _gitSource_branch s
152151
153−commitNameToRef :: Name Commit -> Ref
152+commitNameToRef :: Name Commit -> Ref Ref.SHA1
154153commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
155154
156155-- TODO: Use spinner here.
@@ -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/command/src/Obelisk/Command/Utils.hs+1−1
@@ -134,7 +134,7 @@ gitLsRemote repository mRef mBranch = do
134134 ["ls-remote", "--exit-code", repository, branchName]
135135 let t = T.pack out
136136 maps <- case MP.runParser parseLsRemote "" t of
137− Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
137+ Left err -> failWith $ T.pack $ MP.errorBundlePretty err
138138 Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
139139 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140140 pure (exitCode, maps)
lib/command/src/Obelisk/Command/VmBuilder.hs+2−1
@@ -156,7 +156,8 @@ testLinuxBuild stateDir
156156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157157 | otherwise = do
158158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159− [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
159+ [ "--no-out-link"
160+ , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160161 , "--builders", nixBuildersArgString stateDir
161162 ]
162163 unless (exitCode == ExitSuccess) $ do
lib/executable-config/default.nix+4−1
@@ -16,9 +16,12 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
1616 exit 1
1717 fi
1818 cp -a "${config}"/* "$out/config"
19+ find "$out/config" -type f -printf '%P\0' >"$out/config.files"
1920 '');
2021in with nixpkgs.haskell.lib; {
21− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
22+ haskellOverlay = self: super: {
23+ obelisk-executable-config-lookup = self.callCabal2nix "obelisk-executable-config-lookup" (filterGitSource ./lookup) {};
24+ };
2225 platforms = {
2326 android = {
2427 # Inject the given config directory into an android assets folder
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2
@@ -9,8 +9,8 @@ library
99 build-depends:
1010 base,
1111 bytestring,
12− directory,
13− filepath,
12+ base64-bytestring,
13+ containers,
1414 reflex-dom-core,
1515 text
1616
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40
@@ -2,50 +2,24 @@
22module Obelisk.ExecutableConfig.Inject where
33
44import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
6−import Data.Semigroup ((<>))
5+import Control.Monad.IO.Class (MonadIO)
76import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Base64 as B64
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Semigroup ((<>))
811import Data.Text (Text)
9−import qualified Data.Text as T
10−import qualified Data.Text.IO as T
12+import qualified Data.Text.Encoding as T
1113import 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
2114
2215-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23−-- whose value is the provided configuration
24−injectPure :: DomBuilder t m => Text -> Text -> m ()
16+-- whose value is the provided configuration, encoded in base64.
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
2518injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
27− in elAttr "script" attrs $ text value
28−
29−-- | Scans the "common" and "frontend" configuration folders and produces
30−-- injectable @<script>@ tags containing the configuration keys (filepaths) and
31−-- values.
32−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
33−injectExecutableConfigs = do
34− cfgC <- getConfigs "config/common"
35− cfgF <- getConfigs "config/frontend"
36− mapM_ (uncurry injectPure) (cfgC <> cfgF)
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
3721
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 []
22+-- | Produces injectable @<script>@ tags containing the configuration keys
23+-- (filepaths) and values.
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
25+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
lib/executable-config/lookup/default.nixdeleted−24
@@ -1,24 +0,0 @@
1−{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
3−, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
4−}:
5−let isAndroid = hostPlatform.libc == "bionic";
6− isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
7− isGhcjs = ghc.isGhcjs or false;
8−in mkDerivation {
9− pname = "obelisk-executable-config";
10− version = "0.1";
11− src = ./.;
12− libraryHaskellDepends = [
13− base bytestring filepath text transformers
14− ] ++ (if isAndroid then [
15− android-activity
16− ] else if isIOS then [
17− jsaddle-wkwebview
18− ] else if isGhcjs then [
19− ghcjs-dom
20− ] else [
21− ]);
22− license = stdenv.lib.licenses.bsd3;
23−}
24−
lib/executable-config/lookup/obelisk-executable-config-lookup.cabalnew file+45
@@ -0,0 +1,45 @@
1+name: obelisk-executable-config-lookup
2+version: 0.1.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
8+ if os(ios)
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
11+ build-depends: jsaddle-wkwebview
12+ build-depends: directory
13+ else
14+ if os(linux-android)
15+ hs-source-dirs: src-android, src
16+ c-sources: cbits/ExecutableConfig.c
17+ other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
18+ build-depends: android-activity
19+ ld-options: -landroid
20+ else
21+ if impl(ghcjs)
22+ hs-source-dirs: src-ghcjs, src
23+ build-depends: ghcjs-dom
24+ else
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
27+ build-depends: directory
28+
29+ build-depends:
30+ base,
31+ base64-bytestring,
32+ bytestring,
33+ containers,
34+ filepath,
35+ text,
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
44+
45+ ghc-options: -Wall
lib/executable-config/lookup/obelisk-executable-config.cabaldeleted−30
@@ -1,30 +0,0 @@
1−name: obelisk-executable-config
2−version: 0.1.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− exposed-modules: Obelisk.ExecutableConfig
8− if os(ios)
9− hs-source-dirs: src-ios
10− build-depends: jsaddle-wkwebview
11− else
12− if os(linux-android)
13− hs-source-dirs: src-android
14− c-sources: cbits/ExecutableConfig.c
15− other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
16− build-depends: android-activity
17− ld-options: -landroid
18− else
19− if impl(ghcjs)
20− hs-source-dirs: src-ghcjs
21− build-depends: ghcjs-dom
22− else
23− hs-source-dirs: src
24−
25− build-depends:
26− base,
27− bytestring,
28− filepath,
29− text,
30− transformers
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Control.Exception (bracket)
6+import Control.Monad (forM)
7+import Data.ByteString (ByteString)
8+import qualified Data.ByteString as BS
9+import Data.Map (Map)
10+import qualified Data.Map as Map
11+import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
13+import Foreign.C.String (withCString)
14+import Foreign.Ptr (nullPtr)
15+
16+import Obelisk.ExecutableConfig.Internal.AssetManager
17+
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
19+getFromMgr mgr name = do
20+ let open = do
21+ a <- BS.useAsCString name $ \fn ->
22+ assetManager_open mgr fn 3
23+ return $ if unAAsset a == nullPtr
24+ then Nothing
25+ else Just a
26+ close = mapM_ asset_close
27+ bracket open close $ mapM $ \asset -> do
28+ b <- asset_getBuffer asset
29+ l <- asset_getLength asset
30+ BS.packCStringLen (b, fromIntegral l)
31+
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
34+ mgr <- assetManagerFromJava mgrObj
35+ let openDir = do
36+ d <- withCString "config.files" $ \fn ->
37+ assetManager_open mgr fn 3
38+ return $ if unAAsset d == nullPtr
39+ then Nothing
40+ else Just d
41+ closeDir = mapM_ asset_close
42+ configPaths <- bracket openDir closeDir $ \case
43+ Just asset -> do
44+ b <- asset_getBuffer asset
45+ l <- asset_getLength asset
46+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
47+ Nothing -> error "could not open configuration manifest 'config.files'"
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
51+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
54+
55+lines0 :: BS.ByteString -> [BS.ByteString]
56+lines0 ps
57+ | BS.null ps = []
58+ | otherwise = case BS.elemIndex 0 ps of
59+ Nothing -> [ps]
60+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Base64 as B64
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Maybe (catMaybes)
10+import Data.Text (Text)
11+import qualified Data.Text.Encoding as T
12+import Data.Traversable (for)
13+import GHCJS.DOM
14+import GHCJS.DOM.Document (getHead)
15+import GHCJS.DOM.DOMStringMap (get)
16+import GHCJS.DOM.Element (getInnerHTML)
17+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
19+import GHCJS.DOM.ParentNode (querySelectorAll)
20+import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
21+
22+getConfigs :: IO (Map Text ByteString)
23+getConfigs = do
24+ Just doc <- currentDocument
25+ Just hd <- getHead doc
26+ nodes <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
27+ fmap Map.fromList $ for nodes $ \node -> do
28+ e <- castTo HTMLElement node >>= \case
29+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
30+ Just htmlE -> return htmlE
31+ dataset <- getDataset e
32+ (,)
33+ -- the key is camelCased: https://html.spec.whatwg.org/multipage/dom.html#dom-dataset
34+ <$> get dataset ("obeliskExecutableConfigInjectKey" :: Text)
35+ <*> (fmap decodeOrFail (getInnerHTML e))
36+ where
37+ decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
38+ Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
39+ Right x' -> x'
40+
41+-- | Collect all nodes in the node list.
42+--
43+-- TODO: this and the version in obelisk-frontend should be
44+-- upstreamed to jsaddle.
45+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
46+nodeListNodes es = do
47+ len <- getLength es
48+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
49+ -- accidentally traversing over [0..maxBound::Word]
50+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
51+ pure $ catMaybes nodes
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Control.Monad
5−import Control.Monad.Trans.Maybe
6−import Data.ByteString as BS
7−import Data.Text (Text)
8−import qualified Data.Text as T
9−import qualified Data.Text.IO as T
10−import qualified Data.Text.Encoding as T
11−import Language.Javascript.JSaddle.WKWebView
12−import System.FilePath.Posix ((</>))
13−import System.IO.Error
14−
15−get :: Text -> IO (Maybe Text)
16−get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
18−
19−catchDoesNotExist :: IO a -> IO (Maybe a)
20−catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
21− where
22− doesNotExist e = if isDoesNotExistError e then Just () else Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hsnew file+17
@@ -0,0 +1,17 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.ByteString (ByteString)
5+import Data.Map (Map)
6+import Data.Text (Text)
7+import qualified Data.Text as T
8+import qualified Data.Text.Encoding as T
9+import Language.Javascript.JSaddle.WKWebView
10+import System.FilePath.Posix
11+
12+import Obelisk.Configs.Internal.Directory
13+
14+getConfigs :: IO (Map Text ByteString)
15+getConfigs = mainBundleResourcePath >>= \case
16+ Nothing -> error "Could not get bundle resource path."
17+ Just p -> getConfigsFromDirectory $ T.unpack (T.decodeUtf8 p) </> "config"
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hsnew file+18
@@ -0,0 +1,18 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.Map (Map)
6+import qualified Data.Map as Map
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Data.ByteString (ByteString)
10+import qualified Data.ByteString as BS
11+import System.Directory
12+import System.FilePath
13+import Control.Monad
14+
15+import Obelisk.Configs.Internal.Directory
16+
17+getConfigs :: IO (Map Text ByteString)
18+getConfigs = getConfigsFromDirectory "config"
lib/executable-config/lookup/src/Obelisk/Configs.hsnew file+128
@@ -0,0 +1,128 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.ByteString (ByteString)
31+import Data.Map (Map)
32+import qualified Data.Map as Map
33+import Data.Text (Text)
34+import qualified Data.Text.Encoding as T
35+import Reflex
36+import Reflex.Host.Class
37+import Reflex.Dom
38+#ifndef ghcjs_HOST_OS
39+import Language.Javascript.JSaddle (MonadJSM)
40+#endif
41+
42+class Monad m => HasConfigs m where
43+ getConfigs :: m (Map Text ByteString)
44+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
45+ getConfigs = lift getConfigs
46+ getConfig :: Text -> m (Maybe ByteString)
47+ getConfig k = do
48+ configs <- getConfigs
49+ return $ Map.lookup k configs
50+
51+instance Monad m => HasConfigs (ConfigsT m) where
52+ getConfigs = ConfigsT ask
53+
54+getTextConfig :: HasConfigs m => Text -> m (Maybe Text)
55+getTextConfig k = fmap T.decodeUtf8 <$> getConfig k
56+
57+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
58+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
59+instance HasConfigs m => HasConfigs (EventWriterT t w m)
60+instance HasConfigs m => HasConfigs (PostBuildT t m)
61+instance HasConfigs m => HasConfigs (QueryT t q m)
62+instance HasConfigs m => HasConfigs (ReaderT r m)
63+instance HasConfigs m => HasConfigs (RequesterT t request response m)
64+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
65+instance HasConfigs m => HasConfigs (TriggerEventT t m)
66+
67+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
68+ deriving
69+ ( Functor
70+ , Applicative
71+ , Monad
72+ , MonadPlus
73+ , Alternative
74+ , MonadFix
75+ , MonadIO
76+ , MonadBase m'
77+ , MonadBaseControl m'
78+ , MonadRef
79+ , MonadTrans
80+ , DomBuilder t
81+ , MonadHold t
82+ , MonadReflexCreateTrigger t
83+ , MonadSample t
84+ , NotReady t
85+ , PostBuild t
86+ , TriggerEvent t
87+ , HasDocument
88+ , DomRenderHook t
89+ , HasJSContext
90+ , HasJS js
91+#ifndef ghcjs_HOST_OS
92+ , MonadJSM
93+#endif
94+ )
95+
96+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
97+ type Performable (ConfigsT m) = ConfigsT (Performable m)
98+ performEvent e = ConfigsT $ ReaderT $ \configs ->
99+ performEvent $ runConfigsT configs <$> e
100+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
101+ performEvent_ $ runConfigsT configs <$> e
102+
103+instance Adjustable t m => Adjustable t (ConfigsT m) where
104+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
105+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
106+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
107+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
108+
109+instance Prerender js t m => Prerender js t (ConfigsT m) where
110+ type Client (ConfigsT m) = ConfigsT (Client m)
111+ prerender server client = ConfigsT $ ReaderT $ \configs ->
112+ prerender (runConfigsT configs server) (runConfigsT configs client)
113+
114+instance PrimMonad m => PrimMonad (ConfigsT m) where
115+ type PrimState (ConfigsT m) = PrimState m
116+ primitive = lift . primitive
117+
118+runConfigsT
119+ :: Map Text ByteString
120+ -> ConfigsT m a
121+ -> m a
122+runConfigsT cs child = runReaderT (unConfigsT child) cs
123+
124+mapConfigsT
125+ :: (forall x. m x -> n x)
126+ -> ConfigsT m a
127+ -> ConfigsT n a
128+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabal+8−1
@@ -7,10 +7,15 @@ 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,
16+ mtl,
17+ obelisk-executable-config-inject,
18+ obelisk-executable-config-lookup,
1419 obelisk-route,
1520 primitive,
1621 ref-tf,
@@ -18,5 +23,7 @@ library
1823 reflex-dom-core,
1924 text,
2025 transformers
21− exposed-modules: Obelisk.Frontend
26+ exposed-modules:
27+ Obelisk.Frontend
28+ Obelisk.Frontend.Cookie
2229 ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hs+178−92
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
@@ -5,6 +6,7 @@
56{-# LANGUAGE MultiParamTypeClasses #-}
67{-# LANGUAGE OverloadedStrings #-}
78{-# LANGUAGE RankNTypes #-}
9+{-# LANGUAGE RecursiveDo #-}
810{-# LANGUAGE ScopedTypeVariables #-}
911{-# LANGUAGE TemplateHaskell #-}
1012{-# LANGUAGE TypeFamilies #-}
@@ -13,39 +15,54 @@ module Obelisk.Frontend
1315 ( ObeliskWidget
1416 , Frontend (..)
1517 , runFrontend
18+ , runFrontendWithConfigsAndCurrentRoute
1619 , renderFrontendHtml
20+ , removeHTMLConfigs
21+ , FrontendMode (..)
22+ , module Obelisk.Frontend.Cookie
1723 ) where
1824
1925import Prelude hiding ((.))
2026
2127import Control.Category
22−import Control.Concurrent
2328import Control.Lens
24−import Control.Monad
2529import Control.Monad.Fix
2630import Control.Monad.IO.Class
2731import Control.Monad.Primitive
32+import Control.Monad.Reader
2833import Control.Monad.Ref
29−import Control.Monad.Trans.Class
34+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
3035import Data.ByteString (ByteString)
31−import Data.Dependent.Sum (DSum (..))
36+import Data.Foldable (for_)
3237import Data.Functor.Sum
33−import Data.IORef
38+import Data.Map (Map)
39+import Data.Maybe (catMaybes)
3440import Data.Monoid ((<>))
3541import Data.Text (Text)
36−import GHCJS.DOM hiding (bracket, catch)
37−import GHCJS.DOM.Document
38−import GHCJS.DOM.Node
42+import qualified GHCJS.DOM as DOM
3943import qualified GHCJS.DOM.Types as DOM
40−import Language.Javascript.JSaddle (JSM)
44+import qualified GHCJS.DOM.History as DOM
45+import qualified GHCJS.DOM.Window as DOM
46+import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
47+import GHCJS.DOM (currentDocument)
48+import GHCJS.DOM.Document (getHead)
49+import GHCJS.DOM.Node (Node, removeChild_)
50+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
51+import GHCJS.DOM.ParentNode (querySelectorAll)
52+import Obelisk.Frontend.Cookie
4153import Obelisk.Route.Frontend
4254import Reflex.Dom.Core
4355import Reflex.Host.Class
44−import qualified Reflex.TriggerEvent.Base as TriggerEvent
56+import Obelisk.Configs
57+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
58+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
59+import Web.Cookie
60+
61+import Debug.Trace
4562
4663makePrisms ''Sum
4764
48−type ObeliskWidget t x route m =
65+type ObeliskWidget js t route m =
4966 ( DomBuilder t m
5067 , MonadFix m
5168 , MonadHold t m
@@ -53,8 +70,6 @@ type ObeliskWidget t x route m =
5370 , MonadReflexCreateTrigger t m
5471 , PostBuild t m
5572 , PerformEvent t m
56− , MonadIO m
57− , MonadIO (Performable m)
5873 , TriggerEvent t m
5974 , HasDocument m
6075 , MonadRef m
@@ -63,106 +78,177 @@ type ObeliskWidget t x route m =
6378 , Ref (Performable m) ~ Ref IO
6479 , MonadFix (Performable m)
6580 , PrimMonad m
66− , Prerender x m
67− , SetRoute t route m
81+ , Prerender js t m
82+ , PrebuildAgnostic t route m
83+ , PrebuildAgnostic t route (Client m)
84+ , HasConfigs m
85+ , HasCookies m
86+ , MonadIO (Performable m)
87+ )
88+
89+type PrebuildAgnostic t route m =
90+ ( SetRoute t route m
6891 , RouteToUrl route m
92+ , MonadFix m
93+ , HasConfigs m
94+ , HasConfigs (Performable m)
6995 )
7096
7197data Frontend route = Frontend
72− { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
73− , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
98+ { _frontend_head :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
99+ , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
74100 }
75101
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
102+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
103+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
77104
78−-- | A widget that isn't attached to any particular part of the DOM hierarchy
79−type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
105+removeHTMLConfigs :: JSM ()
106+removeHTMLConfigs = void $ runMaybeT $ do
107+ doc <- MaybeT currentDocument
108+ hd <- MaybeT $ getHead doc
109+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
110+ for_ es $ removeChild_ hd
80111
81−type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
112+-- | Collect all nodes in the node list.
113+--
114+-- TODO: this and the version in exe-config/ghcjs/lookup should be
115+-- upstreamed to jsaddle.
116+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
117+nodeListNodes es = do
118+ len <- getLength es
119+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
120+ -- accidentally traversing over [0..maxBound::Word]
121+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
122+ pure $ catMaybes nodes
82123
83−--TODO: Rename
84−{-# INLINABLE attachWidget''' #-}
85−attachWidget'''
86− :: (EventChannel -> PerformEventT DomTimeline DomHost (IORef (Maybe (EventTrigger DomTimeline ()))))
87− -> IO ()
88−attachWidget''' w = runDomHost $ do
89− events <- liftIO newChan
90− (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
91− mPostBuildTrigger <- readRef postBuildTriggerRef
92− forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
93− liftIO $ processAsyncEvents events fc
124+setInitialRoute :: Bool -> JSM ()
125+setInitialRoute useHash = do
126+ traceM "setInitialRoute"
127+ window <- DOM.currentWindowUnchecked
128+ initialLocation <- DOM.getLocation window
129+ initialUri <- getLocationUri initialLocation
130+ history <- DOM.getHistory window
131+ DOM.replaceState history jsNull ("" :: Text) $ Just $
132+ show $ setAdaptedUriPath useHash "/" initialUri
94133
95−--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
96−runWithHeadAndBody
97− :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
98− -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
99− -> FloatingWidget () ()
100− )
101− -> JSM ()
102−runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
103− globalDoc <- currentDocumentUnchecked
104− headFragment <- createDocumentFragment globalDoc
105− bodyFragment <- createDocumentFragment globalDoc
106− unreadyChildren <- liftIO $ newIORef 0
107− let commit = do
108− headElement <- getHeadUnchecked globalDoc
109− bodyElement <- getBodyUnchecked globalDoc
110− void $ inAnimationFrame' $ \_ -> do
111− replaceElementContents headElement headFragment
112− replaceElementContents bodyElement bodyFragment
113− liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
114− (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
115− let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
116− appendImmediateDom df w = do
117− events' <- TriggerEvent.askEvents
118− lift $ do
119− doc <- getOwnerDocumentUnchecked df
120− let builderEnv = ImmediateDomBuilderEnv
121− { _immediateDomBuilderEnv_document = doc
122− , _immediateDomBuilderEnv_parent = toNode df
123− , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
124− , _immediateDomBuilderEnv_commitAction = commit
125− }
126− runImmediateDomBuilderT w builderEnv events'
127− flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
128− liftIO (readIORef unreadyChildren) >>= \case
129− 0 -> DOM.liftJSM commit
130− _ -> return ()
131− return postBuildTriggerRef
134+data FrontendMode = FrontendMode
135+ { _frontendMode_hydrate :: Bool
136+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
137+ -- than building new DOM
138+ , _frontendMode_adjustRoute :: Bool
139+ -- ^ The page can't use regular routes, so encode routes into the hash
140+ -- instead
141+ }
132142
133−runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
143+-- | Run the frontend, setting the initial route to "/" on platforms where no
144+-- route exists ambiently in the context (e.g. anything but web).
145+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
146+runFrontend
147+ :: forall backendRoute route
148+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
149+ -> Frontend (R route)
150+ -> JSM ()
134151runFrontend validFullEncoder frontend = do
135− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
152+ let mode = FrontendMode
153+ { _frontendMode_hydrate =
154+#ifdef ghcjs_HOST_OS
155+ True
156+#else
157+ False
158+#endif
159+ , _frontendMode_adjustRoute =
160+#ifdef ghcjs_HOST_OS
161+ False
162+#else
163+ True
164+#endif
165+ }
166+ configs <- liftIO Lookup.getConfigs
167+ when (_frontendMode_hydrate mode) removeHTMLConfigs
168+ -- There's no fundamental reason that adjustRoute needs to control setting the
169+ -- initial route and *also* the useHash parameter; that's why these are
170+ -- separate here. However, currently, they are always the same.
171+ when (_frontendMode_adjustRoute mode) $ do
172+ setInitialRoute $ _frontendMode_adjustRoute mode
173+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
174+
175+runFrontendWithConfigsAndCurrentRoute
176+ :: forall backendRoute frontendRoute
177+ . FrontendMode
178+ -> Map Text ByteString
179+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
180+ -> Frontend (R frontendRoute)
181+ -> JSM ()
182+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
183+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
136184 errorLeft = \case
137185 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
138186 Right x -> Identity x
139− runMyRouteViewT
140− :: ( TriggerEvent t m
141− , PerformEvent t m
142− , MonadHold t m
187+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
188+ , Ref (Performable m) ~ Ref IO
189+ , Ref m ~ Ref IO
190+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
191+ , MonadHold DomTimeline m
192+ , MonadRef m
193+ , MonadRef (Performable m)
194+ , MonadReflexCreateTrigger DomTimeline m
195+ , PerformEvent DomTimeline m
196+ , PostBuild DomTimeline m
197+ , PrimMonad m
198+ , MonadSample DomTimeline (Performable m)
143199 , DOM.MonadJSM m
144− , DOM.MonadJSM (Performable m)
145− , MonadFix m
200+ , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
201+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
146202 , MonadFix (Performable m)
203+ , MonadFix m
204+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
205+ , MonadIO (Performable m)
147206 )
148− => RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
149− -> m a
150− runMyRouteViewT = runRouteViewT ve
151− runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
152− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ _frontend_head frontend
153− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ _frontend_body frontend
207+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
208+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
209+ -> FloatingWidget () ()
210+ w appendHead appendBody = do
211+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
212+ (switchover'', fire) <- newTriggerEvent
213+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
214+ -- The order here is important - baseTag has to be before headWidget!
215+ baseTag
216+ _frontend_head frontend
217+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
218+ _frontend_body frontend
219+ switchover' <- case _frontendMode_hydrate mode of
220+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
221+ False -> getPostBuild
222+ performEvent_ $ liftIO (fire ()) <$ switchover'
223+ pure switchover''
224+ pure ()
225+ if _frontendMode_hydrate mode
226+ then runHydrationWidgetWithHeadAndBody (pure ()) w
227+ else runImmediateWidgetWithHeadAndBody w
154228
155229renderFrontendHtml
156− :: (t ~ DomTimeline)
157− => (r' -> Text)
230+ :: ( t ~ DomTimeline
231+ , MonadIO m
232+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
233+ )
234+ => Map Text ByteString
235+ -> Cookies
236+ -> (r -> Text)
158237 -> r
159− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
160− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
161− -> IO ByteString
162−renderFrontendHtml urlEnc route headWidget bodyWidget = do
238+ -> Frontend r
239+ -> widget ()
240+ -> widget ()
241+ -> m ByteString
242+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
163243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
164− html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
244+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
165245 el "html" $ do
166− el "head" headWidget
167− el "body" bodyWidget
246+ el "head" $ do
247+ baseTag
248+ injectExecutableConfigs configs
249+ _frontend_head frontend
250+ headExtra
251+ el "body" $ do
252+ _frontend_body frontend
253+ bodyExtra
168254 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.Configs
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 (ConfigsT m)
50+instance HasConfigs m => HasConfigs (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+7−2
@@ -6,12 +6,14 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base,
9+ aeson,
10+ bytestring,
911 categories,
1012 constraints,
13+ constraints-extras,
1114 containers,
1215 dependent-map,
1316 dependent-sum,
14− dependent-sum-universe-orphans,
1517 dependent-sum-template,
1618 either,
1719 ghcjs-dom,
@@ -21,15 +23,18 @@ library
2123 monad-control,
2224 mtl,
2325 network-uri,
26+ obelisk-executable-config-lookup,
2427 primitive,
2528 ref-tf,
2629 reflex,
2730 reflex-dom-core,
2831 template-haskell,
32+ tabulation,
2933 text,
34+ th-extras,
3035 transformers,
3136 universe,
32− universe-template
37+ universe-dependent-sum
3338 exposed-modules: Obelisk.Route
3439 Obelisk.Route.TH
3540 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+340−39
@@ -12,13 +12,16 @@
1212{-# LANGUAGE PatternSynonyms #-}
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
1516{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeApplications #-}
1618{-# LANGUAGE TypeFamilies #-}
1719{-# LANGUAGE TypeOperators #-}
1820{-# LANGUAGE ViewPatterns #-}
1921module Obelisk.Route
2022 ( R
2123 , pattern (:/)
24+ , hoistR
2225 , PageName
2326 , PathQuery
2427 , Encoder
@@ -60,10 +63,15 @@ module Obelisk.Route
6063 , shadowEncoder
6164 , prismEncoder
6265 , rPrism
66+ , _R
6367 , obeliskRouteEncoder
6468 , obeliskRouteSegment
6569 , pageNameEncoder
6670 , handleEncoder
71+ , FullRoute (..)
72+ , _FullRoute_Frontend
73+ , _FullRoute_Backend
74+ , mkFullRouteEncoder
6775 , ObeliskRoute (..)
6876 , _ObeliskRoute_App
6977 , _ObeliskRoute_Resource
@@ -78,6 +86,19 @@ module Obelisk.Route
7886 , void1Encoder
7987 , pathSegmentsTextEncoder
8088 , queryParametersTextEncoder
89+ , renderObeliskRoute
90+ , renderBackendRoute
91+ , renderFrontendRoute
92+ , readShowEncoder
93+ , integralEncoder
94+ , pathSegmentEncoder
95+ , queryOnlyEncoder
96+ , Decoder(..)
97+ , dmapEncoder
98+ , fieldMapEncoder
99+ , pathFieldEncoder
100+ , jsonEncoder
101+ , byteStringsToPageName
81102 ) where
82103
83104import Prelude hiding ((.), id)
@@ -88,8 +109,32 @@ import qualified Control.Categorical.Functor as Cat
88109import Control.Categorical.Bifunctor
89110import Control.Category.Associative
90111import Control.Category.Monoidal
91−import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
112+import Control.Category.Braided
113+import Control.Lens
114+ ( Identity (..)
115+ , (^.)
116+ , (^?)
117+ , _Just
118+ , _Nothing
119+ , Cons(..)
120+ , from
121+ , imap
122+ , iso
123+ , Iso'
124+ , itraverse
125+ , makePrisms
126+ , Prism'
127+ , prism'
128+ , re
129+ , view
130+ , Wrapped (..)
131+ )
92132import Control.Monad.Except
133+import Control.Monad.Writer (execWriter, tell)
134+import qualified Control.Monad.State.Strict as State
135+import Control.Monad.Trans (lift)
136+import qualified Data.ByteString as BS
137+import qualified Data.ByteString.Lazy as BSL
93138import Data.Dependent.Sum (DSum (..))
94139import Data.Dependent.Map (DMap)
95140import qualified Data.Dependent.Map as DMap
@@ -106,16 +151,19 @@ import Data.Maybe
106151import Data.Monoid ((<>))
107152import Data.Set (Set)
108153import qualified Data.Set as Set
109−import Data.Some (Some)
110−import qualified Data.Some as Some
111−import Data.Some.Universe.Orphans ()
154+import Data.Some (Some(Some))
112155import Data.Text (Text)
113156import qualified Data.Text as T
114−import Data.Text.Encoding
157+import qualified Data.Text.Encoding as T
115158import Data.Universe
159+import Data.Universe.Some
116160import Network.HTTP.Types.URI
161+import qualified Numeric.Lens
117162import Obelisk.Route.TH
118163import Text.Read (readMaybe)
164+import Data.Tabulation
165+import qualified Data.Aeson as Aeson
166+import Data.Aeson (FromJSON, ToJSON)
119167
120168-- Design goals:
121169-- No start-up time on the frontend (not yet met)
@@ -141,13 +189,16 @@ import Text.Read (readMaybe)
141189
142190type R f = DSum f Identity --TODO: Better name
143191
144−--TODO: COMPLETE pragma
192+{-# COMPLETE (:/) #-}
145193infixr 5 :/
146194pattern (:/) :: f a -> a -> R f
147195pattern a :/ b = a :=> Identity b
148196
149197mapSome :: (forall a. f a -> g a) -> Some f -> Some g
150−mapSome f (Some.This a) = Some.This $ f a
198+mapSome f (Some a) = Some $ f a
199+
200+hoistR :: (forall x. f x -> g x) -> R f -> R g
201+hoistR f (x :=> Identity y) = f x :/ y
151202
152203--------------------------------------------------------------------------------
153204-- Encoder fundamentals
@@ -182,7 +233,7 @@ decode e x = runIdentity (tryDecode e x)
182233tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
183234tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
184235
185−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
236+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
186237-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
187238-- one should usually be applying decode and encode to the same 'Encoder'
188239encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -232,6 +283,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
232283 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
233284 }
234285
286+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
287+ braid = isoEncoder (iso swap swap)
288+
289+
235290instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
236291 first f = bimap f id
237292instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -245,6 +300,34 @@ instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (Enco
245300 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
246301 }
247302
303+instance Monad parse => PFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
304+ first f = bimap f id
305+instance Monad parse => QFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
306+ second g = bimap id g
307+instance Monad parse => Bifunctor Either (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
308+ bimap f g = EncoderImpl
309+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
310+ , _encoderImpl_decode = \case
311+ Left a -> Left <$> _encoderImpl_decode f a
312+ Right b -> Right <$> _encoderImpl_decode g b
313+ }
314+
315+instance (Monad parse, Applicative check) => QFunctor Either (Encoder check parse) (Encoder check parse) where
316+ second g = bimap id g
317+instance (Monad parse, Applicative check) => PFunctor Either (Encoder check parse) (Encoder check parse) where
318+ first f = bimap f id
319+instance (Monad parse, Applicative check) => Bifunctor Either (Encoder check parse) (Encoder check parse) (Encoder check parse) where
320+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
321+
322+instance (Applicative check, Monad parse) => Associative (Encoder check parse) Either where
323+ associate = isoEncoder (iso (associate @(->) @Either) disassociate)
324+ disassociate = isoEncoder (iso disassociate associate)
325+
326+instance (Monad parse, Applicative check) => Braided (Encoder check parse) Either where
327+ braid = isoEncoder (iso swap swap)
328+
329+
330+
248331instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
249332 fmap e = Encoder $ do
250333 ve <- unEncoder e
@@ -332,14 +415,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
332415justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
333416justEncoder = prismEncoder _Just
334417
335−-- |
418+-- | Encode () to 'Nothing'.
336419nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
337420nothingEncoder = prismEncoder _Nothing
338421
339422someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
340423someConstEncoder = unsafeMkEncoder $ EncoderImpl
341− { _encoderImpl_encode = \(Some.This (Const a)) -> a
342− , _encoderImpl_decode = pure . Some.This . Const
424+ { _encoderImpl_encode = \(Some (Const a)) -> a
425+ , _encoderImpl_decode = pure . Some . Const
343426 }
344427
345428-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -367,7 +450,7 @@ checkEnum1EncoderFunc
367450 -> check (EncoderFunc check' parse p r)
368451checkEnum1EncoderFunc f = do
369452 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
370− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
453+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
371454 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
372455 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
373456
@@ -422,11 +505,12 @@ chainEncoder cons this rest = Encoder $ do
422505 pure $ EncoderImpl
423506 { _encoderImpl_decode = \v -> do
424507 (here, following) <- _encoderImpl_decode consValid v
425− Some.This r <- _encoderImpl_decode thisValid here
426− (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
508+ _encoderImpl_decode thisValid here >>= \case
509+ Some r ->
510+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
427511 , _encoderImpl_encode = \(r :/ s) ->
428512 _encoderImpl_encode consValid
429− ( _encoderImpl_encode thisValid $ Some.This r
513+ ( _encoderImpl_encode thisValid $ Some r
430514 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
431515 }
432516
@@ -493,7 +577,7 @@ enum1Encoder
493577 , Show r
494578 )
495579 => (forall a. p a -> r) -> Encoder check parse (Some p) r
496−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
580+enum1Encoder f = enumEncoder $ \(Some p) -> f p
497581
498582-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
499583-- have a large number of values - it will use a lot of memory.
@@ -536,13 +620,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
536620 }
537621
538622pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
539−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
540− { _encoderImpl_decode = \(path, query) ->
541− if query == mempty
542− then pure path
543− else throwError "pathOnlyEncoderImpl: query was provided"
544− , _encoderImpl_encode = \path -> (path, mempty)
545− }
623+pathOnlyEncoder = second (unitEncoder mempty) . coidr
624+
625+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
626+queryOnlyEncoder = first (unitEncoder []) . coidl
546627
547628singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
548629singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -585,10 +666,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
585666 in (urlDecodeText True k, urlDecodeText True <$> mv)
586667
587668urlEncodeText :: Bool -> Text -> Text
588−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
669+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
589670
590671urlDecodeText :: Bool -> Text -> Text
591−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
672+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
592673
593674listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
594675listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -646,10 +727,47 @@ joinPairTextEncoder = Encoder . \case
646727 _ -> return (kt, T.drop (T.length separator) vt)
647728 }
648729
649−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
650−-- Or, more likely, the user can compose it themselves
651−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
652−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
730+-- This slight generalization of 'rPrism' happens to be enough to write all our
731+-- prism combinators so far.
732+dSumPrism
733+ :: forall f f' g
734+ . (forall a. Prism' (f a) (f' a))
735+ -> Prism' (DSum f g) (DSum f' g)
736+dSumPrism p = prism'
737+ (\(f' :=> x) -> f' ^. re p :=> x)
738+ (\(f :=> x) -> (:=> x) <$> (f ^? p))
739+
740+-- already in obelisk
741+rPrism
742+ :: forall f f'
743+ . (forall a. Prism' (f a) (f' a))
744+ -> Prism' (R f) (R f')
745+rPrism p = dSumPrism p
746+
747+dSumPrism'
748+ :: forall f g a
749+ . (forall b. Prism' (f b) (a :~: b))
750+ -> Prism' (DSum f g) (g a)
751+dSumPrism' p = dSumPrism p . iso (\(Refl :=> b) -> b) (Refl :=>)
752+
753+dSumGEqPrism
754+ :: GEq f
755+ => f a
756+ -> Prism' (DSum f g) (g a)
757+dSumGEqPrism variant = dSumPrism' $ prism' (\Refl -> variant) (\x -> geq variant x)
758+
759+-- | Given a 'tag :: f a', make a prism for 'R f'. This generalizes the usual
760+-- prisms for a sum type (the ones that 'mkPrisms' would make), just as 'R'
761+-- generalized a usual sum type.
762+--
763+-- [This is given the '_R' name of the "cannonical" prism not because it is the
764+-- most general, but because it seems the most useful for routes, and 'R' itself
765+-- trades generality for route-specificity.]
766+_R
767+ :: GEq f
768+ => f a
769+ -> Prism' (R f) a
770+_R variant = dSumGEqPrism variant . iso runIdentity Identity
653771
654772-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
655773-- if the prism doesn't match.
@@ -693,6 +811,45 @@ handleEncoder recover e = Encoder $ do
693811-- Actual obelisk route info
694812--------------------------------------------------------------------------------
695813
814+-- | The typical full route type comprising all of an Obelisk application's routes.
815+-- Parameterised by the top level GADTs that define backend and frontend routes, respectively.
816+data FullRoute :: (* -> *) -> (* -> *) -> * -> * where
817+ FullRoute_Backend :: br a -> FullRoute br fr a
818+ FullRoute_Frontend :: ObeliskRoute fr a -> FullRoute br fr a
819+
820+instance (GShow br, GShow fr) => GShow (FullRoute br fr) where
821+ gshowsPrec p = \case
822+ FullRoute_Backend x -> showParen (p > 10) (showString "FullRoute_Backend " . gshowsPrec 11 x)
823+ FullRoute_Frontend x -> showParen (p > 10) (showString "FullRoute_Frontend " . gshowsPrec 11 x)
824+
825+instance (GEq br, GEq fr) => GEq (FullRoute br fr) where
826+ geq (FullRoute_Backend x) (FullRoute_Backend y) = geq x y
827+ geq (FullRoute_Frontend x) (FullRoute_Frontend y) = geq x y
828+ geq _ _ = Nothing
829+
830+instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
831+ gcompare (FullRoute_Backend _) (FullRoute_Frontend _) = GLT
832+ gcompare (FullRoute_Frontend _) (FullRoute_Backend _) = GGT
833+ gcompare (FullRoute_Backend x) (FullRoute_Backend y) = gcompare x y
834+ gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
835+
836+instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
837+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
838+ ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
839+
840+-- | Build the typical top level application route encoder from a route for handling 404's,
841+-- and segment encoders for backend and frontend routes.
842+mkFullRouteEncoder
843+ :: (GCompare br, GCompare fr, GShow br, GShow fr, UniverseSome br, UniverseSome fr)
844+ => R (FullRoute br fr) -- ^ 404 handler
845+ -> (forall a. br a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single backend route segment
846+ -> (forall a. fr a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single frontend route segment
847+ -> Encoder (Either Text) Identity (R (FullRoute br fr)) PageName
848+mkFullRouteEncoder missing backendSegment frontendSegment = handleEncoder (const missing) $
849+ pathComponentEncoder $ \case
850+ FullRoute_Backend backendRoute -> backendSegment backendRoute
851+ FullRoute_Frontend obeliskRoute -> obeliskRouteSegment obeliskRoute frontendSegment
852+
696853-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
697854-- frontend.
698855data ObeliskRoute :: (* -> *) -> * -> * where
@@ -700,9 +857,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
700857 ObeliskRoute_App :: f a -> ObeliskRoute f a
701858 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
702859
703−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
704− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
705− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
860+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
861+ universeSome = concat
862+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
863+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
864+ ]
706865
707866instance GEq f => GEq (ObeliskRoute f) where
708867 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -790,12 +949,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
790949
791950someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
792951someSumEncoder = Encoder $ pure $ EncoderImpl
793− { _encoderImpl_encode = \(Some.This t) -> case t of
794− InL l -> Left $ Some.This l
795− InR r -> Right $ Some.This r
952+ { _encoderImpl_encode = \(Some t) -> case t of
953+ InL l -> Left $ Some l
954+ InR r -> Right $ Some r
796955 , _encoderImpl_decode = pure . \case
797− Left (Some.This l) -> Some.This (InL l)
798− Right (Some.This r) -> Some.This (InR r)
956+ Left (Some l) -> Some (InL l)
957+ Right (Some r) -> Some (InR r)
799958 }
800959
801960data Void1 :: * -> * where {}
@@ -806,7 +965,7 @@ instance Universe (Some Void1) where
806965void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
807966void1Encoder = Encoder $ pure $ EncoderImpl
808967 { _encoderImpl_encode = \case
809− Some.This f -> case f of {}
968+ Some f -> case f of {}
810969 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
811970 }
812971
@@ -820,8 +979,150 @@ concat <$> mapM deriveRouteComponent
820979 ]
821980
822981makePrisms ''ObeliskRoute
823−
982+makePrisms ''FullRoute
824983deriveGEq ''Void1
825984deriveGCompare ''Void1
826985
986+-- | Given a backend route and a checked route encoder, render the route (path
987+-- and query string). See 'checkEncoder' for how to produce a checked encoder.
988+renderBackendRoute
989+ :: forall br a.
990+ Encoder Identity Identity (R (FullRoute br a)) PageName
991+ -> R br
992+ -> Text
993+renderBackendRoute enc = renderObeliskRoute enc . hoistR FullRoute_Backend
994+
995+-- | Renders a frontend route with the supplied checked encoder
996+renderFrontendRoute
997+ :: forall a fr.
998+ Encoder Identity Identity (R (FullRoute a fr)) PageName
999+ -> R fr
1000+ -> Text
1001+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (FullRoute_Frontend . ObeliskRoute_App)
1002+
1003+-- | Renders a route of the form typically found in an Obelisk project
1004+renderObeliskRoute
1005+ :: forall a b.
1006+ Encoder Identity Identity (R (FullRoute a b)) PageName
1007+ -> R (FullRoute a b)
1008+ -> Text
1009+renderObeliskRoute e r =
1010+ let enc :: Encoder Identity (Either Text) (R (FullRoute a b)) PathQuery
1011+ enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
1012+ in (T.pack . uncurry (<>)) $ encode enc r
1013+
1014+readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
1015+readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
1016+
1017+integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
1018+integralEncoder = prismEncoder (Numeric.Lens.integral)
1019+
1020+pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
1021+ Encoder check parse (a, (as, b)) (as, b)
1022+pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
1023+
1024+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
1025+
1026+dmapEncoder :: forall check parse k k' v.
1027+ ( Monad check
1028+ , MonadError Text parse
1029+ , Universe (Some k')
1030+ , Ord k
1031+ , GCompare k'
1032+ , GShow k'
1033+ )
1034+ => Encoder check parse (Some k') k
1035+ -> (forall v'. k' v' -> Encoder check parse v' v)
1036+ -> Encoder check parse (DMap k' Identity) (Map k v)
1037+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
1038+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
1039+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
1040+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
1041+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
1042+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
1043+ return $ EncoderImpl
1044+ { _encoderImpl_encode = \dm -> Map.fromList $ do
1045+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
1046+ return ( encode keyEncoder (Some k')
1047+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
1048+ )
1049+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
1050+ tryDecode keyEncoder k >>= \case
1051+ Some (k' :: k' t) -> case DMap.lookup k' valueDecoders of
1052+ Nothing -> throwError . T.pack . keyError $ gshow k'
1053+ Just (Decoder e) -> do
1054+ v' <- tryDecode e v
1055+ return (k' :=> Identity v')
1056+ }
1057+
1058+fieldMapEncoder :: forall check parse r.
1059+ ( Applicative check
1060+ , MonadError Text parse
1061+ , HasFields r
1062+ , Universe (Some (Field r))
1063+ , GShow (Field r)
1064+ , GCompare (Field r)
1065+ )
1066+ => Encoder check parse r (DMap (Field r) Identity)
1067+fieldMapEncoder = unsafeEncoder $ do
1068+ pure $ EncoderImpl
1069+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
1070+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
1071+ case DMap.lookup f dm of
1072+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
1073+ Just (Identity v) -> return v
1074+ }
1075+
1076+-- this is in base 4.12 (GHC 8.6);
1077+newtype Ap f a = Ap {getAp :: f a}
1078+
1079+instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
1080+ Ap x <> Ap y = Ap (liftA2 (<>) x y)
1081+
1082+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
1083+ mappend = (<>)
1084+ mempty = Ap (pure mempty)
1085+
1086+pathFieldEncoder :: forall a p check parse . (HasFields a, Monad check, MonadError Text parse, GCompare (Field a)) => (forall x. Field a x -> Encoder check parse x p) -> Encoder check parse (a, [p]) [p]
1087+pathFieldEncoder fieldEncoder = unsafeEncoder $ do
1088+ fieldEncoderPureMap :: DMap.DMap (Field a) (Decoder Identity parse p) <- getAp $ getConst $ tabulateFieldsA @a $ \f -> Const (Ap $ fmap (DMap.singleton f . Decoder) $ (checkEncoder @Identity) $ fieldEncoder f)
1089+ let fieldEncoderPure :: forall x. Field a x -> Encoder Identity parse x p
1090+ fieldEncoderPure f = toEncoder (DMap.findWithDefault (error "bad") f fieldEncoderPureMap)
1091+ pure $ EncoderImpl
1092+ { _encoderImpl_encode = \(x, rest) -> execWriter $ do
1093+ _ <- traverseWithField (\f x_i -> tell (pure $ encode (fieldEncoderPure f) x_i) *> pure x_i) x
1094+ tell rest
1095+ , _encoderImpl_decode = State.runStateT $ tabulateFieldsA $ \f -> State.get >>= \case
1096+ [] -> throwError $ T.pack "not enough path components"
1097+ p:ps -> do
1098+ State.put ps
1099+ lift $ tryDecode (fieldEncoderPure f) p
1100+ }
1101+
1102+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
1103+jsonEncoder :: forall check parse r.
1104+ ( ToJSON r
1105+ , FromJSON r
1106+ , Applicative check
1107+ , MonadError Text parse
1108+ )
1109+ => Encoder check parse r Text
1110+jsonEncoder = unsafeEncoder $ do
1111+ pure $ EncoderImpl
1112+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
1113+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
1114+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
1115+ Right x -> return x
1116+ }
1117+
1118+-- Useful for app server integration.
1119+-- p must not start with slashes
1120+byteStringsToPageName :: BS.ByteString -> BS.ByteString -> PageName
1121+byteStringsToPageName p q =
1122+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
1123+ pageNameEncoder' = bimap
1124+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
1125+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
1126+ in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
1127+
8271128--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+127−43
@@ -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 #-}
@@ -14,6 +15,7 @@
1415{-# LANGUAGE RankNTypes #-}
1516{-# LANGUAGE ScopedTypeVariables #-}
1617{-# LANGUAGE TemplateHaskell #-}
18+{-# LANGUAGE TupleSections #-}
1719{-# LANGUAGE TypeFamilies #-}
1820{-# LANGUAGE TypeOperators #-}
1921{-# LANGUAGE ViewPatterns #-}
@@ -36,6 +38,9 @@ module Obelisk.Route.Frontend
3638 , maybeRoute
3739 , maybeRoute_
3840 , maybeRouted
41+ , eitherRoute
42+ , eitherRoute_
43+ , eitherRouted
3944 , runRouteViewT
4045 , SetRouteT(..)
4146 , SetRoute(..)
@@ -46,6 +51,9 @@ module Obelisk.Route.Frontend
4651 , runRouteToUrlT
4752 , mapRouteToUrlT
4853 , routeLink
54+ , dynRouteLink
55+ , adaptedUriPath
56+ , setAdaptedUriPath
4957 ) where
5058
5159import Prelude hiding ((.), id)
@@ -55,14 +63,13 @@ import Obelisk.Route
5563import Control.Category (Category (..), (.))
5664import Control.Category.Cartesian
5765import Control.Lens hiding (Bifunctor, bimap, universe, element)
66+import Control.Monad ((<=<))
5867import Control.Monad.Fix
5968import Control.Monad.Primitive
69+import Control.Monad.Reader
6070import Control.Monad.Ref
61−import Control.Monad.Trans
6271import Control.Monad.Trans.Control
63−import Control.Monad.Trans.Reader
6472import Data.Coerce
65−import Data.Constraint (Dict (..))
6673import Data.Dependent.Sum (DSum (..))
6774import Data.GADT.Compare
6875import Data.Monoid
@@ -84,32 +91,43 @@ import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can
8491import Reflex.Dom.Core
8592import qualified GHCJS.DOM.Types as DOM
8693import Network.URI
87−#if defined(ios_HOST_OS)
8894import Data.Maybe (fromMaybe)
8995import qualified Data.List as L
90−#endif
9196
9297import Unsafe.Coerce
9398
99+import Obelisk.Configs
100+
94101infixr 5 :~
95102pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
96103pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
97104
98105class Routed t r m | m -> t r where
99106 askRoute :: m (Dynamic t r)
107+ default askRoute :: (Monad m', MonadTrans f, Routed t r m', m ~ f m') => m (Dynamic t r)
108+ askRoute = lift askRoute
100109
101110instance Monad m => Routed t r (RoutedT t r m) where
102111 askRoute = RoutedT ask
103112
113+instance (Monad m, Routed t r m) => Routed t r (ReaderT r' m)
114+
104115newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
105− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
116+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
117+
118+instance MonadReader r' m => MonadReader r' (RoutedT t r m) where
119+ ask = lift ask
120+ local = mapRoutedT . local
106121
107122instance HasJSContext m => HasJSContext (RoutedT t r m) where
108123 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
109124 askJSContext = lift askJSContext
110125
111−instance Prerender js m => Prerender js (RoutedT t r m) where
112− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
126+instance (Prerender js t m, Monad m) => Prerender js t (RoutedT t r m) where
127+ type Client (RoutedT t r m) = RoutedT t r (Client m)
128+ prerender server client = RoutedT $ do
129+ r <- ask
130+ lift $ prerender (runRoutedT server r) (runRoutedT client r)
113131
114132instance Requester t m => Requester t (RoutedT t r m) where
115133 type Request (RoutedT t r m) = Request m
@@ -161,17 +179,20 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
161179 askQueryResult = lift askQueryResult
162180 queryIncremental = lift . queryIncremental
163181
164−instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m) where
165− askRouteToUrl = lift askRouteToUrl
182+instance HasConfigs m => HasConfigs (RoutedT t r m)
166183
167−instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m) where
168− setRoute = lift . setRoute
169− modifyRoute = lift . modifyRoute
184+instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
185+
186+instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
187+
188+instance (Monad m, RouteToUrl r m) => RouteToUrl r (EventWriterT t w m)
189+
190+instance (Monad m, SetRoute t r m) => SetRoute t r (EventWriterT t w m)
170191
171192runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
172193runRoutedT = runReaderT . unRoutedT
173194
174−mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
195+mapRoutedT :: (m a -> n b) -> RoutedT t r m a -> RoutedT t r n b
175196mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
176197
177198withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
@@ -201,6 +222,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
201222 runRoutedT (f c) r'
202223-}
203224
225+eitherRoute_
226+ :: (MonadFix m, MonadHold t m, Adjustable t m)
227+ => RoutedT t l m ()
228+ -> RoutedT t r m ()
229+ -> RoutedT t (Either l r) m ()
230+eitherRoute_ l r = eitherRouted $ strictDynWidget_ $ either (runRoutedT l) (runRoutedT r)
231+
232+eitherRoute
233+ :: (MonadFix m, MonadHold t m, Adjustable t m)
234+ => RoutedT t l m a
235+ -> RoutedT t r m a
236+ -> RoutedT t (Either l r) m (Dynamic t a)
237+eitherRoute l r = eitherRouted $ strictDynWidget $ either (runRoutedT l) (runRoutedT r)
238+
204239dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
205240dsumValueCoercion Coercion = Coercion
206241
@@ -217,6 +252,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
217252 d' <- maybeDyn d
218253 runRoutedT r d'
219254
255+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
256+eitherRouted r = RoutedT $ ReaderT $ runRoutedT r <=< eitherDyn
257+
220258-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
221259strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
222260strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -231,7 +269,7 @@ strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
231269 pure ()
232270
233271newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
234− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument)
272+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, MonadIO, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
235273
236274instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
237275 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
@@ -253,16 +291,26 @@ runSetRouteT = runEventWriterT . unSetRouteT
253291class Reflex t => SetRoute t r m | m -> t r where
254292 setRoute :: Event t r -> m ()
255293 modifyRoute :: Event t (r -> r) -> m ()
294+ default modifyRoute :: (Monad m', MonadTrans f, SetRoute t r m', m ~ f m') => Event t (r -> r) -> m ()
295+ modifyRoute = lift . modifyRoute
296+
256297 setRoute = modifyRoute . fmap const
257298
258299instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
259300 modifyRoute = SetRouteT . tellEvent . fmap Endo
260301
261−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
262− modifyRoute = lift . modifyRoute
302+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
263303
264−instance Prerender js m => Prerender js (SetRouteT t r m) where
265− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
304+instance (Monad m, SetRoute t r m) => SetRoute t r (ReaderT r' m)
305+
306+instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
307+ type Client (SetRouteT t r m) = SetRouteT t r (Client m)
308+ prerender server client = do
309+ d <- lift $ prerender (runSetRouteT server) (runSetRouteT client)
310+ let (a, r) = splitDynPure d
311+ -- Must be prompt here
312+ SetRouteT . tellEvent $ switchPromptlyDyn r
313+ pure a
266314
267315instance Requester t m => Requester t (SetRouteT t r m) where
268316 type Request (SetRouteT t r m) = Request m
@@ -270,6 +318,8 @@ instance Requester t m => Requester t (SetRouteT t r m) where
270318 requesting = SetRouteT . requesting
271319 requesting_ = SetRouteT . requesting_
272320
321+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
322+
273323#ifndef ghcjs_HOST_OS
274324deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
275325#endif
@@ -293,6 +343,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
293343 type PrimState (SetRouteT t r m) = PrimState m
294344 primitive = lift . primitive
295345
346+instance HasConfigs m => HasConfigs (SetRouteT t r m)
347+
296348instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
297349 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
298350 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -306,9 +358,11 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
306358
307359class RouteToUrl r m | m -> r where
308360 askRouteToUrl :: m (r -> Text)
361+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
362+ askRouteToUrl = lift askRouteToUrl
309363
310364newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
311− deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument)
365+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
312366
313367runRouteToUrlT
314368 :: RouteToUrlT r m a
@@ -323,17 +377,22 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
323377 askRouteToUrl = RouteToUrlT ask
324378
325379instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
326− askRouteToUrl = lift askRouteToUrl
327380
328381instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
329− askRouteToUrl = lift askRouteToUrl
382+
383+instance (Monad m, RouteToUrl r m) => RouteToUrl r (ReaderT r' m) where
384+
385+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m)
330386
331387instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
332388 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
333389 askJSContext = lift askJSContext
334390
335−instance Prerender js m => Prerender js (RouteToUrlT r m) where
336− prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
391+instance (Prerender js t m, Monad m) => Prerender js t (RouteToUrlT r m) where
392+ type Client (RouteToUrlT r m) = RouteToUrlT r (Client m)
393+ prerender server client = do
394+ r <- RouteToUrlT ask
395+ lift $ prerender (runRouteToUrlT server r) (runRouteToUrlT client r)
337396
338397instance Requester t m => Requester t (RouteToUrlT r m) where
339398 type Request (RouteToUrlT r m) = Request m
@@ -383,6 +442,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
383442 askQueryResult = lift askQueryResult
384443 queryIncremental = lift . queryIncremental
385444
445+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
446+
386447runRouteViewT
387448 :: forall t m r a.
388449 ( TriggerEvent t m
@@ -393,16 +454,20 @@ runRouteViewT
393454 , MonadFix m
394455 )
395456 => (Encoder Identity Identity r PageName)
457+ --TODO: Get rid of the switchover and useHash arguments
458+ -- useHash can probably be baked into the encoder
459+ -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
460+ -> Bool
396461 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
397462 -> m a
398−runRouteViewT routeEncoder a = do
399− rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
463+runRouteViewT routeEncoder switchover useHash a = do
464+ rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
400465 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
401466 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
402467 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
403468 -- this ever to happen, we'll just handle it by failing completely with 'error'.
404469 route :: Dynamic t r
405− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
470+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
406471 where
407472 errorLeft (Left e) = error (T.unpack e)
408473 errorLeft (Right x) = x
@@ -423,7 +488,7 @@ runRouteViewT routeEncoder a = do
423488 -- we can change this function later to accommodate.
424489 -- See: https://github.com/whatwg/html/issues/2174
425490 , _historyStateUpdate_title = ""
426− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
491+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
427492 { uriQuery = newQuery
428493 }
429494 }
@@ -450,25 +515,44 @@ routeLink r w = do
450515 setRoute $ r <$ domEvent Click e
451516 return a
452517
518+-- | A link widget that, when clicked, sets the route to current value of the
519+-- provided dynamic route. In non-javascript contexts the value of the dynamic post
520+-- build is used so the link still works like 'routeLink'.
521+dynRouteLink
522+ :: forall t m a route.
523+ ( DomBuilder t m
524+ , PostBuild t m
525+ , RouteToUrl (R route) m
526+ , SetRoute t (R route) m
527+ )
528+ => Dynamic t (R route) -- ^ Target route
529+ -> m a -- ^ Child widget
530+ -> m a
531+dynRouteLink dr w = do
532+ enc <- askRouteToUrl
533+ er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
534+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
535+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
536+ & elementConfig_modifyAttributes .~ er
537+ (e, a) <- element "a" cfg w
538+ setRoute $ tag (current dr) $ domEvent Click e
539+ return a
540+
453541-- On ios due to sandboxing when loading the page from a file adapt the
454542-- path to be based on the hash.
455543
456−adaptedUriPath :: URI -> String
457−#if defined(ios_HOST_OS)
458−adaptedUriPath = hashToPath . uriFragment
544+adaptedUriPath :: Bool -> URI -> String
545+adaptedUriPath = \case
546+ True -> hashToPath . uriFragment
547+ False -> uriPath
459548
460−hashToPath :: String -> String
461−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
462−#else
463−adaptedUriPath = uriPath
464−#endif
465−
466−setAdaptedUriPath :: String -> URI -> URI
467−#if defined(ios_HOST_OS)
468−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
549+setAdaptedUriPath :: Bool -> String -> URI -> URI
550+setAdaptedUriPath useHash s u = case useHash of
551+ True -> u { uriFragment = pathToHash s }
552+ False -> u { uriPath = s }
469553
470554pathToHash :: String -> String
471555pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
472−#else
473−setAdaptedUriPath s u = u { uriPath = s }
474−#endif
556+
557+hashToPath :: String -> String
558+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
11module Obelisk.Route.TH (deriveRouteComponent) where
22
3+import Data.Constraint.Extras.TH
34import Data.GADT.Show.TH
45import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
67import Language.Haskell.TH
78
89-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
@@ -11,8 +12,6 @@ deriveRouteComponent x = concat <$> traverse ($ x)
1112 [ deriveGShow
1213 , deriveGEq
1314 , deriveGCompare
14− , deriveShowTagIdentity
15− , deriveEqTagIdentity
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
15+ , deriveUniverseSome
16+ , deriveArgDict
1817 ]
lib/run/obelisk-run.cabal+3−2
@@ -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,8 +26,7 @@ library
2426 , network
2527 , obelisk-asset-serve-snap
2628 , obelisk-backend
27− , obelisk-executable-config
28− , obelisk-executable-config-inject
29+ , obelisk-executable-config-lookup
2930 , obelisk-frontend
3031 , obelisk-route
3132 , process
lib/run/src/Obelisk/Run.hs+56−43
@@ -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)
@@ -25,17 +20,20 @@ import Control.Exception
2520import Control.Lens ((%~), (^?), _Just, _Right)
2621import qualified Data.Attoparsec.ByteString.Char8 as A
2722import Data.ByteString (ByteString)
23+import qualified Data.ByteString as BS
2824import qualified Data.ByteString.Char8 as BSC
2925import qualified Data.ByteString.Lazy.Char8 as BSLC
30−import Data.Dependent.Sum (DSum (..))
3126import Data.Functor.Identity
32−import Data.Functor.Sum
3327import Data.List (uncons)
28+import Data.Map (Map)
29+import qualified Data.Map as Map
3430import Data.Maybe
3531import Data.Semigroup ((<>))
3632import Data.Streaming.Network (bindPortTCP)
33+import Data.String (fromString)
3734import Data.Text (Text)
3835import qualified Data.Text as T
36+import qualified Data.Text.Encoding as T
3937import Language.Javascript.JSaddle.Run (syncPoint)
4038import Language.Javascript.JSaddle.WebSockets
4139import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -49,8 +47,7 @@ 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)
53−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
50+import Obelisk.Backend
5451import Obelisk.Frontend
5552import Obelisk.Route.Frontend
5653import Reflex.Dom.Core
@@ -62,7 +59,7 @@ import System.Exit (ExitCode(..))
6259import Text.URI (URI)
6360import qualified Text.URI as URI
6461import Text.URI.Lens
65−import Obelisk.Backend
62+import Web.Cookie
6663
6764run
6865 :: Int -- ^ Port to run the backend
@@ -77,36 +74,39 @@ run port serveStaticAsset backend frontend = do
7774 case checkEncoder $ _backend_routeEncoder backend of
7875 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7976 Right validFullEncoder -> do
77+ publicConfigs <- getPublicConfigs
8078 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
8179 _backend_run backend $ \serveRoute -> do
8280 runSnapWithCommandLineArgs $ do
8381 getRouteWith validFullEncoder >>= \case
8482 Identity r -> case r of
85− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
86− InR obeliskRoute :=> Identity a ->
87− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
83+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
84+ FullRoute_Frontend obeliskRoute :/ a ->
85+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
8886 let conf = defRunConfig { _runConfig_redirectPort = port }
89− runWidget conf frontend validFullEncoder `finally` killThread backendTid
87+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
9088
9189-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
9290runServeAsset :: FilePath -> [Text] -> Snap ()
9391runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
9492
95−getConfigRoute :: IO (Maybe URI)
96−getConfigRoute = get "config/common/route" >>= \case
97− Just r -> case URI.mkURI $ T.strip r of
98− Just route -> pure $ Just route
99− Nothing -> do
100− putStrLn $ "Route is invalid: " <> show r
101− pure Nothing
102− Nothing -> pure Nothing
103−
104−defAppUri :: URI
105−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
106−
107−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
108−runWidget conf frontend validFullEncoder = do
109− uri <- fromMaybe defAppUri <$> getConfigRoute
93+getConfigRoute :: Map Text ByteString -> Either Text URI
94+getConfigRoute configs = case Map.lookup "common/route" configs of
95+ Just r ->
96+ let stripped = T.strip (T.decodeUtf8 r)
97+ in case URI.mkURI stripped of
98+ Just route -> Right route
99+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
100+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
101+
102+runWidget
103+ :: RunConfig
104+ -> Map Text ByteString
105+ -> Frontend (R frontendRoute)
106+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
107+ -> IO ()
108+runWidget conf configs frontend validFullEncoder = do
109+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
110110 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
111111 redirectHost = _runConfig_redirectHost conf
112112 redirectPort = _runConfig_redirectPort conf
@@ -118,41 +118,54 @@ runWidget conf frontend validFullEncoder = do
118118 close
119119 (\skt -> do
120120 man <- newManager defaultManagerSettings
121− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
121+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122122 runSettingsSocket settings skt app)
123123
124124obeliskApp
125− :: forall route backendRoute. ConnectionOptions
126− -> Frontend (R route)
127− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
125+ :: forall frontendRoute backendRoute
126+ . Map Text ByteString
127+ -> ConnectionOptions
128+ -> Frontend (R frontendRoute)
129+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
128130 -> URI
129131 -> Application
130132 -> IO Application
131−obeliskApp opts frontend validFullEncoder uri backend = do
132− let entryPoint = do
133− runFrontend validFullEncoder frontend
133+obeliskApp configs opts frontend validFullEncoder uri backend = do
134+ let mode = FrontendMode
135+ { _frontendMode_hydrate = True
136+ , _frontendMode_adjustRoute = False
137+ }
138+ entryPoint = do
139+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
134140 syncPoint
135141 jsaddlePath <- URI.mkPathPiece "jsaddle"
136142 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
137143 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
138144 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
139− return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
145+ return $ \req sendResponse -> case tryDecode validFullEncoder $ byteStringsToPageName (BS.dropWhile (== (fromIntegral $ fromEnum '/')) $ W.rawPathInfo req) (BS.drop 1 $ W.rawQueryString req) of
140146 Identity r -> case r of
141− InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
147+ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :/ jsaddleRoute -> case jsaddleRoute of
142148 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
143149 _ -> flip jsaddle sendResponse $ req
144150 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
145151 }
146− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
147− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
152+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
153+ let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
154+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
148155 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
149156 _ -> backend req sendResponse
150157
151−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
152−renderJsaddleFrontend urlEnc r f =
158+renderJsaddleFrontend
159+ :: Map Text ByteString
160+ -> Cookies
161+ -> (route -> Text)
162+ -> route
163+ -> Frontend route
164+ -> IO ByteString
165+renderJsaddleFrontend configs cookies urlEnc r f =
153166 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
154167 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
155− in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
168+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
156169
157170-- | like 'bindPortTCP' but reconnects on exception
158171bindPortTCPRetry :: 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+15−15
@@ -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
@@ -74,6 +75,7 @@ main = do
7475 unless isVerbose $
7576 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
7677 let verbosity = bool silently verbosely isVerbose
78+ nixBuild args = run "nix-build" ("--no-out-link" : args)
7779 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
7880 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
7981 [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
@@ -126,8 +128,10 @@ main = do
126128 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127129 ls tmp >>= liftIO . assertEqual "" []
128130
129− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
130− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
131+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
132+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133+ configs <- getConfigs
134+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
131135
132136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
133137 -- when not run by 'nix-build --attr selftest'
@@ -138,33 +142,29 @@ main = do
138142 testObRunInDir p2 p3 (Just "frontend") httpManager
139143
140144 describe "obelisk project" $ parallel $ do
141− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
142− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
143− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
144− -- See https://github.com/obsidiansystems/obelisk/issues/101
145− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
145+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
146+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
147+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
148+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [obeliskImpl]
146149
147150 describe "blank initialized project" $ parallel $ do
148151
149152 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
150− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
153+ nixBuild ["-A", "ghc.backend"]
151154 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
152− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
155+ nixBuild ["-A", "ghcjs.frontend"]
153156
154157 if os == "darwin"
155− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
156− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
158+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
159+ else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
157160
158161 forM_ ["ghc", "ghcjs"] $ \compiler -> do
159162 let
160163 shell = "shells." <> compiler
161− inShell cmd' = run "nix-shell" ["-A", fromString shell, "--run", cmd']
164+ inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shell, "--run", cmd']
162165 it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
163166 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
164167
165− it "can build reflex project" $ inTmpObInit $ \_ -> do
166− run "nix-build" []
167−
168168 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
169169 u <- update
170170 uu <- update
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
release.nix+31−14
@@ -1,4 +1,8 @@
1−{ local-self ? import ./. {}
1+{ self-args ? {
2+ config.android_sdk.accept_license = true;
3+ iosSdkVersion = "10.2";
4+ }
5+, local-self ? import ./. self-args
26}:
37
48let
@@ -9,7 +13,7 @@ let
913 obeliskPackagesCommon = [
1014 "obelisk-frontend"
1115 "obelisk-route"
12− "obelisk-executable-config"
16+ "obelisk-executable-config-lookup"
1317 ];
1418
1519 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -44,7 +48,7 @@ let
4448 in pkgAttrs: builtins.concatLists (map extractDeps (builtins.attrValues pkgAttrs));
4549
4650 perPlatform = lib.genAttrs cacheBuildSystems (system: let
47− obelisk = import ./. { inherit system; };
51+ obelisk = import ./. (self-args // { inherit system; });
4852 reflex-platform = obelisk.reflex-platform;
4953 ghc = pnameToAttrs
5054 obelisk.haskellPackageSets.ghc
@@ -57,24 +61,37 @@ let
5761 (builtins.attrValues ghcjs)
5862 (concatDepends ghc)
5963 (concatDepends ghcjs)
64+ (lib.optional reflex-platform.androidSupport androidSkeleton)
65+ (lib.optional reflex-platform.iosSupport iosSkeleton)
66+ [ command serverSkeletonExe serverSkeletonShell ]
6067 ];
61− command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63− builtSkeleton = (import ./skeleton {}).all;
68+ command = obelisk.command;
69+ skeleton = import ./skeleton { inherit obelisk; };
70+ serverSkeletonExe = skeleton.exe;
71+ # TODO fix nixpkgs so it doesn't try to run the result of haskell shells as setup hooks.
72+ serverSkeletonShell = local-self.nixpkgs.runCommand "shell-safe-for-dep" {} ''
73+ touch "$out"
74+ echo "return" >> "$out"
75+ cat "${skeleton.shells.ghc}" >> "$out"
76+ '';
77+ androidSkeleton = (import ./skeleton { inherit obelisk; }).android.frontend;
78+ iosSkeleton = (import ./skeleton { inherit obelisk; }).ios.frontend;
6479 in {
6580 inherit
6681 command
6782 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
69− cache = reflex-platform.pinBuildInputs
70− "obelisk-${system}"
71− cachePackages
72− [command serverExeSkeleton builtSkeleton];
83+ serverSkeletonExe
84+ serverSkeletonShell
85+ ;
86+ cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
87+ } // lib.optionalAttrs reflex-platform.androidSupport {
88+ inherit androidSkeleton;
89+ } // lib.optionalAttrs reflex-platform.iosSupport {
90+ inherit iosSkeleton;
7391 });
7492
75− metaCache = local-self.pinBuildInputs
93+ metaCache = local-self.reflex-platform.pinBuildInputs
7694 "obelisk-everywhere"
77− (map (a: a.cache) (builtins.attrValues perPlatform))
78− [];
95+ (map (a: a.cache) (builtins.attrValues perPlatform));
7996
8097in perPlatform // { inherit metaCache; }
skeleton/backend/src/Backend.hs+1−3
@@ -1,6 +1,4 @@
1−{-# LANGUAGE EmptyCase #-}
21{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE LambdaCase #-}
42{-# LANGUAGE RankNTypes #-}
53{-# LANGUAGE TypeFamilies #-}
64module Backend where
@@ -11,5 +9,5 @@ import Obelisk.Backend
119backend :: Backend BackendRoute FrontendRoute
1210backend = Backend
1311 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
12+ , _backend_routeEncoder = fullRouteEncoder
1513 }
skeleton/common/common.cabal+3−1
@@ -9,7 +9,9 @@ library
99 , obelisk-route
1010 , mtl
1111 , text
12−
12+ default-extensions:
13+ TypeFamilies
14+ PolyKinds
1315 exposed-modules:
1416 Common.Api
1517 Common.Route
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: String
4−commonStuff = "Here is a string defined in code common to the frontend and backend."
4+commonStuff = "Here is a string defined in Common.Api"
skeleton/common/src/Common/Route.hs+12−18
@@ -1,15 +1,13 @@
11{-# LANGUAGE EmptyCase #-}
22{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE TemplateHaskell #-}
75{-# LANGUAGE KindSignatures #-}
8−{-# LANGUAGE EmptyCase #-}
6+{-# LANGUAGE LambdaCase #-}
97{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE FlexibleInstances #-}
118{-# LANGUAGE OverloadedStrings #-}
12−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
1311module Common.Route where
1412
1513{- -- You will probably want these imports for composing Encoders.
@@ -19,7 +17,6 @@ import Control.Category
1917
2018import Data.Text (Text)
2119import Data.Functor.Identity
22−import Data.Functor.Sum
2320
2421import Obelisk.Route
2522import Obelisk.Route.TH
@@ -34,17 +31,14 @@ data FrontendRoute :: * -> * where
3431 FrontendRoute_Main :: FrontendRoute ()
3532 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
3633
37−backendRouteEncoder
38− :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
39−backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
40− pathComponentEncoder $ \case
41− InL backendRoute -> case backendRoute of
42− BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
43− InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
44− -- The encoder given to PathEnd determines how to parse query parameters,
45− -- in this example, we have none, so we insist on it.
46− FrontendRoute_Main -> PathEnd $ unitEncoder mempty
47−
34+fullRouteEncoder
35+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
36+fullRouteEncoder = mkFullRouteEncoder
37+ (FullRoute_Backend BackendRoute_Missing :/ ())
38+ (\case
39+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
40+ (\case
41+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4842
4943concat <$> mapM deriveRouteComponent
5044 [ ''BackendRoute
skeleton/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
skeleton/default.nix+9−3
@@ -1,7 +1,13 @@
1−{ 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";
skeleton/frontend/frontend.cabal+1
@@ -10,6 +10,7 @@ library
1010 , obelisk-frontend
1111 , obelisk-route
1212 , reflex-dom
13+ , obelisk-executable-config-lookup
1314 , obelisk-generated-static
1415 , text
1516 exposed-modules:
skeleton/frontend/src-bin/main.hs+1−1
@@ -6,5 +6,5 @@ import Reflex.Dom
66
77main :: IO ()
88main = do
9− let Right validFullEncoder = checkEncoder backendRouteEncoder
9+ let Right validFullEncoder = checkEncoder fullRouteEncoder
1010 run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+8−1
@@ -4,7 +4,9 @@
44module Frontend where
55
66import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
78import Obelisk.Frontend
9+import Obelisk.Configs
810import Obelisk.Route
911import Reflex.Dom.Core
1012
@@ -12,7 +14,6 @@ import Common.Api
1214import Common.Route
1315import Obelisk.Generated.Static
1416
15−
1617frontend :: Frontend (R FrontendRoute)
1718frontend = Frontend
1819 { _frontend_head = el "title" $ text "Obelisk Minimal Example"
@@ -20,4 +21,10 @@ frontend = Frontend
2021 text "Welcome to Obelisk!"
2122 el "p" $ text $ T.pack commonStuff
2223 elAttr "img" ("src" =: static @"obelisk.jpg") blank
24+ el "div" $ do
25+ exampleConfig <- getConfig "common/example"
26+ case exampleConfig of
27+ Nothing -> text "No config file found in config/common/example"
28+ Just s -> text (T.decodeUtf8 s)
29+ return ()
2330 }
skeleton/shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./. {}).shells.ghc