on master help sign in
Merge remote-tracking branch 'origin/master' into upstream-optparse
commit2105803b51b394285fc6527e94076f1367f33b48 authorJohn Ericson <John.Ericson@Obsidian.Systems> dateMon 7 Oct 2019, 18:11 (7 years ago) parentb9af63fa98ad63
66 files changed+1998−635
.gitignore+1−3 CONTRIBUTING.md+73−3 ChangeLog.md+33 FAQ.md+122 README.md+54−31 default.nix+47−45 dep/reflex-platform/github.json+3−3 jobsets.nix+5 lib/asset/manifest/obelisk-asset-manifest.cabal+1 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2 lib/backend/obelisk-backend.cabal+3 lib/backend/src/Obelisk/Backend.hs+72−30 lib/cliapp/obelisk-cliapp.cabal+1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+26−2 lib/cliapp/src/Obelisk/CliApp/Process.hs+4−3 lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−23 lib/cliapp/src/Obelisk/CliApp/Theme.hs+29 lib/cliapp/src/Obelisk/CliApp/Types.hs+3 lib/command/obelisk-command.cabal+3 lib/command/src/Obelisk/Command.hs+37−19 lib/command/src/Obelisk/Command/Deploy.hs+146−17 lib/command/src/Obelisk/Command/Nix.hs+2 lib/command/src/Obelisk/Command/Project.hs+11−7 lib/command/src/Obelisk/Command/Run.hs+41−10 lib/command/src/Obelisk/Command/Thunk.hs+67−40 lib/command/src/Obelisk/Command/Utils.hs+15−6 lib/command/src/Obelisk/Command/VmBuilder.hs+2−1 lib/executable-config/default.nix+7−3 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+5−3 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+124 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+176−91 lib/frontend/src/Obelisk/Frontend/Cookie.hs+102 lib/route/obelisk-route.cabal+7−2 lib/route/src/Obelisk/Route.hs+162−32 lib/route/src/Obelisk/Route/Frontend.hs+129−38 lib/route/src/Obelisk/Route/TH.hs+4−5 lib/run/obelisk-run.cabal+3−2 lib/run/src/Obelisk/Run.hs+58−38 lib/selftest/obelisk-selftest.cabal+1 lib/selftest/src/Obelisk/SelfTest.hs+33−12 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+17−6 skeleton/common/common.cabal+3−1 skeleton/common/src/Common/Api.hs+1−1 skeleton/config/common/example+1 skeleton/default.nix+9−3 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+8−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+33
@@ -0,0 +1,33 @@
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+## v0.2.0.0 - 2019-8-17
8+
9+* Configs become ByteStrings.
10+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
11+ * 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`.
12+* The backend no longer runs in BackendConfigsT.
13+* Add tabulation package. See Data.Tabulation for details.
14+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
15+* Use IP address for nginx proxy pass instead of localhost
16+
17+## v0.1.1.0 - 2019-05-17
18+
19+* Fix crashes of Android apps on 32-bit ARM devices.
20+* 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`.
21+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
22+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
23+* Add the `ob shell` command to enter a nix shell for an obelisk project
24+* 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.
25+* 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`.
26+* 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`.
27+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
28+
29+
30+## v0.1.0.0 - 2019-03-29
31+
32+* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
33+* Add `HasCookies` and `CookiesT` to allow `ObeliskWidget`s to access cookies during static and "hydrated" DOM rendering.
FAQ.mdnew file+122
@@ -0,0 +1,122 @@
1+# Frequently Asked Questions
2+
3+1. [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)
7+
8+### How do I fix invalid entitlements?
9+
10+You probably did not set `ios.bundleIdentifier` correctly in `default.nix`.
11+When this happens you'll see an error something like this:
12+
13+```
14+2018-11-25 09:34:22.438 ios-deploy[58106:8521046] [ !! ] Error 0xe8008016: The executable was signed with invalid entitlements. AMDeviceSecureInstallApplication(0, device, url, options, install_callback, 0)
15+```
16+
17+Fixing the value of `ios.bundleIdentifier` should fix the error.
18+
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+54−31
@@ -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,12 +25,13 @@ 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 ```
25− 1. If you are using another operating system or linux distribution, ensure that these lines are present in `/etc/nix/nix.conf`:
28+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
29+ 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
2630 ```
2731 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
2832 trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=
2933 ```
30− * other Linux: enable sandboxing (see https://github.com/obsidiansystems/obelisk/issues/6)
34+ * other Linux: enable sandboxing (see these [issue172](https://github.com/obsidiansystems/obelisk/issues/172#issuecomment-411507818) or [issue6](https://github.com/obsidiansystems/obelisk/issues/6) if you run into build problems)
3135 ```
3236 sandbox = true
3337 ```
@@ -79,10 +83,44 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
7983ob run
8084```
8185
82−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
86+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
8387
8488Every 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.
8589
90+### Hoogle
91+
92+To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
93+
94+`nix-shell -A shells.ghc --arg withHoogle true`
95+
96+### Adding packages
97+
98+In order to add package dependencies, declare them under the build-depends field in the appropriate cabal files (backend, common, and frontend each have their own). The corresponding Nix packages will automatically be selected when building.
99+
100+### Adding package overrides
101+
102+To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
103+
104+```nix
105+# ...
106+project ./. ({ pkgs, ... }: {
107+# ...
108+ overrides = self: super: let
109+ aeson = pkgs.fetchFromGitHub {
110+ owner = "obsidiansystems";
111+ repo = "aeson-gadt-th";
112+ rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
113+ sha256 = "08q6rnz7w9pn76jkrafig6f50yd0f77z48rk2z5iyyl2jbhcbhx3";
114+ };
115+ in
116+ {
117+ aeson = self.callCabal2nix "aeson" aeson {};
118+ };
119+# ...
120+```
121+
122+For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
123+
86124## Deploying
87125
88126### Locally
@@ -106,7 +144,7 @@ In this section we will demonstrate how to deploy your Obelisk app to an Amazon
106144
107145First create a new EC2 instance:
108146
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))
147+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))
1101481. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1111491. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1121501. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
@@ -237,7 +275,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237275
238276### Android
239277
278+NOTE: Currently Android builds are only supported on Linux.
279+
2402801. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
281+1. In your project's `default.nix` pass `config.android_sdk.accept_license = true;` in the arguments to the import of of `.obelisk/impl` to indicate your acceptance of the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms), which is required to build Android apps.
2412821. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2422831. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243284
@@ -247,38 +288,20 @@ Now deploy the built apk file to your Android device:
2472881. Connect the device using USB (be sure to confirm any security prompts on the device)
2482891. Run the deploy script: `result-android/bin/deploy`
249290
250−This should copy over and install the application on your device (if you see a "*signatures do not match*" error, simply uninstall the previous app from the device before retrying the deploy). The name of the installed application will be what you have specified for `android.displayName` in the `default.nix`.
251−
252−#### Releasing to Play Store
253−
254−##### Configure signing
291+Alternatively, you can deploy from an obelisk deployment directory (a directory generated post `ob deploy init ...` command) using the `ob deploy test android` command.
292+This command will accomplish the following:
255293
256−The previous section would have generated a debug version of the app. In order to build a release version you will need to sign your app. Obelisk can automatically sign the app during build if you provide it with your keystore file in `default.nix`.
294+1. Create a key store and apk signing key (`android_keystore.jks`)
295+1. Build a Signed Android apk for your application
296+1. Deploy the Signed apk to your connected Android device
257297
258−First, if you do not already have a keystore, create it as follows (for more information, see the [Android documentation](https://developer.android.com/studio/publish/app-signing#signing-manually)):
259−
260−```
261−nix-shell -p androidenv.platformTools --run "keytool -genkey -v -keystore myandroidkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias myandroidalias"
262−```
298+In the event that you change your key or keystore password, you will have to update your credentials within the JSON object found in `android_keytool_config.json`
263299
264−(Be sure to give an appropriate keystore filename and key alias string above.)
300+Additional documentation on java key stores can be found [here] (https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html)
265301
266−The `keytool` command will ask you for some details, including a keystore password and a key password (we will use these passwords further below). It will now have created a `myandroidkey.jks` file under the current directory. Move that to somewhere safe, and note down its full path.
267−
268−Now edit your project's `default.nix` and tell Obelisk of your app's keystore file. Your `default.nix` should look like this after the edit:
302+This should copy over and install the application on your device (if you see a "*signatures do not match*" error, simply uninstall the previous app from the device before retrying the deploy). The name of the installed application will be what you have specified for `android.displayName` in the `default.nix`.
269303
270−```nix
271− ...
272− android.applicationId = "com.example.myapp";
273− android.displayName = "My App";
274− android.releaseKey =
275− { storeFile = /path/to/myandroidkey.jks;
276− storePassword = "abcd1234";
277− keyAlias = "myandroidalias";
278− keyPassword = "abcd1234";
279− };
280− ...
281−```
304+#### Releasing to Play Store
282305
283306##### Build a release version
284307
default.nix+47−45
@@ -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 !(
@@ -12,14 +12,12 @@ let
1212 commandRuntimeDeps = pkgs: with pkgs; [
1313 coreutils
1414 git
15− gitAndTools.hub
1615 nix-prefetch-git
1716 openssh
1817 ];
1918
20− getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
21− getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
22− inherit iosSdkVersion __useLegacyCompilers;
19+ getReflexPlatform = sys: import ./dep/reflex-platform {
20+ inherit iosSdkVersion config;
2321 system = sys;
2422 enableLibraryProfiling = profiling;
2523
@@ -35,21 +33,20 @@ let
3533 (self: super: let
3634 pkgs = self.callPackage ({ pkgs }: pkgs) {};
3735 in {
38− # Need 8.0.2 build support
39− # PR: https://github.com/dmwit/universe/pull/33
40− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
41− owner = "obsidiansystems";
42− repo = "universe";
43− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
44− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
45− } + /template) {};
36+ hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
37+ owner = "haskell-nix";
38+ repo = "hnix";
39+ rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
40+ sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
41+ }) {});
4642 })
4743
44+ pkgs.obeliskExecutableConfig.haskellOverlay
45+
4846 # Add obelisk packages
4947 (self: super: let
5048 pkgs = self.callPackage ({ pkgs }: pkgs) {};
5149 in {
52− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
5350 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5451
5552 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
@@ -62,6 +59,7 @@ let
6259 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
6360 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
6461 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
62+ tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
6563 })
6664
6765 (self: super: let
@@ -87,7 +85,7 @@ let
8785 ];
8886 };
8987
90− reflex-platform = getReflexPlatform' false system;
88+ reflex-platform = getReflexPlatform system;
9189 inherit (reflex-platform) hackGet nixpkgs;
9290 pkgs = nixpkgs;
9391
@@ -180,7 +178,8 @@ in rec {
180178 enableACME = enableHttps;
181179 forceSSL = enableHttps;
182180 locations.${baseUrl} = {
183− proxyPass = "http://localhost:" + toString internalPort;
181+ proxyPass = "http://127.0.0.1:" + toString internalPort;
182+ proxyWebsockets = true;
184183 };
185184 };
186185 };
@@ -188,10 +187,11 @@ in rec {
188187 wantedBy = [ "multi-user.target" ];
189188 after = [ "network.target" ];
190189 restartIfChanged = true;
190+ path = [ pkgs.gnutar ];
191191 script = ''
192192 ln -sft . '${exe}'/*
193193 mkdir -p log
194− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
194+ exec ./backend ${backendArgs} </dev/null
195195 '';
196196 serviceConfig = {
197197 User = user;
@@ -214,18 +214,17 @@ in rec {
214214 };
215215 };
216216
217− serverExe = backend: frontend: assets: config: optimizationLevel: version:
217+ serverExe = backend: frontend: assets: optimizationLevel: version:
218218 pkgs.runCommand "serverExe" {} ''
219219 mkdir $out
220220 set -eux
221221 ln -s "${haskellLib.justStaticExecutables backend}"/bin/* $out/
222222 ln -s "${mkAssets assets}" $out/static.assets
223− cp -r ${config} $out/config
224223 ln -s ${mkAssets (compressedJs frontend optimizationLevel)} $out/frontend.jsexe.assets
225224 echo ${version} > $out/version
226225 '';
227226
228− server = { exe, hostName, adminEmail, routeHost, enableHttps, config, version }@args:
227+ server = { exe, hostName, adminEmail, routeHost, enableHttps, version }@args:
229228 let
230229 nixos = import (pkgs.path + /nixos);
231230 in nixos {
@@ -241,8 +240,7 @@ in rec {
241240
242241 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
243242 project = base: projectDefinition:
244− let configPath = base + "/config";
245− projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
243+ let projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
246244 let mkProject = { android ? null #TODO: Better error when missing
247245 , ios ? null #TODO: Better error when missing
248246 , packages ? {}
@@ -275,7 +273,25 @@ in rec {
275273 privateConfigDirs = ["config/backend"];
276274 injectableConfig = builtins.filterSource (path: _:
277275 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
278− ) configPath;
276+ );
277+ __androidWithConfig = configPath: {
278+ ${if android == null then null else frontendName} = {
279+ executableName = "frontend";
280+ ${if builtins.pathExists staticFiles then "assets" else null} =
281+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
282+ (injectableConfig configPath)
283+ processedStatic.symlinked;
284+ } // android;
285+ };
286+ __iosWithConfig = configPath: {
287+ ${if ios == null then null else frontendName} = {
288+ executableName = "frontend";
289+ ${if builtins.pathExists staticFiles then "staticSrc" else null} =
290+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
291+ (injectableConfig configPath)
292+ processedStatic.symlinked;
293+ } // ios;
294+ };
279295 in {
280296 inherit shellToolOverrides tools withHoogle;
281297 overrides = totalOverrides;
@@ -295,39 +311,25 @@ in rec {
295311 commonName
296312 ];
297313 };
298− android = {
299− ${if android == null then null else frontendName} = {
300− executableName = "frontend";
301− ${if builtins.pathExists staticFiles then "assets" else null} =
302− nixpkgs.obeliskExecutableConfig.platforms.android.inject injectableConfig processedStatic.symlinked;
303− } // android;
304− };
305− ios = {
306− ${if ios == null then null else frontendName} = {
307− executableName = "frontend";
308− ${if builtins.pathExists staticFiles then "staticSrc" else null} =
309− nixpkgs.obeliskExecutableConfig.platforms.ios.inject injectableConfig processedStatic.symlinked;
310− } // ios;
311− };
312− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel; };
314+ android = __androidWithConfig (base + "/config");
315+ ios = __iosWithConfig (base + "/config");
316+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
313317 };
314318 in mkProject (projectDefinition args));
315− serverOn = sys: config: version: serverExe
319+ serverOn = sys: version: serverExe
316320 (projectOut sys).ghc.backend
317321 (projectOut system).ghcjs.frontend
318322 (projectOut sys).passthru.staticFiles
319− config
320323 (projectOut sys).passthru.__closureCompilerOptimizationLevel
321324 version;
322325 linuxExe = serverOn "x86_64-linux";
323326 dummyVersion = "Version number is only available for deployments";
324327 in projectOut system // {
325328 linuxExeConfigurable = linuxExe;
326− linuxExe = linuxExe (base + "/config") dummyVersion;
327− exe = serverOn system (base + "/config") dummyVersion;
328− server = args@{ hostName, adminEmail, routeHost, enableHttps, config, version }: let
329− injectableConfig = builtins.filterSource (path: _: !(lib.hasPrefix (toString config + "/backend") (toString path))) config;
330− in server (args // { exe = linuxExe injectableConfig version; });
329+ linuxExe = linuxExe dummyVersion;
330+ exe = serverOn system dummyVersion;
331+ server = args@{ hostName, adminEmail, routeHost, enableHttps, version }:
332+ server (args // { exe = linuxExe version; });
331333 obelisk = import (base + "/.obelisk/impl") {};
332334 };
333335 haskellPackageSets = {
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "89a9b09259a8e0681661756bb587a8285927c1e8",
6− "sha256": "13pja2jy5f8xk2bbywhqaj582f4kqd2ww8j5jd7kk5ldzl6zv8g3"
4+ "branch": "cg-bump-reflex-dom",
5+ "rev": "7ecb4c255bb053a7a4796ffd6331cdbe210de91c",
6+ "sha256": "1mngxa24cfpvxxq4hgh77nw36vny4abl5wi2xmlwpkk25wzm0h0x"
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/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+72−30
@@ -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,6 +19,7 @@ module Obelisk.Backend
1919 , runBackend
2020 , staticRenderContentType
2121 , mkRouteToUrl
22+ , getPublicConfigs
2223 ) where
2324
2425import Prelude hiding (id, (.))
@@ -32,18 +33,21 @@ import Data.Default (Default (..))
3233import Data.Dependent.Sum
3334import 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(..))
4751import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
4852import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4953
@@ -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)
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,16 +99,16 @@ 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
@@ -112,12 +125,19 @@ getRouteWith e = do
112125 pageName <- getPageName
113126 return $ tryDecode e pageName
114127
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
128+serveObeliskApp
129+ :: (MonadSnap m, HasCookies m)
130+ => (R appRoute -> Text)
131+ -> ([Text] -> m ())
132+ -> GhcjsApp (R appRoute)
133+ -> Map Text ByteString
134+ -> R (ObeliskRoute appRoute)
135+ -> m ()
136+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
137+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
118138 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
119139 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
120− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
140+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
121141 ResourceRoute_JSaddleWarp :=> Identity _ -> do
122142 let msg = "Error: Obelisk.Backend received jsaddle request"
123143 liftIO $ putStrLn $ T.unpack msg
@@ -141,35 +161,57 @@ staticRenderContentType :: ByteString
141161staticRenderContentType = "text/html; charset=utf-8"
142162
143163--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
164+serveGhcjsApp
165+ :: (MonadSnap m, HasCookies m)
166+ => (R appRouteComponent -> Text)
167+ -> GhcjsApp (R appRouteComponent)
168+ -> Map Text ByteString
169+ -> R (GhcjsAppRoute appRouteComponent)
170+ -> m ()
171+serveGhcjsApp urlEnc app config = \case
146172 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
147173 modifyResponse $ setContentType staticRenderContentType
148− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
174+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
149175 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
150176
151177runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
152178runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
153179 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
180+ Right validFullEncoder -> do
181+ publicConfigs <- getPublicConfigs
182+ _backend_run backend $ \serveRoute -> do
183+ runSnapWithCommandLineArgs $ do
184+ getRouteWith validFullEncoder >>= \case
185+ Identity r -> case r of
186+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
187+ InR obeliskRoute :=> Identity a ->
188+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189+ obeliskRoute :/ a
161190
162191mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
163192mkRouteToUrl validFullEncoder =
164193 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
165194 in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
166195
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
196+renderGhcjsFrontend
197+ :: (MonadSnap m, HasCookies m)
198+ => (route -> Text)
199+ -> route
200+ -> Map Text ByteString
201+ -> Frontend route
202+ -> m ByteString
203+renderGhcjsFrontend urlEnc route configs f = do
204+ let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
172205 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)
206+ cookies <- askCookies
207+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
208+
209+instance HasCookies Snap where
210+ askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
211+
212+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
213+-- to frontend applications, and hence visible to end users).
214+getPublicConfigs :: IO (Map Text ByteString)
215+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
216+ where
217+ isMemberOf k = any (`T.isPrefixOf` k)
lib/cliapp/obelisk-cliapp.cabal+1
@@ -29,5 +29,6 @@ library
2929 Obelisk.CliApp.Process
3030 Obelisk.CliApp.Spinner
3131 Obelisk.CliApp.TerminalString
32+ Obelisk.CliApp.Theme
3233 Obelisk.CliApp.Types
3334 ghc-options: -Wall -fobject-code
lib/cliapp/src/Obelisk/CliApp/Logging.hs+26−2
@@ -39,17 +39,20 @@ import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggi
3939import Control.Monad.Loops (iterateUntil)
4040import Control.Monad.Reader (MonadIO, ReaderT (..))
4141import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
42+import Data.Maybe
4243import Data.Semigroup ((<>))
4344import Data.Text (Text)
4445import qualified Data.Text as T
4546import qualified Data.Text.IO as T
47+import GHC.IO.Encoding.Types
4648import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
4749 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
4850 SGR (SetColor, SetConsoleIntensity), clearLine)
4951import System.Exit (ExitCode (..))
50−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
52+import System.IO
5153
5254import qualified Obelisk.CliApp.TerminalString as TS
55+import Obelisk.CliApp.Theme
5356import Obelisk.CliApp.Types
5457
5558newCliConfig
@@ -63,7 +66,11 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
6366 lock <- newMVar False
6467 tipDisplayed <- newIORef False
6568 stack <- newIORef ([], [])
66− return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
69+ textEncoding <- hGetEncoding stdout
70+ let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
71+ then unicodeTheme
72+ else noUnicodeTheme
73+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
6774
6875runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
6976runCli c =
@@ -226,3 +233,20 @@ fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
226233fork f = do
227234 c <- getCliConfig
228235 liftIO $ forkIO $ runCli c f
236+
237+-- | Conservatively determines whether the encoding supports Unicode.
238+--
239+-- Currently this uses a whitelist of known-to-work encodings. In principle it
240+-- could test dynamically by opening a file with this encoding, but it doesn't
241+-- look like base exposes any way to determine this in a pure fashion.
242+supportsUnicode :: TextEncoding -> Bool
243+supportsUnicode enc = any ((textEncodingName enc ==) . textEncodingName)
244+ [ utf8
245+ , utf8_bom
246+ , utf16
247+ , utf16be
248+ , utf16le
249+ , utf32
250+ , utf32be
251+ , utf32le
252+ ]
lib/cliapp/src/Obelisk/CliApp/Process.hs+4−3
@@ -33,6 +33,7 @@ import Data.Monoid ((<>))
3333import Data.Text (Text)
3434import qualified Data.Text as T
3535import qualified Data.Text.Encoding as T
36+import Data.Text.Encoding.Error (lenientDecode)
3637import System.Exit (ExitCode (..))
3738import System.IO (Handle)
3839import System.IO.Streams (InputStream, handleToInputStream)
@@ -64,7 +65,7 @@ readProcessAndLogStderr
6465readProcessAndLogStderr sev process = do
6566 (out, _err) <- withProcess process $ \_out err -> do
6667 streamToLog =<< liftIO (streamHandle sev err)
67− liftIO $ T.decodeUtf8 <$> BS.hGetContents out
68+ liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
6869
6970readCreateProcessWithExitCode
7071 :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
@@ -89,7 +90,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
8990
9091 -- TODO interleave stdout and stderr in log correctly
9192 streamToLog =<< liftIO (streamHandle sev_err err)
92− outText <- liftIO $ T.decodeUtf8 <$> BS.hGetContents out
93+ outText <- liftIO $ T.decodeUtf8With lenientDecode <$> BS.hGetContents out
9394 putLogRaw sev_out outText
9495
9596 liftIO (waitForProcess p) >>= \case
@@ -173,7 +174,7 @@ streamToLog
173174streamToLog stream = fix $ \loop -> do
174175 liftIO (Streams.read stream) >>= \case
175176 Nothing -> return ()
176− Just (sev, line) -> putLogRaw sev (T.decodeUtf8 line) >> loop
177+ Just (sev, line) -> putLogRaw sev (T.decodeUtf8With lenientDecode line) >> loop
177178
178179-- | Pretty print a 'CmdSpec'
179180reconstructCommand :: 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+3
@@ -23,6 +23,7 @@ import Data.Text (Text)
2323import System.Exit (ExitCode (..), exitWith)
2424
2525import Obelisk.CliApp.TerminalString (TerminalString)
26+import Obelisk.CliApp.Theme (CliTheme)
2627
2728--------------------------------------------------------------------------------
2829
@@ -88,6 +89,8 @@ data CliConfig e = CliConfig
8889 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
8990 , -- | Failure handler. How to log error and what exit status to use.
9091 _cliConfig_errorLogExitCode :: e -> (Text, Int)
92+ , -- | Theme strings for spinners
93+ _cliConfig_theme :: CliTheme
9194 }
9295
9396class Monad m => HasCliConfig e m | m -> e where
lib/command/obelisk-command.cabal+3
@@ -19,10 +19,12 @@ library
1919 , data-default
2020 , directory
2121 , either
22+ , errors
2223 , filepath
2324 , github
2425 , here
2526 , hit
27+ , hnix
2628 , hpack
2729 , io-streams
2830 , exceptions
@@ -43,6 +45,7 @@ library
4345 , time
4446 , transformers
4547 , unix
48+ , unordered-containers
4649 , yaml
4750 exposed-modules:
4851 Obelisk.App
lib/command/src/Obelisk/Command.hs+37−19
@@ -17,6 +17,7 @@ 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
2223import System.Directory
@@ -91,6 +92,7 @@ data ObCommand
9192 | ObCommand_Thunk ThunkCommand
9293 | ObCommand_Repl
9394 | ObCommand_Watch
95+ | ObCommand_Shell ShellOpts
9496 | ObCommand_Internal ObInternal
9597 deriving Show
9698
@@ -102,7 +104,7 @@ data ObInternal
102104inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
103105inNixShell' p = withProjectRoot "." $ \root -> do
104106 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
105− projectShell root False "ghc" cmd
107+ projectShell root False "ghc" (Just cmd)
106108 where
107109 mkCmd = do
108110 argsCfg <- getArgsConfig
@@ -126,6 +128,7 @@ obCommand cfg = hsubparser
126128 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
127129 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
128130 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
131+ , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
129132 ])
130133 <|> subparser
131134 (mconcat
@@ -142,8 +145,8 @@ deployCommand cfg = hsubparser $ mconcat
142145 ]
143146 where
144147 platformP = hsubparser $ mconcat
145− [ command "android" $ info (pure Android) mempty
146− , command "ios" $ info (pure IOS <*> strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal")) mempty
148+ [ command "android" $ info (pure (Android, [])) mempty
149+ , command "ios" $ info ((,) <$> pure IOS <*> (fmap pure $ strArgument (metavar "TEAMID" <> help "Your Team ID - found in the Apple developer portal"))) mempty
147150 ]
148151
149152 remoteBuilderParser :: Parser (Maybe RemoteBuilder)
@@ -173,16 +176,13 @@ deployInitOpts = DeployInitOpts
173176 <*> flag True False (long "disable-https" <> help "Disable automatic https configuration for the backend")
174177
175178type TeamID = String
176−data PlatformDeployment = Android | IOS TeamID
177− deriving (Show)
178−
179179data RemoteBuilder = RemoteBuilder_ObeliskVM
180180 deriving (Eq, Show)
181181
182182data DeployCommand
183183 = DeployCommand_Init DeployInitOpts
184184 | DeployCommand_Push (Maybe RemoteBuilder)
185− | DeployCommand_Test PlatformDeployment
185+ | DeployCommand_Test (PlatformDeployment, [String])
186186 | DeployCommand_Update
187187 deriving Show
188188
@@ -211,18 +211,36 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
211211 ]
212212
213213data ThunkCommand
214− = ThunkCommand_Update [FilePath]
214+ = ThunkCommand_Update [FilePath] (Maybe String)
215215 | ThunkCommand_Unpack [FilePath]
216− | ThunkCommand_Pack [FilePath]
216+ | ThunkCommand_Pack [FilePath] Bool
217217 deriving Show
218218
219+forceFlag :: Parser Bool
220+forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
221+
219222thunkCommand :: Parser ThunkCommand
220223thunkCommand = hsubparser $ mconcat
221− [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser) $ progDesc "Update thunk to latest revision available"
224+ [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
222225 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
223− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
226+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
224227 ]
225228
229+data ShellOpts
230+ = ShellOpts
231+ { _shellOpts_shell :: String
232+ , _shellOpts_command :: Maybe String
233+ }
234+ deriving Show
235+
236+shellOpts :: Parser ShellOpts
237+shellOpts = ShellOpts
238+ <$> ( flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
239+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
240+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
241+ )
242+ <*> optional (strArgument (metavar "COMMAND"))
243+
226244parserPrefs :: ParserPrefs
227245parserPrefs = defaultPrefs
228246 { prefShowHelpOnEmpty = True
@@ -327,30 +345,30 @@ ob = \case
327345 Right (ThunkData_Packed ptr) -> return ptr
328346 Right (ThunkData_Checkout (Just ptr)) -> return ptr
329347 Right (ThunkData_Checkout Nothing) ->
330− getThunkPtr' False root
348+ getThunkPtr False root
331349 let sshKeyPath = _deployInitOpts_sshKey deployOpts
332350 hostname = _deployInitOpts_hostname deployOpts
333351 route = _deployInitOpts_route deployOpts
334352 adminEmail = _deployInitOpts_adminEmail deployOpts
335353 enableHttps = _deployInitOpts_enableHttps deployOpts
336− deployInit thunkPtr (root </> "config") deployDir
337− sshKeyPath hostname route adminEmail enableHttps
354+ deployInit thunkPtr deployDir sshKeyPath hostname route adminEmail enableHttps
338355 DeployCommand_Push remoteBuilder -> do
339356 deployPath <- liftIO $ canonicalizePath "."
340357 deployPush deployPath $ case remoteBuilder of
341358 Nothing -> pure []
342359 Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
343360 DeployCommand_Update -> deployUpdate "."
344− DeployCommand_Test Android -> deployMobile "android" []
345− DeployCommand_Test (IOS teamID) -> deployMobile "ios" [teamID]
361+ DeployCommand_Test (platform, extraArgs) -> deployMobile platform extraArgs
346362 ObCommand_Run -> inNixShell' $ static run
347363 -- inNixShell ($(mkClosure 'ghcidAction) ())
348364 ObCommand_Thunk tc -> case tc of
349− ThunkCommand_Update thunks -> mapM_ updateThunkToLatest thunks
365+ ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
350366 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
351− ThunkCommand_Pack thunks -> forM_ thunks packThunk
367+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
352368 ObCommand_Repl -> runRepl
353369 ObCommand_Watch -> inNixShell' $ static runWatch
370+ ObCommand_Shell so -> withProjectRoot "." $ \root ->
371+ projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
354372 ObCommand_Internal icmd -> case icmd of
355373 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
356374 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
@@ -365,7 +383,7 @@ getArgsConfig :: IO ArgsConfig
365383getArgsConfig = pure $ ArgsConfig { _argsConfig_enableVmBuilderByDefault = System.Info.os == "darwin" }
366384
367385encodeStaticKey :: StaticKey -> String
368−encodeStaticKey = T.unpack . decodeUtf8 . Base16.encode . LBS.toStrict . Binary.encode
386+encodeStaticKey = T.unpack . decodeUtf8With lenientDecode . Base16.encode . LBS.toStrict . Binary.encode
369387
370388-- TODO: Use failWith in place of fail to be consistent.
371389decodeStaticKey :: String -> Either String StaticKey
lib/command/src/Obelisk/Command/Deploy.hs+146−17
@@ -3,23 +3,34 @@
33{-# LANGUAGE OverloadedStrings #-}
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
6+{-# LANGUAGE DeriveGeneric #-}
7+{-# LANGUAGE TypeApplications #-}
68module Obelisk.Command.Deploy where
79
810import Control.Lens
911import Control.Monad
10−import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try)
12+import Control.Monad.Catch (Exception (displayException), MonadThrow, throwM, try, bracket_)
1113import Control.Monad.IO.Class (liftIO)
14+import Data.Aeson (FromJSON, ToJSON, encode, eitherDecode)
1215import Data.Bits
16+import qualified Data.ByteString.Lazy as BSL
1317import Data.Default
18+import qualified Data.HashMap.Strict as HM
1419import qualified Data.Map as Map
20+import Data.Text (Text)
1521import qualified Data.Set as Set
1622import qualified Data.Text as T
1723import qualified Data.Text.IO as T
24+import GHC.Generics
25+import Nix.Convert
26+import Nix.Pretty
27+import Nix.Value
1828import System.Directory
1929import System.Environment (getEnvironment)
2030import System.FilePath
31+import System.IO
2132import System.Posix.Files
22−import System.Process (delegate_ctlc, env, proc)
33+import System.Process (delegate_ctlc, env, proc, cwd)
2334import Text.URI (URI)
2435import qualified Text.URI as URI
2536import Text.URI.Lens
@@ -36,33 +47,38 @@ deployInit
3647 => ThunkPtr
3748 -> FilePath
3849 -> FilePath
39− -> FilePath
4050 -> [String] -- ^ hostnames
4151 -> String -- ^ route
4252 -> String -- ^ admin email
4353 -> Bool -- ^ enable https
4454 -> m ()
45−deployInit thunkPtr configDir deployDir sshKeyPath hostnames route adminEmail enableHttps = do
46− (hasConfigDir, localKey) <- withSpinner ("Preparing " <> T.pack deployDir) $ do
47− hasConfigDir <- liftIO $ do
48− createDirectoryIfMissing True deployDir
49− doesDirectoryExist configDir
55+deployInit thunkPtr deployDir sshKeyPath hostnames route adminEmail enableHttps = do
56+ localKey <- withSpinner ("Preparing " <> T.pack deployDir) $ do
5057 localKey <- liftIO (doesFileExist sshKeyPath) >>= \case
5158 False -> failWith $ T.pack $ "ob deploy init: file does not exist: " <> sshKeyPath
5259 True -> pure $ deployDir </> "ssh_key"
5360 callProcessAndLogOutput (Notice, Error) $
5461 proc "cp" [sshKeyPath, localKey]
5562 liftIO $ setFileMode localKey $ ownerReadMode .|. ownerWriteMode
56− return $ (hasConfigDir, localKey)
63+ return localKey
5764 withSpinner "Validating configuration" $ do
5865 void $ getHostFromRoute enableHttps route -- make sure that hostname is present
5966 forM_ hostnames $ \hostname -> do
6067 putLog Notice $ "Verifying host keys (" <> T.pack hostname <> ")"
6168 -- Note: we can't use a spinner here as this function will prompt the user.
6269 verifyHostKey (deployDir </> "backend_known_hosts") localKey hostname
63− when hasConfigDir $ withSpinner "Importing project configuration" $ do
70+ --IMPORTANT: We cannot copy config directory from the development project to
71+ --the deployment directory. If we do, it's very likely someone will
72+ --accidentally create a production deployment that uses development
73+ --credentials to connect to some resources. This could result in, e.g.,
74+ --production data backed up to a dev environment.
75+ withSpinner "Creating project configuration directories" $ do
6476 callProcessAndLogOutput (Notice, Error) $
65− proc "cp" [ "-r" , "-T" , configDir , deployDir </> "config"]
77+ proc "mkdir" [ "-p"
78+ , deployDir </> "config" </> "backend"
79+ , deployDir </> "config" </> "common"
80+ , deployDir </> "config" </> "frontend"
81+ ]
6682 withSpinner "Writing deployment configuration" $ do
6783 writeDeployConfig deployDir "backend_hosts" $ unlines hostnames
6884 writeDeployConfig deployDir "enable_https" $ show enableHttps
@@ -92,7 +108,7 @@ deployPush deployPath getNixBuilders = do
92108 Right (ThunkData_Packed ptr) -> return ptr
93109 Right (ThunkData_Checkout _) -> do
94110 checkGitCleanStatus srcPath True >>= \case
95− True -> packThunk srcPath
111+ True -> packThunk False srcPath
96112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
97113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
98114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -112,7 +128,6 @@ deployPush deployPath getNixBuilders = do
112128 , strArg "routeHost" routeHost
113129 , strArg "version" version
114130 , boolArg "enableHttps" enableHttps
115− , rawArg "config" $ deployPath </> "config"
116131 ]
117132 & nixCmdConfig_builders .~ builders
118133 pure result
@@ -121,7 +136,15 @@ deployPush deployPath getNixBuilders = do
121136 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
122137 callProcess'
123138 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
124− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
139+ "nix-copy-closure" ["-v", "--to", "--use-substitutes", "root@" <> host, "--gzip", outputPath]
140+ withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
141+ callProcessAndLogOutput (Notice, Warning) $
142+ proc "rsync"
143+ [ "-e ssh " <> unwords sshOpts
144+ , "-qarvz"
145+ , deployPath </> "config"
146+ , "root@" <> host <> ":/var/lib/backend"
147+ ]
125148 --TODO: Create GC root so we're sure our closure won't go away during this time period
126149 withSpinner "Switching to new configuration" $ ifor_ buildOutputByHost $ \host outputPath -> do
127150 callProcessAndLogOutput (Notice, Warning) $
@@ -148,15 +171,121 @@ deployPush deployPath getNixBuilders = do
148171 callProcessAndLogOutput (Notice, Notice) p
149172
150173deployUpdate :: MonadObelisk m => FilePath -> m ()
151−deployUpdate deployPath = updateThunkToLatest $ deployPath </> "src"
174+deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
175+
176+keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
177+keytoolToAndroidConfig conf = runIdentity $ do
178+ path <- toValue $ Path $ _keytoolConfig_keystore conf
179+ storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
180+ alias <- toValue $ T.pack $ _keytoolConfig_alias conf
181+ keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
182+ return $ HM.fromList
183+ [ ("storeFile", path)
184+ , ("storePassword", storepass)
185+ , ("keyAlias", alias)
186+ , ("keyPassword", keypass)
187+ ]
152188
153−deployMobile :: MonadObelisk m => String -> [String] -> m ()
189+data PlatformDeployment = Android | IOS
190+ deriving (Show, Eq)
191+
192+renderPlatformDeployment :: PlatformDeployment -> String
193+renderPlatformDeployment = \case
194+ Android -> "android"
195+ IOS -> "ios"
196+
197+deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
154198deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
155199 let srcDir = root </> "src"
200+ configDir = root </> "config"
156201 exists <- liftIO $ doesDirectoryExist srcDir
157202 unless exists $ failWith "ob test should be run inside of a deploy directory"
158− result <- nixBuildAttrWithCache srcDir $ platform <> ".frontend"
203+ nixBuildTarget <- case platform of
204+ Android -> do
205+ let keystorePath = root </> "android_keystore.jks"
206+ keytoolConfPath = root </> "android_keytool_config.json"
207+ hasKeystore <- liftIO $ doesFileExist keystorePath
208+ when (not hasKeystore) $ do
209+ -- TODO log instructions for how to modify the keystore
210+ putLog Notice $ "Creating keystore: " <> T.pack keystorePath
211+ putLog Notice "Enter a keystore password: "
212+ keyStorePassword <- liftIO $ withEcho False getLine
213+ putLog Notice "Re-enter the keystore password: "
214+ keyStorePassword' <- liftIO $ withEcho False getLine
215+ unless (keyStorePassword' == keyStorePassword) $ failWith "passwords do not match"
216+ let keyToolConf = KeytoolConfig
217+ { _keytoolConfig_keystore = keystorePath
218+ , _keytoolConfig_alias = "obelisk"
219+ , _keytoolConfig_storepass = keyStorePassword
220+ , _keytoolConfig_keypass = keyStorePassword
221+ }
222+ createKeystore root $ keyToolConf
223+ liftIO $ BSL.writeFile keytoolConfPath $ encode keyToolConf
224+ checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
225+ unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
226+ keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
227+ liftIO $ putStrLn $ show keytoolConfContents
228+ releaseKey <- case eitherDecode keytoolConfContents of
229+ Left err -> failWith $ T.pack err
230+ Right conf -> do
231+ let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
232+ return $ printNix $ runIdentity nvset
233+ let expr = mconcat
234+ [ "with (import ", srcDir, " {});"
235+ , "android.frontend.override (drv: { "
236+ , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
237+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
238+ , "})"
239+ ]
240+ return $ Target
241+ { _target_path = Nothing
242+ , _target_attr = Nothing
243+ , _target_expr = Just expr
244+ }
245+ IOS -> do
246+ let expr = mconcat
247+ [ "with (import ", srcDir, " {});"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
249+ ]
250+ return $ Target
251+ { _target_path = Nothing
252+ , _target_attr = Nothing
253+ , _target_expr = Just expr
254+ }
255+ result <- nixCmd $ NixCmd_Build $ def
256+ & nixBuildConfig_outLink .~ OutLink_None
257+ & nixCmdConfig_target .~ nixBuildTarget
258+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
159259 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
260+ where
261+ withEcho showEcho f = do
262+ prevEcho <- hGetEcho stdin
263+ bracket_ (hSetEcho stdin showEcho) (hSetEcho stdin prevEcho) f
264+
265+data KeytoolConfig = KeytoolConfig
266+ { _keytoolConfig_keystore :: FilePath
267+ , _keytoolConfig_alias :: String
268+ , _keytoolConfig_storepass :: String
269+ , _keytoolConfig_keypass :: String
270+ } deriving (Show, Generic)
271+
272+instance FromJSON KeytoolConfig
273+instance ToJSON KeytoolConfig
274+
275+createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
276+createKeystore root config = do
277+ let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
278+ callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
279+ where
280+ keytoolCmd = processToShellString "keytool"
281+ [ "-genkeypair", "-noprompt"
282+ , "-keystore", _keytoolConfig_keystore config
283+ , "-keyalg", "RSA", "-keysize", "2048"
284+ , "-validity", "1000000000"
285+ , "-storepass", _keytoolConfig_storepass config
286+ , "-alias", _keytoolConfig_alias config
287+ , "-keypass", _keytoolConfig_keypass config
288+ ]
160289
161290-- | Simplified deployment configuration mechanism. At one point we may revisit this.
162291writeDeployConfig :: MonadObelisk m => FilePath -> FilePath -> String -> m ()
lib/command/src/Obelisk/Command/Nix.hs+2
@@ -15,6 +15,7 @@ module Obelisk.Command.Nix
1515 , nixCmdConfig_args
1616 , nixCmdConfig_builders
1717 , NixBuildConfig (..)
18+ , nixBuildConfig_common
1819 , nixBuildConfig_outLink
1920 , NixInstantiateConfig (..)
2021 , nixInstantiateConfig_eval
@@ -180,3 +181,4 @@ nixCmd cmdCfg = withSpinner' ("Running " <> cmd <> desc) (Just $ const $ "Built
180181 [ (" on " <>) <$> path
181182 , (\a -> " [" <> a <> "]") <$> (commonCfg ^. nixCmdConfig_target . target_attr)
182183 ]
184+
lib/command/src/Obelisk/Command/Project.hs+11−7
@@ -26,7 +26,7 @@ import System.FilePath
2626import System.IO.Temp
2727import System.Posix (FileStatus, UserID, deviceID, fileID, fileMode, fileOwner, getFileStatus, getRealUserID)
2828import System.Posix.Files
29−import System.Process (CreateProcess, cwd, proc, waitForProcess)
29+import System.Process (CreateProcess, cwd, proc, waitForProcess, delegate_ctlc)
3030
3131import GitHub.Data.GitData (Branch)
3232import GitHub.Data.Name (Name)
@@ -196,21 +196,25 @@ 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
207− (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCwd (Just root) $ proc "nix-shell" $
207+ (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208208 [ "--pure" | isPure ] <>
209209 [ "-A"
210210 , "shells." <> shellName
211− , "--run", command
212− ]
211+ ] <> case command of
212+ Nothing -> []
213+ Just c -> ["--run", c]
213214 void $ liftIO $ waitForProcess ph
214215
216+setCtlc :: CreateProcess -> CreateProcess
217+setCtlc cfg = cfg { delegate_ctlc = True }
218+
215219setCwd :: Maybe FilePath -> CreateProcess -> CreateProcess
216220setCwd fp cfg = cfg { cwd = fp }
lib/command/src/Obelisk/Command/Run.hs+41−10
@@ -10,8 +10,8 @@ import Control.Monad
1010import Control.Monad.IO.Class (liftIO)
1111import Control.Monad.Reader (MonadIO)
1212import Data.Either
13−import Data.List
14−import Data.List.NonEmpty as NE
13+import qualified Data.List as L
14+import qualified Data.List.NonEmpty as NE
1515import Data.Maybe
1616import qualified Data.Text as T
1717import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
@@ -46,6 +46,8 @@ data CabalPackageInfo = CabalPackageInfo
4646 -- ^ List of hs src dirs of the library component
4747 , _cabalPackageInfo_defaultExtensions :: [Extension]
4848 -- ^ List of globally enable extensions of the library component
49+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
50+ -- ^ List of globally set languages of the library component
4951 }
5052
5153-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -54,14 +56,32 @@ run = do
5456 pkgs <- getLocalPkgs
5557 withGhciScript pkgs $ \dotGhciPath -> do
5658 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"]
59+ assets <- withProjectRoot "." $ \root -> do
60+ let importableRoot = if "/" `isInfixOf` root
61+ then root
62+ else "./" <> root
63+ isDerivation <- readProcessAndLogStderr Debug $
64+ proc "nix"
65+ [ "eval"
66+ , "-f"
67+ , root
68+ , "(let a = import " <> importableRoot <> " {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))"
69+ , "--raw"
70+ -- `--raw` is not available with old nix-instantiate. It drops quotation
71+ -- marks and trailing newline, so is very convenient for shelling out.
72+ ]
73+ -- Check whether the impure static files are a derivation (and so must be built)
74+ if isDerivation == "1"
75+ then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
76+ proc "nix-build"
77+ [ "--no-out-link"
78+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
79+ ]
80+ else readProcessAndLogStderr Debug $
81+ proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
6282 putLog Debug $ "Assets impurely loaded from: " <> assets
6383 runGhcid dotGhciPath $ Just $ unwords
64− [ "run"
84+ [ "Obelisk.Run.run"
6585 , show freePort
6686 , "(runServeAsset " ++ show assets ++ ")"
6787 , "Backend.backend"
@@ -121,6 +141,8 @@ parseCabalPackage dir = do
121141 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
122142 , _cabalPackageInfo_defaultExtensions =
123143 defaultExtensions $ libBuildInfo lib
144+ , _cabalPackageInfo_defaultLanguage =
145+ defaultLanguage $ libBuildInfo lib
124146 }
125147 Left (_, errors) -> do
126148 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -150,17 +172,22 @@ withGhciScript pkgs f = do
150172 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
151173
152174 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
175+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
176+ -- NOTE when no default-language is present cabal sets Haskell98
177+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
153178 extensionsLine = if extensions == mempty
154179 then ""
155180 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
156181 dotGhci = unlines $
157182 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
158183 , extensionsLine
184+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
159185 , ":load Backend Frontend"
160186 , "import Obelisk.Run"
161187 , "import qualified Frontend"
162188 , "import qualified Backend"
163189 ]
190+ warnDifferentLanguages language
164191 withSystemTempDirectory "ob-ghci" $ \fp -> do
165192 let dotGhciPath = fp </> ".ghci"
166193 liftIO $ writeFile dotGhciPath dotGhci
@@ -170,12 +197,16 @@ withGhciScript pkgs f = do
170197 rootedSourceDirs pkg = NE.toList $
171198 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
172199
200+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
201+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
202+warnDifferentLanguages _ = return ()
203+
173204-- | Run ghci repl
174205runGhciRepl
175206 :: MonadObelisk m
176207 => FilePath -- ^ Path to .ghci
177208 -> m ()
178−runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-ghci-script", dotGhci]
209+runGhciRepl dotGhci = inProjectShell "ghc" $ unwords $ "ghci" : ["-no-user-package-db", "-ghci-script", dotGhci]
179210
180211-- | Run ghcid
181212runGhcid
@@ -188,7 +219,7 @@ runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
188219 opts =
189220 [ "-W"
190221 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
191− , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -ghci-script " <> dotGhci <> "' "
222+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -no-user-package-db -ghci-script " <> dotGhci <> "' "
192223 , "--reload=config"
193224 , "--outputfile=ghcid-output.txt"
194225 ] <> testCmd
lib/command/src/Obelisk/Command/Thunk.hs+67−40
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
2626 , readThunk
2727 , updateThunk
2828 , getThunkPtr
29− , getThunkPtr'
3029 , parseGitUri
3130 , uriThunkPtr
3231 ) where
@@ -378,19 +377,38 @@ createThunkWithLatest target s = do
378377 , _thunkPtr_rev = rev
379378 }
380379
381−updateThunkToLatest :: MonadObelisk m => FilePath -> m ()
382−updateThunkToLatest target = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
383− (overwrite, ptr) <- readThunk target >>= \case
384− Left err -> failWith $ T.pack $ "thunk update: " <> show err
385− Right c -> case c of
386− ThunkData_Packed t -> return (target, t)
387− ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
388− let src = _thunkPtr_source ptr
389− rev <- getLatestRev src
390− overwriteThunk overwrite $ ThunkPtr
391− { _thunkPtr_source = src
392− , _thunkPtr_rev = rev
393− }
380+updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
381+updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $ do
382+ checkThunkDirectory "ob thunk update directory cannot be '.'" target
383+ -- check to see if thunk should be updated to a specific branch or just update it's current branch
384+ case mBranch of
385+ Nothing -> do
386+ (overwrite, ptr) <- readThunk target >>= \case
387+ Left err -> failWith $ T.pack $ "thunk update: " <> show err
388+ Right c -> case c of
389+ ThunkData_Packed t -> return (target, t)
390+ ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
391+ let src = _thunkPtr_source ptr
392+ rev <- getLatestRev src
393+ overwriteThunk overwrite $ ThunkPtr
394+ { _thunkPtr_source = src
395+ , _thunkPtr_rev = rev
396+ }
397+ Just branch -> readThunk target >>= \case
398+ Left err -> failWith $ T.pack $ "thunk update: " <> show err
399+ Right c -> case c of
400+ ThunkData_Packed t -> case _thunkPtr_source t of
401+ ThunkSource_Git tsg -> setThunk target tsg branch
402+ ThunkSource_GitHub tsgh -> do
403+ let tsg = forgetGithub False tsgh
404+ setThunk target tsg branch
405+ ThunkData_Checkout _ -> failWith $ T.pack $ "thunk located at " <> (show target) <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk update again."
406+
407+setThunk :: MonadObelisk m => FilePath -> GitSource -> String -> m ()
408+setThunk target gs branch = do
409+ newThunkPtr <- uriThunkPtr (_gitSource_url gs) (Just $ T.pack branch) Nothing
410+ overwriteThunk target newThunkPtr
411+ updateThunkToLatest target Nothing
394412
395413-- | All recognized github standalone loaders, ordered from newest to oldest.
396414-- This tool will only ever produce the newest one when it writes a thunk.
@@ -554,7 +572,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
554572 return tmpThunk
555573 Right _ -> failWith $ "Thunk is not packed"
556574 updateThunkFromTmp p' = do
557− _ <- packThunk' True p'
575+ _ <- packThunk' True False p'
558576 callProcessAndLogOutput (Notice, Error) $
559577 proc "cp" ["-r", "-T", p', p]
560578
@@ -565,15 +583,14 @@ unpackThunk :: MonadObelisk m => FilePath -> m ()
565583unpackThunk = unpackThunk' False
566584
567585-- | Check that we are not somewhere inside the thunk directory
568−checkThunkDirectory :: MonadObelisk m => FilePath -> m ()
569−checkThunkDirectory thunkDir = do
586+checkThunkDirectory :: MonadObelisk m => Text -> FilePath -> m ()
587+checkThunkDirectory msg thunkDir = do
570588 currentDir <- liftIO getCurrentDirectory
571589 thunkDir' <- liftIO $ canonicalizePath thunkDir
572− when (thunkDir' `L.isInfixOf` currentDir) $
573− failWith "Can't pack/unpack from within the thunk directory"
590+ when (thunkDir' `L.isInfixOf` currentDir) $ failWith msg
574591
575592unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
576−unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
593+unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
577594 Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
578595 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
579596 Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
@@ -604,28 +621,24 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkD
604621 callProcessAndLogOutput (Notice, Error) $
605622 proc "mv" ["-T", tmpRepo, thunkDir]
606623
607−--TODO: add force mode to pack even if changes are present
608624--TODO: add a rollback mode to pack to the original thunk
609−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
625+packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
610626packThunk = packThunk' False
611627
612−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
613−packThunk' noTrail thunkDir = checkThunkDirectory 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
614630 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
615631 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
616632 Right (ThunkData_Checkout _) -> do
617633 withSpinner' ("Packing thunk " <> T.pack thunkDir)
618634 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
619− thunkPtr <- getThunkPtr thunkDir
635+ thunkPtr <- getThunkPtr (not force) thunkDir
620636 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
621637 liftIO $ createThunk thunkDir thunkPtr
622638 pure thunkPtr
623639
624−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
625−getThunkPtr = getThunkPtr' True
626−
627−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628−getThunkPtr' checkClean thunkDir = do
640+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641+getThunkPtr checkClean thunkDir = do
629642 when checkClean $ ensureCleanGitRepo thunkDir True $
630643 "thunk pack: thunk checkout contains unsaved modifications"
631644
@@ -639,6 +652,22 @@ getThunkPtr' checkClean thunkDir = do
639652 ] ++ T.lines stashOutput
640653 True -> return ()
641654
655+ -- Get current branch ``
656+ (mCurrentBranch, mCurrentCommit) <- do
657+ b <- listToMaybe
658+ <$> T.lines
659+ <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
660+ c <- listToMaybe
661+ <$> T.lines
662+ <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
663+ case b of
664+ (Just "HEAD") -> failWith $ T.unlines $
665+ [ "thunk pack: You are in 'detached HEAD' state."
666+ , "If you want to pack at the current ref \
667+ \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
668+ ]
669+ _ -> return (b, c)
670+
642671 -- Get information on all branches and their (optional) designated upstream
643672 -- correspondents
644673 (headDump :: [Text]) <- T.lines <$> readGitProcess thunkDir
@@ -714,11 +743,6 @@ getThunkPtr' checkClean thunkDir = do
714743 -- We assume it's safe to pack the thunk at this point
715744 putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
716745
717− -- Get current branch ``
718− mCurrentBranch <- listToMaybe
719− <$> T.lines
720− <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
721−
722746 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
723747
724748 [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir
@@ -730,7 +754,7 @@ getThunkPtr' checkClean thunkDir = do
730754 remoteUri <- case parseGitUri remoteUri' of
731755 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
732756 Just uri -> pure uri
733− uriThunkPtr remoteUri mCurrentBranch
757+ uriThunkPtr remoteUri mCurrentBranch mCurrentCommit
734758
735759-- | Get the latest revision available from the given source
736760getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
@@ -749,9 +773,11 @@ getLatestRev os = do
749773-- performance. If that doesn't work (e.g. authentication issue), we fall back
750774-- on just doing things the normal way for git repos in general, and save it as
751775-- a regular git thunk.
752−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> m ThunkPtr
753−uriThunkPtr uri mbranch = do
754− (_, commit) <- gitGetCommitBranch uri mbranch
776+uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> Maybe Text -> m ThunkPtr
777+uriThunkPtr uri mbranch mcommit = do
778+ commit <- case mcommit of
779+ Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
780+ (Just c) -> return c
755781 (src, rev) <- case uriToThunkSource uri mbranch of
756782 ThunkSource_GitHub s -> do
757783 rev <- runExceptT $ githubThunkRev s commit
@@ -865,9 +891,10 @@ gitThunkRev s commit = do
865891gitGetCommitBranch
866892 :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
867893gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
868− bothMaps <- gitLsRemote
894+ (_, bothMaps) <- gitLsRemote
869895 (T.unpack $ render uri)
870896 (GitRef_Branch <$> mbranch)
897+ Nothing
871898 branch <- case mbranch of
872899 Nothing -> withExitFailMessage "Failed to find default branch" $ do
873900 b <- rethrowE $ gitLookupDefaultBranch bothMaps
lib/command/src/Obelisk/Command/Utils.hs+15−6
@@ -22,6 +22,7 @@ import qualified Data.Text as T
2222import Data.Void (Void)
2323import System.Directory (canonicalizePath)
2424import System.Environment (getExecutablePath)
25+import System.Exit (ExitCode)
2526import qualified System.Process as P
2627import Text.Megaparsec as MP
2728import Text.Megaparsec.Char as MP
@@ -78,6 +79,9 @@ copyDir src dest =
7879readGitProcess :: MonadObelisk m => FilePath -> [String] -> m Text
7980readGitProcess repo = readProcessAndLogOutput (Debug, Notice) . gitProc repo
8081
82+readGitProcessNoRepo :: MonadObelisk m => [String] -> m Text
83+readGitProcessNoRepo = readProcessAndLogOutput (Debug, Notice) . gitProcNoRepo
84+
8185processToShellString :: FilePath -> [String] -> String
8286processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
8387 where quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
@@ -119,16 +123,21 @@ gitLsRemote
119123 :: MonadObelisk m
120124 => String
121125 -> Maybe GitRef
122− -> m GitLsRemoteMaps
123−gitLsRemote repository mRef = do
124− t <- readProc $ gitProcNoRepo $
125− ["ls-remote", "--symref", repository]
126− ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
126+ -> Maybe String
127+ -> m (ExitCode, GitLsRemoteMaps)
128+gitLsRemote repository mRef mBranch = do
129+ (exitCode, out, _err) <- case mBranch of
130+ Nothing -> readCreateProcessWithExitCode $ gitProcNoRepo $
131+ ["ls-remote", "--exit-code", "--symref", repository]
132+ ++ (maybeToList $ T.unpack . showGitRef <$> mRef)
133+ Just branchName -> readCreateProcessWithExitCode $ gitProcNoRepo $
134+ ["ls-remote", "--exit-code", repository, branchName]
135+ let t = T.pack out
127136 maps <- case MP.runParser parseLsRemote "" t of
128137 Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
129138 Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
130139 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
131− pure maps
140+ pure (exitCode, maps)
132141
133142lexeme :: Parsec Void Text a -> Parsec Void Text a
134143lexeme = ML.lexeme $ void $ MP.takeWhileP (Just "within-line white space") $
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+7−3
@@ -5,19 +5,23 @@
55{ nixpkgs
66, filterGitSource # TODO define this in obelisk
77}:
8−let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} ''
8+let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
99 set -x
1010 mkdir -p $out
1111 cp --no-preserve=mode -Lr "${assets}" $out/static
1212 chmod +w "$out"
13+ '' + nixpkgs.lib.optionalString (!(builtins.isNull config)) ''
1314 if ! mkdir $out/config; then
1415 2>&1 echo config directory already exists or could not be created
1516 exit 1
1617 fi
1718 cp -a "${config}"/* "$out/config"
18− '';
19+ find "$out/config" -type f -printf '%P\0' >"$out/config.files"
20+ '');
1921in with nixpkgs.haskell.lib; {
20− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
22+ haskellOverlay = self: super: {
23+ obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
24+ };
2125 platforms = {
2226 android = {
2327 # 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.nix+5−3
@@ -1,16 +1,18 @@
11{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
2+, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3+, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4+, jsaddle
35, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
46}:
57let isAndroid = hostPlatform.libc == "bionic";
68 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
79 isGhcjs = ghc.isGhcjs or false;
810in mkDerivation {
9− pname = "obelisk-executable-config";
11+ pname = "obelisk-executable-config-lookup";
1012 version = "0.1";
1113 src = ./.;
1214 libraryHaskellDepends = [
13− base bytestring filepath text transformers
15+ base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
1416 ] ++ (if isAndroid then [
1517 android-activity
1618 ] else if isIOS then [
lib/executable-config/lookup/obelisk-executable-config-lookup.cabalnew file+45
@@ -0,0 +1,45 @@
1+name: obelisk-executable-config-lookup
2+version: 0.1.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
8+ if os(ios)
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
11+ build-depends: jsaddle-wkwebview
12+ build-depends: directory
13+ else
14+ if os(linux-android)
15+ hs-source-dirs: src-android, src
16+ c-sources: cbits/ExecutableConfig.c
17+ other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
18+ build-depends: android-activity
19+ ld-options: -landroid
20+ else
21+ if impl(ghcjs)
22+ hs-source-dirs: src-ghcjs, src
23+ build-depends: ghcjs-dom
24+ else
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
27+ build-depends: directory
28+
29+ build-depends:
30+ base,
31+ base64-bytestring,
32+ bytestring,
33+ containers,
34+ filepath,
35+ text,
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
44+
45+ ghc-options: -Wall
lib/executable-config/lookup/obelisk-executable-config.cabaldeleted−30
@@ -1,30 +0,0 @@
1−name: obelisk-executable-config
2−version: 0.1.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− exposed-modules: Obelisk.ExecutableConfig
8− if os(ios)
9− hs-source-dirs: src-ios
10− build-depends: jsaddle-wkwebview
11− else
12− if os(linux-android)
13− hs-source-dirs: src-android
14− c-sources: cbits/ExecutableConfig.c
15− other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
16− build-depends: android-activity
17− ld-options: -landroid
18− else
19− if impl(ghcjs)
20− hs-source-dirs: src-ghcjs
21− build-depends: ghcjs-dom
22− else
23− hs-source-dirs: src
24−
25− build-depends:
26− base,
27− bytestring,
28− filepath,
29− text,
30− transformers
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Control.Exception (bracket)
6+import Control.Monad (forM)
7+import Data.ByteString (ByteString)
8+import qualified Data.ByteString as BS
9+import Data.Map (Map)
10+import qualified Data.Map as Map
11+import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
13+import Foreign.C.String (withCString)
14+import Foreign.Ptr (nullPtr)
15+
16+import Obelisk.ExecutableConfig.Internal.AssetManager
17+
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
19+getFromMgr mgr name = do
20+ let open = do
21+ a <- BS.useAsCString name $ \fn ->
22+ assetManager_open mgr fn 3
23+ return $ if unAAsset a == nullPtr
24+ then Nothing
25+ else Just a
26+ close = mapM_ asset_close
27+ bracket open close $ mapM $ \asset -> do
28+ b <- asset_getBuffer asset
29+ l <- asset_getLength asset
30+ BS.packCStringLen (b, fromIntegral l)
31+
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
34+ mgr <- assetManagerFromJava mgrObj
35+ let openDir = do
36+ d <- withCString "config.files" $ \fn ->
37+ assetManager_open mgr fn 3
38+ return $ if unAAsset d == nullPtr
39+ then Nothing
40+ else Just d
41+ closeDir = mapM_ asset_close
42+ configPaths <- bracket openDir closeDir $ \case
43+ Just asset -> do
44+ b <- asset_getBuffer asset
45+ l <- asset_getLength asset
46+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
47+ Nothing -> error "could not open configuration manifest 'config.files'"
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
51+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
54+
55+lines0 :: BS.ByteString -> [BS.ByteString]
56+lines0 ps
57+ | BS.null ps = []
58+ | otherwise = case BS.elemIndex 0 ps of
59+ Nothing -> [ps]
60+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+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+124
@@ -0,0 +1,124 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.ByteString (ByteString)
31+import Data.Map (Map)
32+import qualified Data.Map as Map
33+import Data.Text (Text)
34+import Reflex
35+import Reflex.Host.Class
36+import Reflex.Dom
37+#ifndef ghcjs_HOST_OS
38+import Language.Javascript.JSaddle (MonadJSM)
39+#endif
40+
41+class Monad m => HasConfigs m where
42+ getConfigs :: m (Map Text ByteString)
43+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
44+ getConfigs = lift getConfigs
45+ getConfig :: Text -> m (Maybe ByteString)
46+ getConfig k = do
47+ configs <- getConfigs
48+ return $ Map.lookup k configs
49+
50+instance Monad m => HasConfigs (ConfigsT m) where
51+ getConfigs = ConfigsT ask
52+
53+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
54+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
55+instance HasConfigs m => HasConfigs (EventWriterT t w m)
56+instance HasConfigs m => HasConfigs (PostBuildT t m)
57+instance HasConfigs m => HasConfigs (QueryT t q m)
58+instance HasConfigs m => HasConfigs (ReaderT r m)
59+instance HasConfigs m => HasConfigs (RequesterT t request response m)
60+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
61+instance HasConfigs m => HasConfigs (TriggerEventT t m)
62+
63+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
64+ deriving
65+ ( Functor
66+ , Applicative
67+ , Monad
68+ , MonadPlus
69+ , Alternative
70+ , MonadFix
71+ , MonadIO
72+ , MonadBase m'
73+ , MonadBaseControl m'
74+ , MonadRef
75+ , MonadTrans
76+ , DomBuilder t
77+ , MonadHold t
78+ , MonadReflexCreateTrigger t
79+ , MonadSample t
80+ , NotReady t
81+ , PostBuild t
82+ , TriggerEvent t
83+ , HasDocument
84+ , DomRenderHook t
85+ , HasJSContext
86+ , HasJS js
87+#ifndef ghcjs_HOST_OS
88+ , MonadJSM
89+#endif
90+ )
91+
92+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
93+ type Performable (ConfigsT m) = ConfigsT (Performable m)
94+ performEvent e = ConfigsT $ ReaderT $ \configs ->
95+ performEvent $ runConfigsT configs <$> e
96+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
97+ performEvent_ $ runConfigsT configs <$> e
98+
99+instance Adjustable t m => Adjustable t (ConfigsT m) where
100+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
101+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
102+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
103+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
104+
105+instance Prerender js t m => Prerender js t (ConfigsT m) where
106+ type Client (ConfigsT m) = ConfigsT (Client m)
107+ prerender server client = ConfigsT $ ReaderT $ \configs ->
108+ prerender (runConfigsT configs server) (runConfigsT configs client)
109+
110+instance PrimMonad m => PrimMonad (ConfigsT m) where
111+ type PrimState (ConfigsT m) = PrimState m
112+ primitive = lift . primitive
113+
114+runConfigsT
115+ :: Map Text ByteString
116+ -> ConfigsT m a
117+ -> m a
118+runConfigsT cs child = runReaderT (unConfigsT child) cs
119+
120+mapConfigsT
121+ :: (forall x. m x -> n x)
122+ -> ConfigsT m a
123+ -> ConfigsT n a
124+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabal+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+176−91
@@ -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,176 @@ 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+ )
87+
88+type PrebuildAgnostic t route m =
89+ ( SetRoute t route m
6890 , RouteToUrl route m
91+ , MonadFix m
92+ , HasConfigs m
93+ , HasConfigs (Performable m)
6994 )
7095
7196data 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 ())
97+ { _frontend_head :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
98+ , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
7499 }
75100
76−type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
101+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
102+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
77103
78−-- | A widget that isn't attached to any particular part of the DOM hierarchy
79−type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
104+removeHTMLConfigs :: JSM ()
105+removeHTMLConfigs = void $ runMaybeT $ do
106+ doc <- MaybeT currentDocument
107+ hd <- MaybeT $ getHead doc
108+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109+ for_ es $ removeChild_ hd
80110
81−type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
111+-- | Collect all nodes in the node list.
112+--
113+-- TODO: this and the version in exe-config/ghcjs/lookup should be
114+-- upstreamed to jsaddle.
115+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
116+nodeListNodes es = do
117+ len <- getLength es
118+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
119+ -- accidentally traversing over [0..maxBound::Word]
120+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
121+ pure $ catMaybes nodes
82122
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
123+setInitialRoute :: Bool -> JSM ()
124+setInitialRoute useHash = do
125+ traceM "setInitialRoute"
126+ window <- DOM.currentWindowUnchecked
127+ initialLocation <- DOM.getLocation window
128+ initialUri <- getLocationUri initialLocation
129+ history <- DOM.getHistory window
130+ DOM.replaceState history jsNull ("" :: Text) $ Just $
131+ show $ setAdaptedUriPath useHash "/" initialUri
94132
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
133+data FrontendMode = FrontendMode
134+ { _frontendMode_hydrate :: Bool
135+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
136+ -- than building new DOM
137+ , _frontendMode_adjustRoute :: Bool
138+ -- ^ The page can't use regular routes, so encode routes into the hash
139+ -- instead
140+ }
132141
133−runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
142+-- | Run the frontend, setting the initial route to "/" on platforms where no
143+-- route exists ambiently in the context (e.g. anything but web).
144+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
145+runFrontend
146+ :: forall backendRoute route
147+ . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
148+ -> Frontend (R route)
149+ -> JSM ()
134150runFrontend validFullEncoder frontend = do
151+ let mode = FrontendMode
152+ { _frontendMode_hydrate =
153+#ifdef ghcjs_HOST_OS
154+ True
155+#else
156+ False
157+#endif
158+ , _frontendMode_adjustRoute =
159+#ifdef ghcjs_HOST_OS
160+ False
161+#else
162+ True
163+#endif
164+ }
165+ configs <- liftIO Lookup.getConfigs
166+ when (_frontendMode_hydrate mode) removeHTMLConfigs
167+ -- There's no fundamental reason that adjustRoute needs to control setting the
168+ -- initial route and *also* the useHash parameter; that's why these are
169+ -- separate here. However, currently, they are always the same.
170+ when (_frontendMode_adjustRoute mode) $ do
171+ setInitialRoute $ _frontendMode_adjustRoute mode
172+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
173+
174+runFrontendWithConfigsAndCurrentRoute
175+ :: forall backendRoute route
176+ . FrontendMode
177+ -> Map Text ByteString
178+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
179+ -> Frontend (R route)
180+ -> JSM ()
181+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
135182 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
136183 errorLeft = \case
137184 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
138185 Right x -> Identity x
139− runMyRouteViewT
140− :: ( TriggerEvent t m
141− , PerformEvent t m
142− , MonadHold t m
186+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
187+ , Ref (Performable m) ~ Ref IO
188+ , Ref m ~ Ref IO
189+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
190+ , MonadHold DomTimeline m
191+ , MonadRef m
192+ , MonadRef (Performable m)
193+ , MonadReflexCreateTrigger DomTimeline m
194+ , PerformEvent DomTimeline m
195+ , PostBuild DomTimeline m
196+ , PrimMonad m
197+ , MonadSample DomTimeline (Performable m)
143198 , DOM.MonadJSM m
144− , DOM.MonadJSM (Performable m)
145− , MonadFix m
199+ , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
200+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
146201 , MonadFix (Performable m)
202+ , MonadFix m
203+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
204+ , MonadIO (Performable m)
147205 )
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
206+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
207+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
208+ -> FloatingWidget () ()
209+ w appendHead appendBody = do
210+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
211+ (switchover'', fire) <- newTriggerEvent
212+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
213+ -- The order here is important - baseTag has to be before headWidget!
214+ baseTag
215+ _frontend_head frontend
216+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
217+ _frontend_body frontend
218+ switchover' <- case _frontendMode_hydrate mode of
219+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
220+ False -> getPostBuild
221+ performEvent_ $ liftIO (fire ()) <$ switchover'
222+ pure switchover''
223+ pure ()
224+ if _frontendMode_hydrate mode
225+ then runHydrationWidgetWithHeadAndBody (pure ()) w
226+ else runImmediateWidgetWithHeadAndBody w
154227
155228renderFrontendHtml
156− :: (t ~ DomTimeline)
157− => (r' -> Text)
229+ :: ( t ~ DomTimeline
230+ , MonadIO m
231+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
232+ )
233+ => Map Text ByteString
234+ -> Cookies
235+ -> (r -> Text)
158236 -> 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
237+ -> Frontend r
238+ -> widget ()
239+ -> widget ()
240+ -> m ByteString
241+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
163242 --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) $
243+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
165244 el "html" $ do
166− el "head" headWidget
167− el "body" bodyWidget
245+ el "head" $ do
246+ baseTag
247+ injectExecutableConfigs configs
248+ _frontend_head frontend
249+ headExtra
250+ el "body" $ do
251+ _frontend_body frontend
252+ bodyExtra
168253 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+162−32
@@ -13,12 +13,14 @@
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
1515{-# LANGUAGE TemplateHaskell #-}
16+{-# LANGUAGE TypeApplications #-}
1617{-# LANGUAGE TypeFamilies #-}
1718{-# LANGUAGE TypeOperators #-}
1819{-# LANGUAGE ViewPatterns #-}
1920module Obelisk.Route
2021 ( R
2122 , pattern (:/)
23+ , hoistR
2224 , PageName
2325 , PathQuery
2426 , Encoder
@@ -78,6 +80,17 @@ module Obelisk.Route
7880 , void1Encoder
7981 , pathSegmentsTextEncoder
8082 , queryParametersTextEncoder
83+ , renderObeliskRoute
84+ , renderBackendRoute
85+ , renderFrontendRoute
86+ , readShowEncoder
87+ , integralEncoder
88+ , pathSegmentEncoder
89+ , queryOnlyEncoder
90+ , Decoder(..)
91+ , dmapEncoder
92+ , fieldMapEncoder
93+ , jsonEncoder
8194 ) where
8295
8396import Prelude hiding ((.), id)
@@ -90,6 +103,7 @@ import Control.Category.Associative
90103import Control.Category.Monoidal
91104import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
92105import Control.Monad.Except
106+import qualified Data.ByteString.Lazy as BSL
93107import Data.Dependent.Sum (DSum (..))
94108import Data.Dependent.Map (DMap)
95109import qualified Data.Dependent.Map as DMap
@@ -106,16 +120,19 @@ import Data.Maybe
106120import Data.Monoid ((<>))
107121import Data.Set (Set)
108122import qualified Data.Set as Set
109−import Data.Some (Some)
110−import qualified Data.Some as Some
111−import Data.Some.Universe.Orphans ()
123+import Data.Some (Some(Some))
112124import Data.Text (Text)
113125import qualified Data.Text as T
114−import Data.Text.Encoding
126+import qualified Data.Text.Encoding as T
115127import Data.Universe
128+import Data.Universe.Some
116129import Network.HTTP.Types.URI
130+import qualified Numeric.Lens
117131import Obelisk.Route.TH
118132import Text.Read (readMaybe)
133+import Data.Tabulation
134+import qualified Data.Aeson as Aeson
135+import Data.Aeson (FromJSON, ToJSON)
119136
120137-- Design goals:
121138-- No start-up time on the frontend (not yet met)
@@ -141,13 +158,16 @@ import Text.Read (readMaybe)
141158
142159type R f = DSum f Identity --TODO: Better name
143160
144−--TODO: COMPLETE pragma
161+{-# COMPLETE (:/) #-}
145162infixr 5 :/
146163pattern (:/) :: f a -> a -> R f
147164pattern a :/ b = a :=> Identity b
148165
149166mapSome :: (forall a. f a -> g a) -> Some f -> Some g
150−mapSome f (Some.This a) = Some.This $ f a
167+mapSome f (Some a) = Some $ f a
168+
169+hoistR :: (forall x. f x -> g x) -> R f -> R g
170+hoistR f (x :=> Identity y) = f x :/ y
151171
152172--------------------------------------------------------------------------------
153173-- Encoder fundamentals
@@ -182,7 +202,7 @@ decode e x = runIdentity (tryDecode e x)
182202tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
183203tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
184204
185−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
205+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
186206-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
187207-- one should usually be applying decode and encode to the same 'Encoder'
188208encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -332,14 +352,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
332352justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
333353justEncoder = prismEncoder _Just
334354
335−-- |
355+-- | Encode () to 'Nothing'.
336356nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
337357nothingEncoder = prismEncoder _Nothing
338358
339359someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
340360someConstEncoder = unsafeMkEncoder $ EncoderImpl
341− { _encoderImpl_encode = \(Some.This (Const a)) -> a
342− , _encoderImpl_decode = pure . Some.This . Const
361+ { _encoderImpl_encode = \(Some (Const a)) -> a
362+ , _encoderImpl_decode = pure . Some . Const
343363 }
344364
345365-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -367,7 +387,7 @@ checkEnum1EncoderFunc
367387 -> check (EncoderFunc check' parse p r)
368388checkEnum1EncoderFunc f = do
369389 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
370− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
390+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
371391 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
372392 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
373393
@@ -422,11 +442,11 @@ chainEncoder cons this rest = Encoder $ do
422442 pure $ EncoderImpl
423443 { _encoderImpl_decode = \v -> do
424444 (here, following) <- _encoderImpl_decode consValid v
425− Some.This r <- _encoderImpl_decode thisValid here
445+ Some r <- _encoderImpl_decode thisValid here
426446 (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
427447 , _encoderImpl_encode = \(r :/ s) ->
428448 _encoderImpl_encode consValid
429− ( _encoderImpl_encode thisValid $ Some.This r
449+ ( _encoderImpl_encode thisValid $ Some r
430450 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
431451 }
432452
@@ -493,7 +513,7 @@ enum1Encoder
493513 , Show r
494514 )
495515 => (forall a. p a -> r) -> Encoder check parse (Some p) r
496−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
516+enum1Encoder f = enumEncoder $ \(Some p) -> f p
497517
498518-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
499519-- have a large number of values - it will use a lot of memory.
@@ -536,13 +556,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
536556 }
537557
538558pathOnlyEncoder :: (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− }
559+pathOnlyEncoder = second (unitEncoder mempty) . coidr
560+
561+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
562+queryOnlyEncoder = first (unitEncoder []) . coidl
546563
547564singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
548565singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -585,10 +602,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
585602 in (urlDecodeText True k, urlDecodeText True <$> mv)
586603
587604urlEncodeText :: Bool -> Text -> Text
588−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
605+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
589606
590607urlDecodeText :: Bool -> Text -> Text
591−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
608+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
592609
593610listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
594611listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -700,9 +717,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
700717 ObeliskRoute_App :: f a -> ObeliskRoute f a
701718 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
702719
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
720+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
721+ universeSome = concat
722+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
723+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
724+ ]
706725
707726instance GEq f => GEq (ObeliskRoute f) where
708727 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -790,12 +809,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
790809
791810someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
792811someSumEncoder = 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
812+ { _encoderImpl_encode = \(Some t) -> case t of
813+ InL l -> Left $ Some l
814+ InR r -> Right $ Some r
796815 , _encoderImpl_decode = pure . \case
797− Left (Some.This l) -> Some.This (InL l)
798− Right (Some.This r) -> Some.This (InR r)
816+ Left (Some l) -> Some (InL l)
817+ Right (Some r) -> Some (InR r)
799818 }
800819
801820data Void1 :: * -> * where {}
@@ -806,7 +825,7 @@ instance Universe (Some Void1) where
806825void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
807826void1Encoder = Encoder $ pure $ EncoderImpl
808827 { _encoderImpl_encode = \case
809− Some.This f -> case f of {}
828+ Some f -> case f of {}
810829 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
811830 }
812831
@@ -824,4 +843,115 @@ makePrisms ''ObeliskRoute
824843deriveGEq ''Void1
825844deriveGCompare ''Void1
826845
846+-- | Given a backend route and a checked route encoder, render the route (path
847+-- and query string). See 'checkEncoder' for how to produce a checked encoder.
848+renderBackendRoute
849+ :: forall br a.
850+ Encoder Identity Identity (R (Sum br a)) PageName
851+ -> R br
852+ -> Text
853+renderBackendRoute enc = renderObeliskRoute enc . hoistR InL
854+
855+-- | Renders a frontend route with the supplied checked encoder
856+renderFrontendRoute
857+ :: forall a fr.
858+ Encoder Identity Identity (R (Sum a (ObeliskRoute fr))) PageName
859+ -> R fr
860+ -> Text
861+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (InR . ObeliskRoute_App)
862+
863+-- | Renders a route of the form typically found in an Obelisk project
864+renderObeliskRoute
865+ :: forall a b.
866+ Encoder Identity Identity (R (Sum a b)) PageName
867+ -> R (Sum a b)
868+ -> Text
869+renderObeliskRoute e r =
870+ let enc :: Encoder Identity (Either Text) (R (Sum a b)) PathQuery
871+ enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
872+ in (T.pack . uncurry (<>)) $ encode enc r
873+
874+readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
875+readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
876+
877+integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
878+integralEncoder = prismEncoder (Numeric.Lens.integral)
879+
880+pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
881+ Encoder check parse (Text, PageName) PageName
882+pathSegmentEncoder = unsafeMkEncoder EncoderImpl
883+ { _encoderImpl_encode = \(x, (y, z)) -> (x:y, z)
884+ , _encoderImpl_decode = \(xss, y) -> case xss of
885+ [] -> throwError "not enough path segments"
886+ (x:xs) -> pure (x, (xs, y))
887+ }
888+
889+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
890+
891+dmapEncoder :: forall check parse k k' v.
892+ ( Monad check
893+ , MonadError Text parse
894+ , Universe (Some k')
895+ , Ord k
896+ , GCompare k'
897+ , GShow k'
898+ )
899+ => Encoder check parse (Some k') k
900+ -> (forall v'. k' v' -> Encoder check parse v' v)
901+ -> Encoder check parse (DMap k' Identity) (Map k v)
902+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
903+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
904+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
905+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
906+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
907+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
908+ return $ EncoderImpl
909+ { _encoderImpl_encode = \dm -> Map.fromList $ do
910+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
911+ return ( encode keyEncoder (Some k')
912+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
913+ )
914+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
915+ Some (k' :: k' t) <- tryDecode keyEncoder k
916+ case DMap.lookup k' valueDecoders of
917+ Nothing -> throwError . T.pack . keyError $ gshow k'
918+ Just (Decoder e) -> do
919+ v' <- tryDecode e v
920+ return (k' :=> Identity v')
921+ }
922+
923+fieldMapEncoder :: forall check parse r.
924+ ( Applicative check
925+ , MonadError Text parse
926+ , HasFields r
927+ , Universe (Some (Field r))
928+ , GShow (Field r)
929+ , GCompare (Field r)
930+ )
931+ => Encoder check parse r (DMap (Field r) Identity)
932+fieldMapEncoder = unsafeEncoder $ do
933+ pure $ EncoderImpl
934+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
935+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
936+ case DMap.lookup f dm of
937+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
938+ Just (Identity v) -> return v
939+ }
940+
941+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
942+jsonEncoder :: forall check parse r.
943+ ( ToJSON r
944+ , FromJSON r
945+ , Applicative check
946+ , MonadError Text parse
947+ )
948+ => Encoder check parse r Text
949+jsonEncoder = unsafeEncoder $ do
950+ pure $ EncoderImpl
951+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
952+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
953+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
954+ Right x -> return x
955+ }
956+
827957--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+129−38
@@ -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,10 +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
182+instance HasConfigs m => HasConfigs (RoutedT t r m)
183+
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)
191+
164192runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
165193runRoutedT = runReaderT . unRoutedT
166194
167−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
168196mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
169197
170198withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
@@ -194,6 +222,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
194222 runRoutedT (f c) r'
195223-}
196224
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+
197239dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
198240dsumValueCoercion Coercion = Coercion
199241
@@ -210,6 +252,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
210252 d' <- maybeDyn d
211253 runRoutedT r d'
212254
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+
213258-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
214259strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
215260strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -224,7 +269,7 @@ strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
224269 pure ()
225270
226271newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
227− 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)
228273
229274instance (MonadFix m, MonadHold t m, DomBuilder t m) => DomBuilder t (SetRouteT t r m) where
230275 type DomBuilderSpace (SetRouteT t r m) = DomBuilderSpace m
@@ -246,16 +291,26 @@ runSetRouteT = runEventWriterT . unSetRouteT
246291class Reflex t => SetRoute t r m | m -> t r where
247292 setRoute :: Event t r -> m ()
248293 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+
249297 setRoute = modifyRoute . fmap const
250298
251299instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
252300 modifyRoute = SetRouteT . tellEvent . fmap Endo
253301
254−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
255− modifyRoute = lift . modifyRoute
302+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
256303
257−instance Prerender js m => Prerender js (SetRouteT t r m) where
258− 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
259314
260315instance Requester t m => Requester t (SetRouteT t r m) where
261316 type Request (SetRouteT t r m) = Request m
@@ -263,6 +318,8 @@ instance Requester t m => Requester t (SetRouteT t r m) where
263318 requesting = SetRouteT . requesting
264319 requesting_ = SetRouteT . requesting_
265320
321+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
322+
266323#ifndef ghcjs_HOST_OS
267324deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
268325#endif
@@ -286,6 +343,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
286343 type PrimState (SetRouteT t r m) = PrimState m
287344 primitive = lift . primitive
288345
346+instance HasConfigs m => HasConfigs (SetRouteT t r m)
347+
289348instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
290349 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
291350 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -299,9 +358,11 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
299358
300359class RouteToUrl r m | m -> r where
301360 askRouteToUrl :: m (r -> Text)
361+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
362+ askRouteToUrl = lift askRouteToUrl
302363
303364newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
304− 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)
305366
306367runRouteToUrlT
307368 :: RouteToUrlT r m a
@@ -316,17 +377,22 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
316377 askRouteToUrl = RouteToUrlT ask
317378
318379instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
319− askRouteToUrl = lift askRouteToUrl
320380
321381instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
322− 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)
323386
324387instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
325388 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
326389 askJSContext = lift askJSContext
327390
328−instance Prerender js m => Prerender js (RouteToUrlT r m) where
329− 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)
330396
331397instance Requester t m => Requester t (RouteToUrlT r m) where
332398 type Request (RouteToUrlT r m) = Request m
@@ -376,6 +442,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
376442 askQueryResult = lift askQueryResult
377443 queryIncremental = lift . queryIncremental
378444
445+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
446+
379447runRouteViewT
380448 :: forall t m r a.
381449 ( TriggerEvent t m
@@ -386,16 +454,20 @@ runRouteViewT
386454 , MonadFix m
387455 )
388456 => (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
389461 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
390462 -> m a
391−runRouteViewT routeEncoder a = do
392− rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
463+runRouteViewT routeEncoder switchover useHash a = do
464+ rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
393465 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
394466 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
395467 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
396468 -- this ever to happen, we'll just handle it by failing completely with 'error'.
397469 route :: Dynamic t r
398− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
470+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
399471 where
400472 errorLeft (Left e) = error (T.unpack e)
401473 errorLeft (Right x) = x
@@ -416,7 +488,7 @@ runRouteViewT routeEncoder a = do
416488 -- we can change this function later to accommodate.
417489 -- See: https://github.com/whatwg/html/issues/2174
418490 , _historyStateUpdate_title = ""
419− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
491+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
420492 { uriQuery = newQuery
421493 }
422494 }
@@ -443,25 +515,44 @@ routeLink r w = do
443515 setRoute $ r <$ domEvent Click e
444516 return a
445517
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+
446541-- On ios due to sandboxing when loading the page from a file adapt the
447542-- path to be based on the hash.
448543
449−adaptedUriPath :: URI -> String
450−#if defined(ios_HOST_OS)
451−adaptedUriPath = hashToPath . uriFragment
544+adaptedUriPath :: Bool -> URI -> String
545+adaptedUriPath = \case
546+ True -> hashToPath . uriFragment
547+ False -> uriPath
452548
453−hashToPath :: String -> String
454−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
455−#else
456−adaptedUriPath = uriPath
457−#endif
458−
459−setAdaptedUriPath :: String -> URI -> URI
460−#if defined(ios_HOST_OS)
461−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 }
462553
463554pathToHash :: String -> String
464555pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
465−#else
466−setAdaptedUriPath s u = u { uriPath = s }
467−#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+58−38
@@ -1,17 +1,12 @@
11{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyCase #-}
3−{-# LANGUAGE EmptyDataDecls #-}
42{-# LANGUAGE FlexibleContexts #-}
53{-# LANGUAGE FlexibleInstances #-}
64{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE KindSignatures #-}
85{-# LANGUAGE LambdaCase #-}
96{-# LANGUAGE MultiParamTypeClasses #-}
107{-# LANGUAGE OverloadedStrings #-}
118{-# LANGUAGE Rank2Types #-}
12−{-# LANGUAGE RecursiveDo #-}
139{-# LANGUAGE ScopedTypeVariables #-}
14−{-# LANGUAGE TemplateHaskell #-}
1510{-# LANGUAGE TypeFamilies #-}
1611{-# LANGUAGE UndecidableInstances #-}
1712{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
@@ -31,11 +26,15 @@ import Data.Dependent.Sum (DSum (..))
3126import Data.Functor.Identity
3227import Data.Functor.Sum
3328import Data.List (uncons)
29+import Data.Map (Map)
30+import qualified Data.Map as Map
3431import Data.Maybe
3532import Data.Semigroup ((<>))
3633import Data.Streaming.Network (bindPortTCP)
34+import Data.String (fromString)
3735import Data.Text (Text)
3836import qualified Data.Text as T
37+import qualified Data.Text.Encoding as T
3938import Language.Javascript.JSaddle.Run (syncPoint)
4039import Language.Javascript.JSaddle.WebSockets
4140import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -49,8 +48,7 @@ import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4948import Network.WebSockets (ConnectionOptions)
5049import Network.WebSockets.Connection (defaultConnectionOptions)
5150import qualified Obelisk.Asset.Serve.Snap as Snap
52−import Obelisk.ExecutableConfig (get)
53−import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
51+import Obelisk.Backend
5452import Obelisk.Frontend
5553import Obelisk.Route.Frontend
5654import Reflex.Dom.Core
@@ -58,10 +56,11 @@ import Snap.Core (Snap)
5856import System.Environment
5957import System.IO
6058import System.Process
59+import System.Exit (ExitCode(..))
6160import Text.URI (URI)
6261import qualified Text.URI as URI
6362import Text.URI.Lens
64−import Obelisk.Backend
63+import Web.Cookie
6564
6665run
6766 :: Int -- ^ Port to run the backend
@@ -76,6 +75,7 @@ run port serveStaticAsset backend frontend = do
7675 case checkEncoder $ _backend_routeEncoder backend of
7776 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7877 Right validFullEncoder -> do
78+ publicConfigs <- getPublicConfigs
7979 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
8080 _backend_run backend $ \serveRoute -> do
8181 runSnapWithCommandLineArgs $ do
@@ -83,29 +83,31 @@ run port serveStaticAsset backend frontend = do
8383 Identity r -> case r of
8484 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
8585 InR obeliskRoute :=> Identity a ->
86− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
86+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
8787 let conf = defRunConfig { _runConfig_redirectPort = port }
88− runWidget conf frontend validFullEncoder `finally` killThread backendTid
88+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
8989
9090-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
9191runServeAsset :: FilePath -> [Text] -> Snap ()
9292runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
9393
94−getConfigRoute :: IO (Maybe URI)
95−getConfigRoute = get "config/common/route" >>= \case
96− Just r -> case URI.mkURI $ T.strip r of
97− Just route -> pure $ Just route
98− Nothing -> do
99− putStrLn $ "Route is invalid: " <> show r
100− pure Nothing
101− Nothing -> pure Nothing
102−
103−defAppUri :: URI
104−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
105−
106−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
107−runWidget conf frontend validFullEncoder = do
108− uri <- fromMaybe defAppUri <$> getConfigRoute
94+getConfigRoute :: Map Text ByteString -> Either Text URI
95+getConfigRoute configs = case Map.lookup "common/route" configs of
96+ Just r ->
97+ let stripped = T.strip (T.decodeUtf8 r)
98+ in case URI.mkURI stripped of
99+ Just route -> Right route
100+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
101+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
102+
103+runWidget
104+ :: RunConfig
105+ -> Map Text ByteString
106+ -> Frontend (R route)
107+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
108+ -> IO ()
109+runWidget conf configs frontend validFullEncoder = do
110+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
109111 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
110112 redirectHost = _runConfig_redirectHost conf
111113 redirectPort = _runConfig_redirectPort conf
@@ -117,19 +119,25 @@ runWidget conf frontend validFullEncoder = do
117119 close
118120 (\skt -> do
119121 man <- newManager defaultManagerSettings
120− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
121123 runSettingsSocket settings skt app)
122124
123125obeliskApp
124− :: forall route backendRoute. ConnectionOptions
126+ :: forall route backendRoute
127+ . Map Text ByteString
128+ -> ConnectionOptions
125129 -> Frontend (R route)
126130 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
127131 -> URI
128132 -> Application
129133 -> IO Application
130−obeliskApp opts frontend validFullEncoder uri backend = do
131− let entryPoint = do
132− runFrontend validFullEncoder frontend
134+obeliskApp configs opts frontend validFullEncoder uri backend = do
135+ let mode = FrontendMode
136+ { _frontendMode_hydrate = True
137+ , _frontendMode_adjustRoute = False
138+ }
139+ entryPoint = do
140+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
133141 syncPoint
134142 jsaddlePath <- URI.mkPathPiece "jsaddle"
135143 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
@@ -143,15 +151,22 @@ obeliskApp opts frontend validFullEncoder uri backend = do
143151 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
144152 }
145153 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
146− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
154+ let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
155+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
147156 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
148157 _ -> backend req sendResponse
149158
150−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
151−renderJsaddleFrontend urlEnc r f =
159+renderJsaddleFrontend
160+ :: Map Text ByteString
161+ -> Cookies
162+ -> (route -> Text)
163+ -> route
164+ -> Frontend route
165+ -> IO ByteString
166+renderJsaddleFrontend configs cookies urlEnc r f =
152167 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
153168 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
154− in renderFrontendHtml urlEnc r (_frontend_head f >> injectExecutableConfigs >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
169+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
155170
156171-- | like 'bindPortTCP' but reconnects on exception
157172bindPortTCPRetry :: Settings
@@ -170,10 +185,15 @@ logPortBindErr p e = getProcessIdForPort p >>= \case
170185
171186getProcessIdForPort :: Int -> IO (Maybe Int)
172187getProcessIdForPort port = do
173− xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
174− case uncons xs of
175− Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
176− _ -> return Nothing
188+ -- First check if 'ss' is available
189+ (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
190+ case c of
191+ ExitSuccess -> do
192+ xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
193+ case uncons xs of
194+ Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
195+ _ -> return Nothing
196+ _ -> return Nothing
177197
178198parseSsPid :: A.Parser Int
179199parseSsPid = do
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+33−12
@@ -22,7 +22,7 @@ import qualified Network.HTTP.Client as HTTP
2222import qualified Network.HTTP.Types as HTTP
2323import qualified Network.Socket as Socket
2424import Shelly
25−import System.Directory (withCurrentDirectory)
25+import System.Directory (withCurrentDirectory, getDirectoryContents)
2626import System.Environment
2727import System.Exit (ExitCode (..))
2828import System.Info
@@ -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+ configs <- getConfigs
133+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
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,22 +142,22 @@ 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]
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]
144148 -- See https://github.com/obsidiansystems/obelisk/issues/101
145− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
149+ -- it "can build everything" $ shelly_ $ nixBuild [obeliskImpl]
146150
147151 describe "blank initialized project" $ parallel $ do
148152
149153 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
150− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
154+ nixBuild ["-A", "ghc.backend"]
151155 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
152− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
156+ nixBuild ["-A", "ghcjs.frontend"]
153157
154158 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"]
159+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
160+ else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
157161
158162 forM_ ["ghc", "ghcjs"] $ \compiler -> do
159163 let
@@ -163,7 +167,7 @@ main = do
163167 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
164168
165169 it "can build reflex project" $ inTmpObInit $ \_ -> do
166− run "nix-build" []
170+ nixBuild []
167171
168172 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
169173 u <- update
@@ -213,6 +217,23 @@ main = do
213217 void $ unpack
214218 testThunkPack (dir </> thunk)
215219
220+ describe "ob thunk update --branch" $ parallel $ do
221+ it "can change a thunk to the latest version of a desired branch" $ withTmp $ \dir -> do
222+ let branch1 = "master"
223+ branch2 = "develop"
224+ run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", branch1]
225+ run_ "ob" ["thunk" , "pack", toTextIgnore dir]
226+ run_ "ob" ["thunk", "update", toTextIgnore dir, "--branch", branch2]
227+
228+ it "doesn't create anything when given an invalid branch" $ withTmp $ \dir -> do
229+ let checkDir dir' = liftIO $ getDirectoryContents $ T.unpack $ toTextIgnore dir'
230+ run_ "git" ["clone", "https://github.com/reflex-frp/reflex.git", toTextIgnore dir, "--branch", "master"]
231+ run_ "ob" ["thunk" , "pack", toTextIgnore dir]
232+ startingContents <- checkDir dir
233+ void $ errExit False $ run "ob" ["thunk", "update", toTextIgnore dir, "--branch", "dumble-palooza"]
234+ checkDir dir >>= liftIO . assertEqual "" startingContents
235+
236+
216237-- | Run `ob run` in the given directory (maximum of one level deep)
217238testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
218239testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
release.nix+17−6
@@ -1,4 +1,5 @@
1−{ local-self ? import ./. {}
1+{ self-args ? {}
2+, local-self ? import ./. self-args
23}:
34
45let
@@ -9,7 +10,7 @@ let
910 obeliskPackagesCommon = [
1011 "obelisk-frontend"
1112 "obelisk-route"
12− "obelisk-executable-config"
13+ "obelisk-executable-config-lookup"
1314 ];
1415
1516 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -59,17 +60,27 @@ let
5960 (concatDepends ghcjs)
6061 ];
6162 command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63− builtSkeleton = (import ./skeleton {}).all;
63+ serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
64+ androidObelisk = import ./. (self-args // {
65+ config.android_sdk.accept_license = true;
66+ });
67+ androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
68+ iosObelisk = import ./. (self-args // {
69+ system = "x86_64-darwin";
70+ iosSdkVersion = "10.2";
71+ });
72+ iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
6473 in {
6574 inherit
6675 command
6776 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
77+ serverExeSkeleton
78+ iosSkeleton
79+ androidSkeleton;
6980 cache = reflex-platform.pinBuildInputs
7081 "obelisk-${system}"
7182 cachePackages
72− [command serverExeSkeleton builtSkeleton];
83+ [command serverExeSkeleton iosSkeleton androidSkeleton];
7384 });
7485
7586 metaCache = local-self.pinBuildInputs
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/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/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 }