on master help sign in
Merge remote-tracking branch 'origin/db-config-fixes' into cg-tabulation-nodevelop
38bef16Cale Gibbard
CONTRIBUTING.md+60−3
@@ -1,9 +1,66 @@
1−## Contributing to Obelisk
1+# Contribution Guide
2
2
3−Thanks for taking the time to contribute!
3+Contributions and issue reports are encouraged and appreciated!
4
4
5−Before submitting a pull request, please run the tests:
5+- [Opening Issues](#opening-issues)
6+- [Submitting Changes](#submitting-changes)
7+ - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Code Quality](#code-quality)
9+ - [Documentation](#documentation)
6
10
11+## Opening Issues
12+
13+Before opening an issue, please check whether your issue has already been reported. Assuming it has not:
14+
15+* Describe the issue you're encountering or the suggestion you're making
16+* Include any relevant steps to reproduce or code samples you can. It's always easier for us to debug if we have something that demonstrates the error.
17+* Let us know what version of this project you were using. If you're using a github checkout, provide the git hash.
18+
19+## Submitting Changes
20+
21+Most pull requests should target the `develop` branch. `master` is the release branch. `develop` is periodically merged into master after a period of testing.
22+
23+### Guidelines for Commit Messages
24+
25+#### Summary Line
26+The summary line of your commit message should summarize the changes being made. Commit messages should be written in the imperative mood and should describe what happens when the commit is applied. If your commit modifies one of the in-tree haskell packages (found in `./lib`), please prefix your commit summary with the name of the package being modified.
27+
28+One way to think about it is that your commit message should be able to complete the sentence:
29+"When applied, this commit will..."
30+
31+##### Note on bumping dependencies
32+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
33+
34+#### Body
35+For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
36+
37+### Code Quality
38+
39+#### Warnings
40+
41+Your pull request should add no new warnings to the project. It should also generally not disable any warnings.
42+
43+#### Build and Test
44+
45+Make sure the project builds and that the tests pass! This will generally also be checked by CI before merge, but trying it yourself first means you'll catch problems earlier and your contribution can be merged that much sooner!
46+
47+You can run the tests like this:
7```bash
48```bash
8$(nix-build -A selftest --no-out-link)
49$(nix-build -A selftest --no-out-link)
9```
50```
51+
52+### Documentation
53+
54+#### In the code
55+We're always striving to improve documentation. Please include [haddock](https://haskell-haddock.readthedocs.io/en/latest/index.html) documentation for any added code, and update the documentation for any code you modify.
56+
57+#### In the [Changelog](ChangeLog.md)
58+Add an entry to the changelog when your PR:
59+* Adds a feature
60+* Deprecates something
61+* Includes a breaking change
62+* Makes any other change that will impact users
63+
64+#### In the [Readme](README.md)
65+The readme is the first place a lot of people look for information about the repository. Update any parts of the readme that are affected by your PR.
66+
ChangeLog.mdnew file+24
@@ -0,0 +1,24 @@
1+# Revision history for obelisk
2+
3+This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
4+
5+## Unreleased
6+
7+
8+## v0.1.1.0 - 2019-05-17
9+
10+* Fix crashes of Android apps on 32-bit ARM devices.
11+* Provide a way to indicate acceptance of the Android SDK license by passing `config.android_sdk.accept_license = true;` in the arguments to the import of `.obelisk/impl` in the project's `default.nix`.
12+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
13+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
14+* Add the `ob shell` command to enter a nix shell for an obelisk project
15+* Allow skeleton's obelisk to be overridden. This changes the skeleton's default.nix interface: the arguments that it used to take are now part of the new "obelisk" argument.
16+* Removed `MonadIO` from `ObeliskWidget` to prevent accidental IO during prerendering. If you need to do IO in a widget it should be on the right hand side of a `prerender`.
17+* Significantly changed the interface to the "executable config" packages. `obelisk-executable-config-lookup` is a new internal package which looks up all configs in a platform-specific way. `obelisk-executable-frontend` and `obelisk-executable-backend` provide MTL-style monad classes (`HasFrontendConfigs` and `HasBackendConfigs`) which the frontend and backend, respectively, can use to look up configs. This replaces the old `get` function which ran in `IO`.
18+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
19+
20+
21+## v0.1.0.0 - 2019-03-29
22+
23+* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
24+* Add `HasCookies` and `CookiesT` to allow `ObeliskWidget`s to access cookies during static and "hydrated" DOM rendering.
FAQ.md+76
@@ -2,6 +2,8 @@
2
2
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
31. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
41. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
41. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
5+1. [How do I fix `Ambiguous module name` errors?](#how-do-i-fix-ambiguous-module-name-errors)
6+1. [What does `ob run` actually do?](#what-does-ob-run-actually-do)
5
7
6### How do I fix invalid entitlements?
8### How do I fix invalid entitlements?
7
9
@@ -44,3 +46,77 @@ ob thunk pack $SOME_THUNK
44Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
46Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
45
47
46(Based on issue #351).
48(Based on issue #351).
49+
50+### How do I fix `Ambiguous module name` errors?
51+Since obelisk places the common/backend/frontend modules packages into the same ghci, and ghci doesn't "sandbox" them, it is possible to have conflicting module errors inside `ob repl/watch/run` that do not appear when doing a cabal build.
52+
53+You can disambiguate this via [PackageImports](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#package-qualified-imports). For instance, if you see
54+```
55+error:
56+ Ambiguous module name ‘Crypto.Hash’:
57+ it was found in multiple packages:
58+ cryptohash-0.11.9 cryptonite-0.25
59+```
60+then specify the package you want in the import, e.g:
61+`import "cryptonite" Crypto.Hash`
62+
63+
64+### What does `ob run` actually do?
65+
66+
67+#### Short version:
68+
69+`ob run` starts a [`ghcid`](https://github.com/ndmitchell/ghcid) process which
70+tries to build your project within a carefully crafted `nix-shell` with all the
71+project's dependencies and,
72+
73+* either displays compilation errors/warnings,
74+* or starts the Obelisk server, which serves:
75+ * your backend's route handlers,
76+ * the static assets,
77+ * the [JSaddle](https://github.com/ghcjs/jsaddle) frontend code (while opening its websocket).
78+
79+#### Longer version:
80+
81+Assuming we are in a project created with `ob init`, `ob run` calls (see
82+`lib/command/src/Obelisk/Command.hs`):
83+
84+ nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
85+
86+where
87+
88+* `shells.ghc` is defined in `./default.nix` by importing `./.obelisk/impl/default.nix` which
89+ is `default.nix` in the present (Obelisk) repository,
90+* `run-static-io` is a logging-enabled command wrapper
91+ (cf. `runObelisk` in `lib/command/src/Obelisk/App.hs`).
92+
93+In this case, it runs the function `Obelisk.Command.Command.run` (defined in
94+`lib/command/src/Obelisk/Command/Run.hs`).
95+
96+* It creates a GHCi config from a Nix expression which:
97+ * loads three packages: `backend`, `common`, `frontend`,
98+ * obtains a free port number.
99+* Then runs `ghcid`
100+ * with a command that reruns `Obelisk.Run.run` at each restart (option
101+ `--test`).
102+
103+It is defined in `lib/run/src/Obelisk/Run.hs`:
104+
105+* It creates a thread which starts the main backend.
106+ This runs `runSnapWithCommandLineArgs` and passes routes:
107+ * to the backend (result of the `_backend_run` field of your
108+ implementation of the `Backend fullRoute frontendRoute` record),
109+ * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110+ serve static assets.
111+* Starts `runWidget`
112+ which itself runs
113+ [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114+ (the *“TCP listen loop”*):
115+ * it binds to TCP socket, and
116+ * creates the HTTP connection manager
117+ ( [`newManager`](https://hackage.haskell.org/package/http-client-0.6.3/docs/Network-HTTP-Client.html#v:newManager))
118+ * calls `obeliskApp` which
119+ * has a route to serve the [JSaddle](https://github.com/ghcjs/jsaddle) frontend,
120+ * runs the frontend (*on* the server), to produce pre-rendered pages,
121+ * starts the JSaddle web-socket,
122+ * “falls back” to proxying the backend (`fallbackProxy`).
README.md+38−2
@@ -4,6 +4,8 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
4
4
5- [Installing Obelisk](#installing-obelisk)
5- [Installing Obelisk](#installing-obelisk)
6- [Developing an Obelisk project](#developing-an-obelisk-project)
6- [Developing an Obelisk project](#developing-an-obelisk-project)
7+ - [Hoogle](#hoogle)
8+ - [Adding Package Overrides](#adding-package-overrides)
7- [Deploying](#deploying)
9- [Deploying](#deploying)
8 - [Locally](#locally)
10 - [Locally](#locally)
9 - [EC2](#ec2)
11 - [EC2](#ec2)
@@ -22,6 +24,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
22 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
24 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
23 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
25 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
24 ```
26 ```
27+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
25 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
28 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)):
26 ```
29 ```
27 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
30 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
@@ -79,10 +82,40 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
79ob run
82ob run
80```
83```
81
84
82−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
85+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
83
86
84Every 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.
87Every 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.
85
88
89+### Hoogle
90+
91+To enter a nix-shell from which you can run the hoogle command-line client or a hoogle server for your project:
92+
93+`nix-shell -A shells.ghc --arg withHoogle true`
94+
95+### Adding package overrides
96+
97+To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
98+
99+```nix
100+# ...
101+project ./. ({ pkgs, ... }: {
102+# ...
103+ overrides = self: super: let
104+ aeson = pkgs.fetchFromGitHub {
105+ owner = "obsidiansystems";
106+ repo = "aeson-gadt-th";
107+ rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
108+ sha256 = "08q6rnz7w9pn76jkrafig6f50yd0f77z48rk2z5iyyl2jbhcbhx3";
109+ };
110+ in
111+ {
112+ aeson = self.callCabal2nix "aeson" aeson {};
113+ };
114+# ...
115+```
116+
117+For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
118+
86## Deploying
119## Deploying
87
120
88### Locally
121### Locally
@@ -106,7 +139,7 @@ In this section we will demonstrate how to deploy your Obelisk app to an Amazon
106
139
107First create a new EC2 instance:
140First create a new EC2 instance:
108
141
109−1. Launch a NixOS 17.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-40bee63a))
142+1. Launch a NixOS 18.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-009c9c3f1af480ff3))
1101. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1431. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1111. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1441. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
1121. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
1451. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
@@ -237,7 +270,10 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
237
270
238### Android
271### Android
239
272
273+NOTE: Currently Android builds are only supported on Linux.
274+
2401. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
2751. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
276+1. In your project's `default.nix` pass `config.android_sdk.accept_license = true;` in the arguments to the import of of `.obelisk/impl` to indicate your acceptance of the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms), which is required to build Android apps.
2411. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2771. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2421. A debug version of the app should be generated at `result-android/android-app-debug.apk`
2781. A debug version of the app should be generated at `result-android/android-app-debug.apk`
243
279
default.nix+25−19
@@ -1,7 +1,7 @@
1{ system ? builtins.currentSystem
1{ system ? builtins.currentSystem
2, profiling ? false
2, profiling ? false
3, iosSdkVersion ? "10.2"
3, iosSdkVersion ? "10.2"
4−, __useLegacyCompilers ? false
4+, config ? {}
5}:
5}:
6let
6let
7 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
7 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,9 +16,8 @@ let
16 openssh
16 openssh
17 ];
17 ];
18
18
19− getReflexPlatform = getReflexPlatform' __useLegacyCompilers;
20− getReflexPlatform' = __useLegacyCompilers: sys: import ./dep/reflex-platform {
19+ getReflexPlatform = sys: import ./dep/reflex-platform {
21− inherit iosSdkVersion __useLegacyCompilers;
20+ inherit iosSdkVersion config;
22 system = sys;
21 system = sys;
23 enableLibraryProfiling = profiling;
22 enableLibraryProfiling = profiling;
24
23
@@ -50,11 +49,12 @@ let
50 } + /template) {};
49 } + /template) {};
51 })
50 })
52
51
52+ pkgs.obeliskExecutableConfig.haskellOverlay
53+
53 # Add obelisk packages
54 # Add obelisk packages
54 (self: super: let
55 (self: super: let
55 pkgs = self.callPackage ({ pkgs }: pkgs) {};
56 pkgs = self.callPackage ({ pkgs }: pkgs) {};
56 in {
57 in {
57− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
58 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
58 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
59
59
60 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
60 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
@@ -113,7 +113,7 @@ let
113 ];
113 ];
114 };
114 };
115
115
116− reflex-platform = getReflexPlatform' false system;
116+ reflex-platform = getReflexPlatform system;
117 inherit (reflex-platform) hackGet nixpkgs;
117 inherit (reflex-platform) hackGet nixpkgs;
118 pkgs = nixpkgs;
118 pkgs = nixpkgs;
119
119
@@ -165,12 +165,16 @@ in rec {
165 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
165 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
166 '';
166 '';
167
167
168− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
168+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
169 mkdir $out
169 mkdir $out
170 cd $out
170 cd $out
171 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
171 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
172+ ${if optimizationLevel == null then ''
173+ ln -s all.unminified.js all.js
174+ '' else ''
172− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
175+ ${pkgs.closurecompiler}/bin/closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
173− echo "//# sourceMappingURL=all.js.map" >> all.js
176+ echo "//# sourceMappingURL=all.js.map" >> all.js
177+ ''}
174 '';
178 '';
175
179
176 serverModules = {
180 serverModules = {
@@ -276,7 +280,7 @@ in rec {
276 , tools ? _: []
280 , tools ? _: []
277 , shellToolOverrides ? _: _: {}
281 , shellToolOverrides ? _: _: {}
278 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
282 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
279− , __closureCompilerOptimizationLevel ? "ADVANCED"
283+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
280 }:
284 }:
281 let frontendName = "frontend";
285 let frontendName = "frontend";
282 backendName = "backend";
286 backendName = "backend";
@@ -301,20 +305,22 @@ in rec {
301 injectableConfig = builtins.filterSource (path: _:
305 injectableConfig = builtins.filterSource (path: _:
302 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
306 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
303 );
307 );
304− __android = configPath: {
308+ __androidWithConfig = configPath: {
305 ${if android == null then null else frontendName} = {
309 ${if android == null then null else frontendName} = {
306 executableName = "frontend";
310 executableName = "frontend";
307 ${if builtins.pathExists staticFiles then "assets" else null} =
311 ${if builtins.pathExists staticFiles then "assets" else null} =
308− (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.android.inject (injectableConfig configPath))
312+ nixpkgs.obeliskExecutableConfig.platforms.android.inject
313+ (injectableConfig configPath)
309− processedStatic.symlinked;
314+ processedStatic.symlinked;
310 } // android;
315 } // android;
311 };
316 };
312− __ios = configPath: {
317+ __iosWithConfig = configPath: {
313 ${if ios == null then null else frontendName} = {
318 ${if ios == null then null else frontendName} = {
314 executableName = "frontend";
319 executableName = "frontend";
315 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
320 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
316− (if configPath == null then lib.id else nixpkgs.obeliskExecutableConfig.platforms.ios.inject (injectableConfig configPath))
321+ nixpkgs.obeliskExecutableConfig.platforms.ios.inject
322+ (injectableConfig configPath)
317− processedStatic.symlinked;
323+ processedStatic.symlinked;
318 } // ios;
324 } // ios;
319 };
325 };
320 in {
326 in {
@@ -336,9 +342,9 @@ in rec {
336 commonName
342 commonName
337 ];
343 ];
338 };
344 };
339− android = __android null;
345+ android = __androidWithConfig (base + "/config");
340− ios = __ios null;
346+ ios = __iosWithConfig (base + "/config");
341− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
347+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
342 };
348 };
343 in mkProject (projectDefinition args));
349 in mkProject (projectDefinition args));
344 serverOn = sys: version: serverExe
350 serverOn = sys: version: serverExe
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
1{
1{
2 "owner": "reflex-frp",
2 "owner": "reflex-frp",
3 "repo": "reflex-platform",
3 "repo": "reflex-platform",
4− "branch": "hydration",
4+ "branch": "develop",
5− "rev": "d4b1dcfa6d2795e612b9a4d89c40467afb9fad9d",
5+ "rev": "d68ab80e3a92425d4bcbfa2da7c685aded550c16",
6− "sha256": "0wqcvmsivhk9h30p8fmwwp10qn8l8mwfxishpxpxwprc1kh4dbc4"
6+ "sha256": "0d5hc4v56pldy65rl0pmdzx2jylh8rjqqg5n0qb48039yy3pp8cq"
7}
7}
jobsets.nix+5
@@ -29,6 +29,11 @@ let
29 emailresponsible = false;
29 emailresponsible = false;
30 };
30 };
31 };
31 };
32+ self-args = {
33+ type = "nix";
34+ value = "{ config.android_sdk.accept_license = true; }";
35+ emailresponsible = false;
36+ };
32 };
37 };
33 branchJobset = branch: defaults {
38 branchJobset = branch: defaults {
34 description = "obelisk-${branch}";
39 description = "obelisk-${branch}";
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+2−1
@@ -49,7 +49,8 @@ toHashedPath root relativePath = do
49 contents <- LBS.readFile path
49 contents <- LBS.readFile path
50 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
50 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
51 (dir, filename) = splitFileName relativePath
51 (dir, filename) = splitFileName relativePath
52− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
53
54
54-- | Convert a ByteString to base 32 in the way that Nix does
55-- | Convert a ByteString to base 32 in the way that Nix does
55toNixBase32 :: LBS.ByteString -> LBS.ByteString
56toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/backend/obelisk-backend.cabal+4
@@ -8,12 +8,16 @@ library
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10 categories,
10 categories,
11+ containers,
12+ cookie,
11 data-default,
13 data-default,
12 dependent-sum,
14 dependent-sum,
13 dependent-sum-template,
15 dependent-sum-template,
14 lens,
16 lens,
15 mtl,
17 mtl,
16 obelisk-asset-serve-snap,
18 obelisk-asset-serve-snap,
19+ obelisk-executable-config-backend,
20+ obelisk-executable-config-lookup,
17 obelisk-frontend,
21 obelisk-frontend,
18 obelisk-route,
22 obelisk-route,
19 obelisk-snap-extras,
23 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+87−28
@@ -6,11 +6,12 @@
6{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
9+{-# OPTIONS_GHC -fno-warn-orphans #-}
10module Obelisk.Backend
10module Obelisk.Backend
11 ( Backend (..)
11 ( Backend (..)
12 -- * Re-exports
12 -- * Re-exports
13 , Default (def)
13 , Default (def)
14+ , ComponentConfigs(..)
14 , getPageName
15 , getPageName
15 , getRouteWith
16 , getRouteWith
16 , runSnapWithCommandLineArgs
17 , runSnapWithCommandLineArgs
@@ -19,6 +20,7 @@ module Obelisk.Backend
19 , runBackend
20 , runBackend
20 , staticRenderContentType
21 , staticRenderContentType
21 , mkRouteToUrl
22 , mkRouteToUrl
23+ , getComponentConfigs
22 ) where
24 ) where
23
25
24import Prelude hiding (id, (.))
26import Prelude hiding (id, (.))
@@ -32,23 +34,35 @@ import Data.Default (Default (..))
32import Data.Dependent.Sum
34import Data.Dependent.Sum
33import Data.Functor.Sum
35import Data.Functor.Sum
34import Data.Functor.Identity
36import Data.Functor.Identity
37+import Data.Map (Map)
38+import qualified Data.Map as Map
35import Data.Monoid ((<>))
39import Data.Monoid ((<>))
36import Data.Text (Text)
40import Data.Text (Text)
37import qualified Data.Text as T
41import qualified Data.Text as T
38import Data.Text.Encoding
42import Data.Text.Encoding
39import Obelisk.Asset.Serve.Snap (serveAsset)
43import Obelisk.Asset.Serve.Snap (serveAsset)
44+import Obelisk.ExecutableConfig.Backend
45+import Obelisk.ExecutableConfig.Lookup
40import Obelisk.Frontend
46import Obelisk.Frontend
41import Obelisk.Route
47import Obelisk.Route
42import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
48import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
43import Reflex.Dom
49import Reflex.Dom
44import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
50import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
45− , rqPathInfo, rqQueryString, setContentType, writeBS, writeText)
51+ , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
52+ , rqCookies, Cookie(..))
46import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
53import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
47import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
54import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
48
55
49data Backend backendRoute frontendRoute = Backend
56data Backend backendRoute frontendRoute = Backend
50 { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
57 { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
51− , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
58+ , _backend_run :: ((R backendRoute -> BackendConfigsT Snap ()) -> BackendConfigsT IO ()) -> BackendConfigsT IO ()
59+ }
60+
61+-- | All the configs separated by which context they are accessible in. (Common
62+-- configs are present in both.)
63+data ComponentConfigs = ComponentConfigs
64+ { _componentConfigs_backend :: Map Text Text
65+ , _componentConfigs_frontend :: Map Text Text
52 }
66 }
53
67
54-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
68-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
@@ -59,8 +73,17 @@ data GhcjsApp route = GhcjsApp
59
73
60-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
74-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
61--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
75--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
62−serveDefaultObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
76+serveDefaultObeliskApp
77+ :: (MonadSnap m, HasCookies m)
78+ => (R appRoute
79+ -> Text)
80+ -> ([Text]
81+ -> m ())
82+ -> Frontend (R appRoute)
83+ -> Map Text Text
84+ -> R (ObeliskRoute appRoute)
85+ -> m ()
63−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
86+serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
64 where frontendApp = GhcjsApp
87 where frontendApp = GhcjsApp
65 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
88 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
66 , _ghcjsApp_value = frontend
89 , _ghcjsApp_value = frontend
@@ -85,16 +108,16 @@ defaultFrontendGhcjsAssets = StaticAssets
85 , _staticAssets_unprocessed = "frontend.jsexe"
108 , _staticAssets_unprocessed = "frontend.jsexe"
86 }
109 }
87
110
88−runSnapWithCommandLineArgs :: Snap () -> IO ()
111+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
89runSnapWithCommandLineArgs a = do
112runSnapWithCommandLineArgs a = do
90 -- Get the web server configuration from the command line
113 -- Get the web server configuration from the command line
91− cmdLineConf <- commandLineConfig defaultConfig
114+ cmdLineConf <- liftIO $ commandLineConfig defaultConfig
92 let httpConf = cmdLineConf
115 let httpConf = cmdLineConf
93 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
116 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
94 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
117 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
95 }
118 }
96 -- Start the web server
119 -- Start the web server
97− httpServe httpConf a
120+ liftIO $ httpServe httpConf a
98
121
99getPageName :: (MonadSnap m) => m PageName
122getPageName :: (MonadSnap m) => m PageName
100getPageName = do
123getPageName = do
@@ -111,12 +134,19 @@ getRouteWith e = do
111 pageName <- getPageName
134 pageName <- getPageName
112 return $ tryDecode e pageName
135 return $ tryDecode e pageName
113
136
137+serveObeliskApp
138+ :: (MonadSnap m, HasCookies m)
139+ => (R appRoute -> Text)
140+ -> ([Text] -> m ())
141+ -> GhcjsApp (R appRoute)
142+ -> Map Text Text
114−serveObeliskApp :: MonadSnap m => (R appRoute -> Text) -> ([Text] -> m ()) -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
143+ -> R (ObeliskRoute appRoute)
144+ -> m ()
115−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
145+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
116− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
146+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
117 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
147 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
118 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
148 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
119− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
149+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
120 ResourceRoute_JSaddleWarp :=> Identity _ -> do
150 ResourceRoute_JSaddleWarp :=> Identity _ -> do
121 let msg = "Error: Obelisk.Backend received jsaddle request"
151 let msg = "Error: Obelisk.Backend received jsaddle request"
122 liftIO $ putStrLn $ T.unpack msg
152 liftIO $ putStrLn $ T.unpack msg
@@ -140,34 +170,63 @@ staticRenderContentType :: ByteString
140staticRenderContentType = "text/html; charset=utf-8"
170staticRenderContentType = "text/html; charset=utf-8"
141
171
142--TODO: Don't assume we're being served at "/"
172--TODO: Don't assume we're being served at "/"
173+serveGhcjsApp
174+ :: (MonadSnap m, HasCookies m)
175+ => (R appRouteComponent -> Text)
176+ -> GhcjsApp (R appRouteComponent)
177+ -> Map Text Text
143−serveGhcjsApp :: MonadSnap m => (R appRouteComponent -> Text) -> GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
178+ -> R (GhcjsAppRoute appRouteComponent)
179+ -> m ()
144−serveGhcjsApp urlEnc app = \case
180+serveGhcjsApp urlEnc app config = \case
145 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
181 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
146 modifyResponse $ setContentType staticRenderContentType
182 modifyResponse $ setContentType staticRenderContentType
147− writeBS <=< liftIO $ renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
183+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
148 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
184 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
149
185
150runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
186runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
151runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
187runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
152 Left e -> fail $ "backend error:\n" <> T.unpack e
188 Left e -> fail $ "backend error:\n" <> T.unpack e
189+ Right validFullEncoder -> do
190+ configs <- getComponentConfigs
191+ runBackendConfigsT (_componentConfigs_backend configs) $
153− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
192+ _backend_run backend $ \serveRoute -> do
154− runSnapWithCommandLineArgs $ do
193+ runSnapWithCommandLineArgs $ do
155− getRouteWith validFullEncoder >>= \case
194+ getRouteWith validFullEncoder >>= \case
156− Identity r -> case r of
195+ Identity r -> case r of
157− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
196+ InL backendRoute :=> Identity a ->
197+ runBackendConfigsT (_componentConfigs_backend configs) $
198+ serveRoute $ backendRoute :/ a
158− InR obeliskRoute :=> Identity a ->
199+ InR obeliskRoute :=> Identity a ->
159− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
200+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
160
201
161mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
202mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
162mkRouteToUrl validFullEncoder =
203mkRouteToUrl validFullEncoder =
163 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
204 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
164 in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
205 in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
165
206
166−
207+renderGhcjsFrontend
167−renderGhcjsFrontend :: MonadIO m => (route -> Text) -> route -> Frontend route -> m ByteString
208+ :: (MonadSnap m, HasCookies m)
209+ => (route -> Text)
210+ -> route
211+ -> Map Text Text
212+ -> Frontend route
213+ -> m ByteString
168−renderGhcjsFrontend urlEnc route f = do
214+renderGhcjsFrontend urlEnc route configs f = do
169 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
215 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
170 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
216 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
217+ cookies <- askCookies
171− liftIO $ renderFrontendHtml urlEnc route
218+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
172− (_frontend_head f >> ghcjsPreload)
219+
173− (_frontend_body f >> ghcjsScript)
220+instance HasCookies Snap where
221+ askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
222+
223+getComponentConfigs :: IO ComponentConfigs
224+getComponentConfigs = do
225+ allConfigs <- getConfigs
226+ return $ ComponentConfigs
227+ { _componentConfigs_backend =
228+ Map.filterWithKey (\k _ -> isMemberOf k ["common", "backend"]) allConfigs
229+ , _componentConfigs_frontend =
230+ Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) allConfigs
231+ }
232+ where isMemberOf k dirs = or $ map (\dir -> ("config/" <> dir) `T.isPrefixOf` k) dirs
lib/cliapp/obelisk-cliapp.cabal+1
@@ -29,5 +29,6 @@ library
29 Obelisk.CliApp.Process
29 Obelisk.CliApp.Process
30 Obelisk.CliApp.Spinner
30 Obelisk.CliApp.Spinner
31 Obelisk.CliApp.TerminalString
31 Obelisk.CliApp.TerminalString
32+ Obelisk.CliApp.Theme
32 Obelisk.CliApp.Types
33 Obelisk.CliApp.Types
33 ghc-options: -Wall -fobject-code
34 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
39import Control.Monad.Loops (iterateUntil)
39import Control.Monad.Loops (iterateUntil)
40import Control.Monad.Reader (MonadIO, ReaderT (..))
40import Control.Monad.Reader (MonadIO, ReaderT (..))
41import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
41import Data.IORef (atomicModifyIORef', newIORef, readIORef, writeIORef)
42+import Data.Maybe
42import Data.Semigroup ((<>))
43import Data.Semigroup ((<>))
43import Data.Text (Text)
44import Data.Text (Text)
44import qualified Data.Text as T
45import qualified Data.Text as T
45import qualified Data.Text.IO as T
46import qualified Data.Text.IO as T
47+import GHC.IO.Encoding.Types
46import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
48import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
47 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
49 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
48 SGR (SetColor, SetConsoleIntensity), clearLine)
50 SGR (SetColor, SetConsoleIntensity), clearLine)
49import System.Exit (ExitCode (..))
51import System.Exit (ExitCode (..))
50−import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
52+import System.IO
51
53
52import qualified Obelisk.CliApp.TerminalString as TS
54import qualified Obelisk.CliApp.TerminalString as TS
55+import Obelisk.CliApp.Theme
53import Obelisk.CliApp.Types
56import Obelisk.CliApp.Types
54
57
55newCliConfig
58newCliConfig
@@ -63,7 +66,11 @@ newCliConfig sev noColor noSpinner errorLogExitCode = do
63 lock <- newMVar False
66 lock <- newMVar False
64 tipDisplayed <- newIORef False
67 tipDisplayed <- newIORef False
65 stack <- newIORef ([], [])
68 stack <- newIORef ([], [])
69+ textEncoding <- hGetEncoding stdout
70+ let theme = if fromMaybe False $ supportsUnicode <$> textEncoding
71+ then unicodeTheme
72+ else noUnicodeTheme
66− return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode
73+ return $ CliConfig level noColor noSpinner lock tipDisplayed stack errorLogExitCode theme
67
74
68runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
75runCli :: MonadIO m => CliConfig e -> CliT e m a -> m a
69runCli c =
76runCli c =
@@ -226,3 +233,20 @@ fork :: (HasCliConfig e m, MonadIO m) => CliT e IO () -> m ThreadId
226fork f = do
233fork f = do
227 c <- getCliConfig
234 c <- getCliConfig
228 liftIO $ forkIO $ runCli c f
235 liftIO $ forkIO $ runCli c f
236+
237+-- | Conservatively determines whether the encoding supports Unicode.
238+--
239+-- Currently this uses a whitelist of known-to-work encodings. In principle it
240+-- could test dynamically by opening a file with this encoding, but it doesn't
241+-- look like base exposes any way to determine this in a pure fashion.
242+supportsUnicode :: TextEncoding -> Bool
243+supportsUnicode enc = any ((textEncodingName enc ==) . textEncodingName)
244+ [ utf8
245+ , utf8_bom
246+ , utf16
247+ , utf16be
248+ , utf16le
249+ , utf32
250+ , utf32be
251+ , utf32le
252+ ]
lib/cliapp/src/Obelisk/CliApp/Spinner.hs+18−17
@@ -24,6 +24,7 @@ import System.Console.ANSI (Color (Blue, Cyan, Green, Red))
24
24
25import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
25import Obelisk.CliApp.Logging (allowUserToMakeLoggingVerbose, fork, putLog)
26import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
26import Obelisk.CliApp.TerminalString (TerminalString (..), enquiryCode)
27+import Obelisk.CliApp.Theme
27import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
28import Obelisk.CliApp.Types (CliLog, CliConfig (..), HasCliConfig, Output (..), getCliConfig)
28
29
29-- | Run an action with a CLI spinner.
30-- | Run an action with a CLI spinner.
@@ -51,7 +52,8 @@ withSpinner'
51 -> m a
52 -> m a
52 -> m a
53 -> m a
53withSpinner' msg mkTrail action = do
54withSpinner' msg mkTrail action = do
54− noSpinner <- _cliConfig_noSpinner <$> getCliConfig
55+ cliConf <- getCliConfig
56+ let noSpinner = _cliConfig_noSpinner cliConf
55 if noSpinner
57 if noSpinner
56 then putLog Notice msg >> action
58 then putLog Notice msg >> action
57 else bracket' run cleanup $ const action
59 else bracket' run cleanup $ const action
@@ -61,8 +63,11 @@ withSpinner' msg mkTrail action = do
61 modifyStack pushSpinner >>= \case
63 modifyStack pushSpinner >>= \case
62 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
64 True -> do -- Top-level spinner; fork a thread to manage output of anything on the stack
63 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
65 ctrleThread <- fork $ allowUserToMakeLoggingVerbose enquiryCode
66+ cliConf <- getCliConfig
67+ let theme = _cliConfig_theme cliConf
68+ spinner = coloredSpinner $ _cliTheme_spinner theme
64 spinnerThread <- fork $ runSpinner spinner $ \c -> do
69 spinnerThread <- fork $ runSpinner spinner $ \c -> do
65− logs <- renderSpinnerStack c . snd <$> readStack
70+ logs <- renderSpinnerStack theme c . snd <$> readStack
66 logMessage $ Output_Overwrite logs
71 logMessage $ Output_Overwrite logs
67 pure [ctrleThread, spinnerThread]
72 pure [ctrleThread, spinnerThread]
68 False -> -- Sub-spinner; nothing to do.
73 False -> -- Sub-spinner; nothing to do.
@@ -70,13 +75,15 @@ withSpinner' msg mkTrail action = do
70 cleanup tids resultM = do
75 cleanup tids resultM = do
71 liftIO $ mapM_ killThread tids
76 liftIO $ mapM_ killThread tids
72 logMessage Output_ClearLine
77 logMessage Output_ClearLine
78+ cliConf <- getCliConfig
79+ let theme = _cliConfig_theme cliConf
73− logsM <- modifyStack $ popSpinner $ case resultM of
80+ logsM <- modifyStack $ (popSpinner theme) $ case resultM of
74 Nothing ->
81 Nothing ->
75− ( TerminalString_Colorized Red "✖"
82+ ( TerminalString_Colorized Red $ _cliTheme_failed $ _cliConfig_theme cliConf
76 , Just msg -- Always display final message if there was an exception.
83 , Just msg -- Always display final message if there was an exception.
77 )
84 )
78 Just result ->
85 Just result ->
79− ( TerminalString_Colorized Green "✔"
86+ ( TerminalString_Colorized Green $ _cliTheme_done $ _cliConfig_theme cliConf
80 , mkTrail <*> pure result
87 , mkTrail <*> pure result
81 )
88 )
82 -- Last message, finish off with newline.
89 -- Last message, finish off with newline.
@@ -87,10 +94,10 @@ withSpinner' msg mkTrail action = do
87 )
94 )
88 where
95 where
89 isTemporary = isNothing mkTrail
96 isTemporary = isNothing mkTrail
90− popSpinner (mark, trailMsgM) (flag, old) =
97+ popSpinner theme (mark, trailMsgM) (flag, old) =
91 ( (newFlag, new)
98 ( (newFlag, new)
92 -- With final trail spinner message to render
99 -- With final trail spinner message to render
93− , renderSpinnerStack mark . (: new) . TerminalString_Normal <$> (
100+ , renderSpinnerStack theme mark . (: new) . TerminalString_Normal <$> (
94 if inTemporarySpinner then Nothing else trailMsgM
101 if inTemporarySpinner then Nothing else trailMsgM
95 )
102 )
96 )
103 )
@@ -102,19 +109,19 @@ withSpinner' msg mkTrail action = do
102 =<< fmap _cliConfig_spinnerStack getCliConfig
109 =<< fmap _cliConfig_spinnerStack getCliConfig
103 modifyStack f = liftIO . flip atomicModifyIORef' f
110 modifyStack f = liftIO . flip atomicModifyIORef' f
104 =<< fmap _cliConfig_spinnerStack getCliConfig
111 =<< fmap _cliConfig_spinnerStack getCliConfig
105− spinner = coloredSpinner defaultSpinnerTheme
106
112
107-- | How nested spinner logs should be displayed
113-- | How nested spinner logs should be displayed
108renderSpinnerStack
114renderSpinnerStack
115+ :: CliTheme
109− :: TerminalString -- ^ That which comes before the final element in stack
116+ -> TerminalString -- ^ That which comes before the final element in stack
110 -> [TerminalString] -- ^ Spinner elements in reverse order
117 -> [TerminalString] -- ^ Spinner elements in reverse order
111 -> [TerminalString]
118 -> [TerminalString]
112−renderSpinnerStack mark = L.intersperse space . go . L.reverse
119+renderSpinnerStack theme mark = L.intersperse space . go . L.reverse
113 where
120 where
114 go [] = []
121 go [] = []
115 go (x:[]) = mark : [x]
122 go (x:[]) = mark : [x]
116 go (x:xs) = arrow : x : go xs
123 go (x:xs) = arrow : x : go xs
117− arrow = TerminalString_Colorized Blue "⇾"
124+ arrow = TerminalString_Colorized Blue $ _cliTheme_arrow theme
118 space = TerminalString_Normal " "
125 space = TerminalString_Normal " "
119
126
120-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
127-- | A spinner is simply an infinite list of strings that supplant each other in a delayed loop, creating the
@@ -130,12 +137,6 @@ runSpinner spinner f = forM_ spinner $ f >=> const delay
130 where
137 where
131 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
138 delay = liftIO $ threadDelay 100000 -- A shorter delay ensures that we update promptly.
132
139
133−type SpinnerTheme = [Text]
134−
135−-- Find more spinners at https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json
136−defaultSpinnerTheme :: SpinnerTheme
137−defaultSpinnerTheme = ["◐", "◓", "◑", "◒"]
138−
139-- | Like `bracket` but the `release` function can know whether an exception was raised
140-- | Like `bracket` but the `release` function can know whether an exception was raised
140bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
141bracket' :: MonadMask m => m a -> (a -> Maybe c -> m b) -> (a -> m c) -> m c
141bracket' acquire release use = mask $ \unmasked -> do
142bracket' 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)
23import System.Exit (ExitCode (..), exitWith)
23import System.Exit (ExitCode (..), exitWith)
24
24
25import Obelisk.CliApp.TerminalString (TerminalString)
25import Obelisk.CliApp.TerminalString (TerminalString)
26+import Obelisk.CliApp.Theme (CliTheme)
26
27
27--------------------------------------------------------------------------------
28--------------------------------------------------------------------------------
28
29
@@ -88,6 +89,8 @@ data CliConfig e = CliConfig
88 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
89 _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
89 , -- | Failure handler. How to log error and what exit status to use.
90 , -- | Failure handler. How to log error and what exit status to use.
90 _cliConfig_errorLogExitCode :: e -> (Text, Int)
91 _cliConfig_errorLogExitCode :: e -> (Text, Int)
92+ , -- | Theme strings for spinners
93+ _cliConfig_theme :: CliTheme
91 }
94 }
92
95
93class Monad m => HasCliConfig e m | m -> e where
96class Monad m => HasCliConfig e m | m -> e where
lib/command/src/Obelisk/Command.hs+7−4
@@ -213,14 +213,17 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
213data ThunkCommand
213data ThunkCommand
214 = ThunkCommand_Update [FilePath] (Maybe String)
214 = ThunkCommand_Update [FilePath] (Maybe String)
215 | ThunkCommand_Unpack [FilePath]
215 | ThunkCommand_Unpack [FilePath]
216− | ThunkCommand_Pack [FilePath]
216+ | ThunkCommand_Pack [FilePath] Bool
217 deriving Show
217 deriving Show
218
218
219+forceFlag :: Parser Bool
220+forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
221+
219thunkCommand :: Parser ThunkCommand
222thunkCommand :: Parser ThunkCommand
220thunkCommand = hsubparser $ mconcat
223thunkCommand = hsubparser $ mconcat
221 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
224 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
222 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
225 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
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"
224 ]
227 ]
225
228
226data ShellOpts
229data ShellOpts
@@ -342,7 +345,7 @@ ob = \case
342 Right (ThunkData_Packed ptr) -> return ptr
345 Right (ThunkData_Packed ptr) -> return ptr
343 Right (ThunkData_Checkout (Just ptr)) -> return ptr
346 Right (ThunkData_Checkout (Just ptr)) -> return ptr
344 Right (ThunkData_Checkout Nothing) ->
347 Right (ThunkData_Checkout Nothing) ->
345− getThunkPtr' False root
348+ getThunkPtr False root
346 let sshKeyPath = _deployInitOpts_sshKey deployOpts
349 let sshKeyPath = _deployInitOpts_sshKey deployOpts
347 hostname = _deployInitOpts_hostname deployOpts
350 hostname = _deployInitOpts_hostname deployOpts
348 route = _deployInitOpts_route deployOpts
351 route = _deployInitOpts_route deployOpts
@@ -361,7 +364,7 @@ ob = \case
361 ObCommand_Thunk tc -> case tc of
364 ObCommand_Thunk tc -> case tc of
362 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
365 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
363 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
366 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
364− ThunkCommand_Pack thunks -> forM_ thunks packThunk
367+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
365 ObCommand_Repl -> runRepl
368 ObCommand_Repl -> runRepl
366 ObCommand_Watch -> inNixShell' $ static runWatch
369 ObCommand_Watch -> inNixShell' $ static runWatch
367 ObCommand_Shell so -> withProjectRoot "." $ \root ->
370 ObCommand_Shell so -> withProjectRoot "." $ \root ->
lib/command/src/Obelisk/Command/Deploy.hs+4−4
@@ -108,7 +108,7 @@ deployPush deployPath getNixBuilders = do
108 Right (ThunkData_Packed ptr) -> return ptr
108 Right (ThunkData_Packed ptr) -> return ptr
109 Right (ThunkData_Checkout _) -> do
109 Right (ThunkData_Checkout _) -> do
110 checkGitCleanStatus srcPath True >>= \case
110 checkGitCleanStatus srcPath True >>= \case
111− True -> packThunk srcPath
111+ True -> packThunk False srcPath
112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -136,7 +136,7 @@ deployPush deployPath getNixBuilders = do
136 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
136 withSpinner "Uploading closures" $ ifor_ buildOutputByHost $ \host outputPath -> do
137 callProcess'
137 callProcess'
138 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
138 (Map.fromList [("NIX_SSHOPTS", unwords sshOpts)])
139− "nix-copy-closure" ["-v", "--to", "root@" <> host, "--gzip", outputPath]
139+ "nix-copy-closure" ["-v", "--to", "--use-substitutes", "root@" <> host, "--gzip", outputPath]
140 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
140 withSpinner "Uploading config" $ ifor_ buildOutputByHost $ \host _ -> do
141 callProcessAndLogOutput (Notice, Warning) $
141 callProcessAndLogOutput (Notice, Warning) $
142 proc "rsync"
142 proc "rsync"
@@ -234,7 +234,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
234 [ "with (import ", srcDir, " {});"
234 [ "with (import ", srcDir, " {});"
235 , "android.frontend.override (drv: { "
235 , "android.frontend.override (drv: { "
236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
237− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
237+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
238 , "})"
238 , "})"
239 ]
239 ]
240 return $ Target
240 return $ Target
@@ -245,7 +245,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
245 IOS -> do
245 IOS -> do
246 let expr = mconcat
246 let expr = mconcat
247 [ "with (import ", srcDir, " {});"
247 [ "with (import ", srcDir, " {});"
248− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
249 ]
249 ]
250 return $ Target
250 return $ Target
251 { _target_path = Nothing
251 { _target_path = Nothing
lib/command/src/Obelisk/Command/Run.hs+15−2
@@ -10,8 +10,8 @@ import Control.Monad
10import Control.Monad.IO.Class (liftIO)
10import Control.Monad.IO.Class (liftIO)
11import Control.Monad.Reader (MonadIO)
11import Control.Monad.Reader (MonadIO)
12import Data.Either
12import Data.Either
13−import Data.List
13+import qualified Data.List as L
14−import Data.List.NonEmpty as NE
14+import qualified Data.List.NonEmpty as NE
15import Data.Maybe
15import Data.Maybe
16import qualified Data.Text as T
16import qualified Data.Text as T
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
17import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
@@ -46,6 +46,8 @@ data CabalPackageInfo = CabalPackageInfo
46 -- ^ List of hs src dirs of the library component
46 -- ^ List of hs src dirs of the library component
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
47 , _cabalPackageInfo_defaultExtensions :: [Extension]
48 -- ^ List of globally enable extensions of the library component
48 -- ^ List of globally enable extensions of the library component
49+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
50+ -- ^ List of globally set languages of the library component
49 }
51 }
50
52
51-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
53-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -136,6 +138,8 @@ parseCabalPackage dir = do
136 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
138 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
137 , _cabalPackageInfo_defaultExtensions =
139 , _cabalPackageInfo_defaultExtensions =
138 defaultExtensions $ libBuildInfo lib
140 defaultExtensions $ libBuildInfo lib
141+ , _cabalPackageInfo_defaultLanguage =
142+ defaultLanguage $ libBuildInfo lib
139 }
143 }
140 Left (_, errors) -> do
144 Left (_, errors) -> do
141 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
145 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -165,17 +169,22 @@ withGhciScript pkgs f = do
165 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
169 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
166
170
167 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
171 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
172+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
173+ -- NOTE when no default-language is present cabal sets Haskell98
174+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
168 extensionsLine = if extensions == mempty
175 extensionsLine = if extensions == mempty
169 then ""
176 then ""
170 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
177 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
171 dotGhci = unlines $
178 dotGhci = unlines $
172 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
179 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
173 , extensionsLine
180 , extensionsLine
181+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
174 , ":load Backend Frontend"
182 , ":load Backend Frontend"
175 , "import Obelisk.Run"
183 , "import Obelisk.Run"
176 , "import qualified Frontend"
184 , "import qualified Frontend"
177 , "import qualified Backend"
185 , "import qualified Backend"
178 ]
186 ]
187+ warnDifferentLanguages language
179 withSystemTempDirectory "ob-ghci" $ \fp -> do
188 withSystemTempDirectory "ob-ghci" $ \fp -> do
180 let dotGhciPath = fp </> ".ghci"
189 let dotGhciPath = fp </> ".ghci"
181 liftIO $ writeFile dotGhciPath dotGhci
190 liftIO $ writeFile dotGhciPath dotGhci
@@ -185,6 +194,10 @@ withGhciScript pkgs f = do
185 rootedSourceDirs pkg = NE.toList $
194 rootedSourceDirs pkg = NE.toList $
186 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
195 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
187
196
197+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
198+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
199+warnDifferentLanguages _ = return ()
200+
188-- | Run ghci repl
201-- | Run ghci repl
189runGhciRepl
202runGhciRepl
190 :: MonadObelisk m
203 :: MonadObelisk m
lib/command/src/Obelisk/Command/Thunk.hs+7−12
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
26 , readThunk
26 , readThunk
27 , updateThunk
27 , updateThunk
28 , getThunkPtr
28 , getThunkPtr
29− , getThunkPtr'
30 , parseGitUri
29 , parseGitUri
31 , uriThunkPtr
30 , uriThunkPtr
32 ) where
31 ) where
@@ -573,7 +572,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
573 return tmpThunk
572 return tmpThunk
574 Right _ -> failWith $ "Thunk is not packed"
573 Right _ -> failWith $ "Thunk is not packed"
575 updateThunkFromTmp p' = do
574 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
575+ _ <- packThunk' True False p'
577 callProcessAndLogOutput (Notice, Error) $
576 callProcessAndLogOutput (Notice, Error) $
578 proc "cp" ["-r", "-T", p', p]
577 proc "cp" ["-r", "-T", p', p]
579
578
@@ -622,28 +621,24 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
622 callProcessAndLogOutput (Notice, Error) $
621 callProcessAndLogOutput (Notice, Error) $
623 proc "mv" ["-T", tmpRepo, thunkDir]
622 proc "mv" ["-T", tmpRepo, thunkDir]
624
623
625−--TODO: add force mode to pack even if changes are present
626--TODO: add a rollback mode to pack to the original thunk
624--TODO: add a rollback mode to pack to the original thunk
627−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
625+packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628packThunk = packThunk' False
626packThunk = packThunk' False
629
627
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628+packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
629+packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
630 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
631 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634 Right (ThunkData_Checkout _) -> do
632 Right (ThunkData_Checkout _) -> do
635 withSpinner' ("Packing thunk " <> T.pack thunkDir)
633 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
634 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
635+ thunkPtr <- getThunkPtr (not force) thunkDir
638 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
636 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639 liftIO $ createThunk thunkDir thunkPtr
637 liftIO $ createThunk thunkDir thunkPtr
640 pure thunkPtr
638 pure thunkPtr
641
639
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
643−getThunkPtr = getThunkPtr' True
644−
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
640+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
641+getThunkPtr checkClean thunkDir = do
647 when checkClean $ ensureCleanGitRepo thunkDir True $
642 when checkClean $ ensureCleanGitRepo thunkDir True $
648 "thunk pack: thunk checkout contains unsaved modifications"
643 "thunk pack: thunk checkout contains unsaved modifications"
649
644
lib/executable-config/backend/obelisk-executable-config-backend.cabalnew file+20
@@ -0,0 +1,20 @@
1+name: obelisk-executable-config-backend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ containers,
10+ monad-control,
11+ mtl,
12+ primitive,
13+ ref-tf,
14+ snap-core,
15+ text,
16+ transformers,
17+ transformers-base
18+ exposed-modules:
19+ Obelisk.ExecutableConfig.Backend
20+ ghc-options: -Wall
lib/executable-config/backend/src/Obelisk/ExecutableConfig/Backend.hsnew file+77
@@ -0,0 +1,77 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.ExecutableConfig.Backend
14+ ( HasBackendConfigs(..)
15+ , BackendConfigsT(..)
16+ , runBackendConfigsT
17+ , mapBackendConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.Map (Map)
31+import qualified Data.Map as Map
32+import Data.Text (Text)
33+import Snap.Core (MonadSnap)
34+
35+class Monad m => HasBackendConfigs m where
36+ getBackendConfig :: Text -> m (Maybe Text)
37+ default getBackendConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
38+ getBackendConfig = lift . getBackendConfig
39+ getCommonConfig :: Text -> m (Maybe Text)
40+ default getCommonConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
41+ getCommonConfig = lift . getCommonConfig
42+
43+newtype BackendConfigsT m a = BackendConfigsT { unBackendConfigsT :: ReaderT (Map Text Text) m a }
44+ deriving
45+ ( Functor
46+ , Applicative
47+ , Monad
48+ , MonadPlus
49+ , Alternative
50+ , MonadFix
51+ , MonadIO
52+ , MonadBase m'
53+ , MonadBaseControl m'
54+ , MonadRef
55+ , MonadTrans
56+ , MonadSnap
57+ )
58+
59+instance PrimMonad m => PrimMonad (BackendConfigsT m) where
60+ type PrimState (BackendConfigsT m) = PrimState m
61+ primitive = lift . primitive
62+
63+runBackendConfigsT
64+ :: Map Text Text
65+ -> BackendConfigsT m a
66+ -> m a
67+runBackendConfigsT cs child = runReaderT (unBackendConfigsT child) cs
68+
69+instance Monad m => HasBackendConfigs (BackendConfigsT m) where
70+ getBackendConfig k = BackendConfigsT $ Map.lookup ("config/backend/" <> k) <$> ask
71+ getCommonConfig k = BackendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
72+
73+mapBackendConfigsT
74+ :: (forall x. m x -> n x)
75+ -> BackendConfigsT m a
76+ -> BackendConfigsT n a
77+mapBackendConfigsT f (BackendConfigsT x) = BackendConfigsT $ mapReaderT f x
lib/executable-config/default.nix+6−1
@@ -16,9 +16,14 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
16 exit 1
16 exit 1
17 fi
17 fi
18 cp -a "${config}"/* "$out/config"
18 cp -a "${config}"/* "$out/config"
19+ (cd $out && find config -type f -print0 >config.files)
19 '');
20 '');
20in with nixpkgs.haskell.lib; {
21in with nixpkgs.haskell.lib; {
22+ haskellOverlay = self: super: {
23+ obelisk-executable-config-backend = self.callCabal2nix "obelisk-executable-config-backend" (filterGitSource ./backend) {};
24+ obelisk-executable-config-frontend = self.callCabal2nix "obelisk-executable-config-frontend" (filterGitSource ./frontend) {};
21− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
25+ obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
26+ };
22 platforms = {
27 platforms = {
23 android = {
28 android = {
24 # Inject the given config directory into an android assets folder
29 # Inject the given config directory into an android assets folder
lib/executable-config/frontend/obelisk-executable-config-frontend.cabalnew file+21
@@ -0,0 +1,21 @@
1+name: obelisk-executable-config-frontend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ containers,
10+ ghcjs-dom,
11+ jsaddle,
12+ mtl,
13+ primitive,
14+ ref-tf,
15+ reflex,
16+ reflex-dom-core,
17+ text,
18+ transformers
19+ exposed-modules:
20+ Obelisk.ExecutableConfig.Frontend
21+ ghc-options: -Wall
lib/executable-config/frontend/src/Obelisk/ExecutableConfig/Frontend.hsnew file+115
@@ -0,0 +1,115 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.ExecutableConfig.Frontend
14+ ( HasFrontendConfigs(..)
15+ , FrontendConfigsT
16+ , runFrontendConfigsT
17+ , mapFrontendConfigsT
18+ ) where
19+
20+import Control.Monad.Fix
21+import Control.Monad.Primitive
22+import Control.Monad.Ref
23+import Control.Monad.IO.Class
24+import Control.Monad.Trans.Class
25+import Control.Monad.Trans.Reader
26+import Data.Map (Map)
27+import qualified Data.Map as Map
28+import Data.Text (Text)
29+import Reflex
30+import Reflex.Host.Class
31+import Reflex.Dom.Core
32+#ifndef ghcjs_HOST_OS
33+import GHCJS.DOM.Types (MonadJSM)
34+#endif
35+
36+class Monad m => HasFrontendConfigs m where
37+ getFrontendConfig :: Text -> m (Maybe Text)
38+ default getFrontendConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
39+ getFrontendConfig = lift . getFrontendConfig
40+ getCommonConfig :: Text -> m (Maybe Text)
41+ default getCommonConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
42+ getCommonConfig = lift . getCommonConfig
43+
44+instance HasFrontendConfigs m => HasFrontendConfigs (BehaviorWriterT t w m)
45+instance HasFrontendConfigs m => HasFrontendConfigs (DynamicWriterT t w m)
46+instance HasFrontendConfigs m => HasFrontendConfigs (EventWriterT t w m)
47+instance HasFrontendConfigs m => HasFrontendConfigs (PostBuildT t m)
48+instance HasFrontendConfigs m => HasFrontendConfigs (QueryT t q m)
49+instance HasFrontendConfigs m => HasFrontendConfigs (ReaderT r m)
50+instance HasFrontendConfigs m => HasFrontendConfigs (RequesterT t request response m)
51+instance HasFrontendConfigs m => HasFrontendConfigs (StaticDomBuilderT t m)
52+instance HasFrontendConfigs m => HasFrontendConfigs (TriggerEventT t m)
53+
54+newtype FrontendConfigsT m a = FrontendConfigsT { unFrontendConfigsT :: ReaderT (Map Text Text) m a }
55+ deriving
56+ ( Functor
57+ , Applicative
58+ , DomBuilder t
59+ , Monad
60+ , MonadFix
61+ , MonadHold t
62+ , MonadIO
63+#ifndef ghcjs_HOST_OS
64+ , MonadJSM
65+#endif
66+ , MonadRef
67+ , MonadReflexCreateTrigger t
68+ , MonadSample t
69+ , MonadTrans
70+ , NotReady t
71+ , PostBuild t
72+ , TriggerEvent t
73+ , HasDocument
74+ , DomRenderHook t
75+ , HasJSContext
76+ , HasJS js
77+ )
78+
79+instance PerformEvent t m => PerformEvent t (FrontendConfigsT m) where
80+ type Performable (FrontendConfigsT m) = FrontendConfigsT (Performable m)
81+ performEvent e = FrontendConfigsT $ ReaderT $ \configs ->
82+ performEvent $ runFrontendConfigsT configs <$> e
83+ performEvent_ e = FrontendConfigsT $ ReaderT $ \configs ->
84+ performEvent_ $ runFrontendConfigsT configs <$> e
85+
86+instance Adjustable t m => Adjustable t (FrontendConfigsT m) where
87+ runWithReplace a e = FrontendConfigsT $ runWithReplace (unFrontendConfigsT a) (unFrontendConfigsT <$> e)
88+ traverseDMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
89+ traverseIntMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
90+ traverseDMapWithKeyWithAdjustWithMove f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFrontendConfigsT $ f k v) m e
91+
92+instance Prerender js t m => Prerender js t (FrontendConfigsT m) where
93+ type Client (FrontendConfigsT m) = FrontendConfigsT (Client m)
94+ prerender server client = FrontendConfigsT $ ReaderT $ \configs ->
95+ prerender (runFrontendConfigsT configs server) (runFrontendConfigsT configs client)
96+
97+instance PrimMonad m => PrimMonad (FrontendConfigsT m) where
98+ type PrimState (FrontendConfigsT m) = PrimState m
99+ primitive = lift . primitive
100+
101+runFrontendConfigsT
102+ :: Map Text Text
103+ -> FrontendConfigsT m a
104+ -> m a
105+runFrontendConfigsT cs child = runReaderT (unFrontendConfigsT child) cs
106+
107+instance Monad m => HasFrontendConfigs (FrontendConfigsT m) where
108+ getFrontendConfig k = FrontendConfigsT $ Map.lookup ("config/frontend/" <> k) <$> ask
109+ getCommonConfig k = FrontendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
110+
111+mapFrontendConfigsT
112+ :: (forall x. m x -> n x)
113+ -> FrontendConfigsT m a
114+ -> FrontendConfigsT n a
115+mapFrontendConfigsT f (FrontendConfigsT x) = FrontendConfigsT $ mapReaderT f x
lib/executable-config/inject/obelisk-executable-config-inject.cabal+1−3
@@ -8,9 +8,7 @@ library
8
8
9 build-depends:
9 build-depends:
10 base,
10 base,
11− bytestring,
11+ containers,
12− directory,
13− filepath,
14 reflex-dom-core,
12 reflex-dom-core,
15 text
13 text
16
14
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+8−37
@@ -2,50 +2,21 @@
2module Obelisk.ExecutableConfig.Inject where
2module Obelisk.ExecutableConfig.Inject where
3
3
4import Control.Monad (mapM_)
4import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
5+import Control.Monad.IO.Class (MonadIO)
6+import Data.Map (Map)
7+import qualified Data.Map as Map
6import Data.Semigroup ((<>))
8import Data.Semigroup ((<>))
7−import Data.ByteString (ByteString)
8import Data.Text (Text)
9import Data.Text (Text)
9−import qualified Data.Text as T
10−import qualified Data.Text.IO as T
11import Reflex.Dom.Core hiding (value)
10import Reflex.Dom.Core hiding (value)
12−import System.Directory
13−import System.FilePath ((</>))
14−
15−-- | Produces a @<script>@ tag containing the contents of the config item,
16−-- rendered as a 'ByteString'.
17−inject :: FilePath -> IO ByteString
18−inject key = do
19− value <- T.readFile key
20− fmap snd $ renderStatic $ injectPure (T.pack key) value
21
11
22-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
12-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23-- whose value is the provided configuration
13-- whose value is the provided configuration
24injectPure :: DomBuilder t m => Text -> Text -> m ()
14injectPure :: DomBuilder t m => Text -> Text -> m ()
25injectPure key value =
15injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
16+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key)
27 in elAttr "script" attrs $ text value
17 in elAttr "script" attrs $ text value
28
18
29−-- | Scans the "common" and "frontend" configuration folders and produces
30−-- injectable @<script>@ tags containing the configuration keys (filepaths) and
19+-- | Produces injectable @<script>@ tags containing the configuration keys
31−-- values.
20+-- (filepaths) and values.
32−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
21+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text Text -> m ()
33−injectExecutableConfigs = do
22+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
34− cfgC <- getConfigs "config/common"
35− cfgF <- getConfigs "config/frontend"
36− mapM_ (uncurry injectPure) (cfgC <> cfgF)
37−
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
39−getConfigs fp = liftIO $ do
40− dir <- doesDirectoryExist fp
41− if dir
42− then do
43− ps <- listDirectory fp
44− fmap concat $ mapM (\p -> getConfigs $ fp </> p) ps
45− else do
46− file <- doesFileExist fp
47− if file
48− then do
49− f <- T.readFile fp
50− return [(T.pack fp, f)]
51− else return []
lib/executable-config/lookup/default.nix+1−1
@@ -6,7 +6,7 @@ let isAndroid = hostPlatform.libc == "bionic";
6 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
6 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
7 isGhcjs = ghc.isGhcjs or false;
7 isGhcjs = ghc.isGhcjs or false;
8in mkDerivation {
8in mkDerivation {
9− pname = "obelisk-executable-config";
9+ pname = "obelisk-executable-config-lookup";
10 version = "0.1";
10 version = "0.1";
11 src = ./.;
11 src = ./.;
12 libraryHaskellDepends = [
12 libraryHaskellDepends = [
lib/executable-config/lookup/obelisk-executable-config.cabal → lib/executable-config/lookup/obelisk-executable-config-lookup.cabalrenamed+11−4
@@ -1,13 +1,15 @@
1−name: obelisk-executable-config
1+name: obelisk-executable-config-lookup
2version: 0.1.1
2version: 0.1.1
3cabal-version: >= 1.2
3cabal-version: >= 1.2
4build-type: Simple
4build-type: Simple
5
5
6library
6library
7− exposed-modules: Obelisk.ExecutableConfig
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup
8 if os(ios)
8 if os(ios)
9− hs-source-dirs: src-ios
9+ hs-source-dirs: src-ios, src-directory
10+ other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
10 build-depends: jsaddle-wkwebview
11 build-depends: jsaddle-wkwebview
12+ build-depends: directory
11 else
13 else
12 if os(linux-android)
14 if os(linux-android)
13 hs-source-dirs: src-android
15 hs-source-dirs: src-android
@@ -20,11 +22,16 @@ library
20 hs-source-dirs: src-ghcjs
22 hs-source-dirs: src-ghcjs
21 build-depends: ghcjs-dom
23 build-depends: ghcjs-dom
22 else
24 else
23− hs-source-dirs: src
25+ hs-source-dirs: src, src-directory
26+ other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
27+ build-depends: directory
24
28
25 build-depends:
29 build-depends:
26 base,
30 base,
27 bytestring,
31 bytestring,
32+ containers,
28 filepath,
33 filepath,
29 text,
34 text,
30 transformers
35 transformers
36+
37+ ghc-options: -Wall
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+56
@@ -0,0 +1,56 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Control.Exception (bracket)
5+import Control.Monad (forM)
6+import qualified Data.ByteString as BS
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Text (Text)
10+import qualified Data.Text.Encoding as T
11+import Foreign.C.String (withCString)
12+import Foreign.Ptr (nullPtr)
13+
14+import Obelisk.ExecutableConfig.Internal.AssetManager
15+
16+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe Text)
17+getFromMgr mgr name = do
18+ let open = do
19+ a <- BS.useAsCString name $ \fn ->
20+ assetManager_open mgr fn 3
21+ return $ if unAAsset a == nullPtr
22+ then Nothing
23+ else Just a
24+ close = mapM_ asset_close
25+ bracket open close $ mapM $ \asset -> do
26+ b <- asset_getBuffer asset
27+ l <- asset_getLength asset
28+ fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
29+
30+getConfigs :: IO (Map Text Text)
31+getConfigs = bracket getAssets freeAssetManager $ \mgrObj -> do
32+ mgr <- assetManagerFromJava mgrObj
33+ let openDir = do
34+ d <- withCString "config.files" $ \fn ->
35+ assetManager_open mgr fn 3
36+ return $ if unAAsset d == nullPtr
37+ then Nothing
38+ else Just d
39+ closeDir = mapM_ asset_close
40+ configPaths <- bracket openDir closeDir $ \case
41+ Just asset -> do
42+ b <- asset_getBuffer asset
43+ l <- asset_getLength asset
44+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
45+ Nothing -> error "could not open configuration manifest 'config.files'"
46+ fmap Map.fromList $ forM configPaths $ \fp ->
47+ getFromMgr mgr fp >>= \case
48+ Just v -> return (T.decodeUtf8 fp, v)
49+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
50+
51+lines0 :: BS.ByteString -> [BS.ByteString]
52+lines0 ps
53+ | BS.null ps = []
54+ | otherwise = case BS.elemIndex 0 ps of
55+ Nothing -> [ps]
56+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-directory/Obelisk/ExecutableConfig/Internal/ConfigDirectory.hsnew file+24
@@ -0,0 +1,24 @@
1+module Obelisk.ExecutableConfig.Internal.ConfigDirectory where
2+
3+import Data.Map (Map)
4+import qualified Data.Map as Map
5+import Data.Text (Text)
6+import Data.Text as T
7+import Data.Text.IO as T
8+import System.Directory
9+import System.FilePath.Posix ((</>))
10+
11+getConfigsFromDirectory :: FilePath -> FilePath -> IO (Map Text Text)
12+getConfigsFromDirectory base fp = do
13+ dir <- doesDirectoryExist (base </> fp)
14+ if dir
15+ then do
16+ ps <- listDirectory (base </> fp)
17+ fmap mconcat $ mapM (\p -> getConfigsFromDirectory base $ fp </> p) ps
18+ else do
19+ file <- doesFileExist (base </> fp)
20+ if file
21+ then do
22+ f <- T.readFile (base </> fp)
23+ return $ Map.singleton (T.pack fp) f
24+ else return mempty
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+36
@@ -0,0 +1,36 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.Map (Map)
6+import qualified Data.Map as Map
7+import Data.Maybe (catMaybes)
8+import Data.Text (Text)
9+import Data.Traversable (for)
10+import GHCJS.DOM
11+import GHCJS.DOM.Document (getHead)
12+import GHCJS.DOM.DOMStringMap (get)
13+import GHCJS.DOM.Element (getInnerHTML)
14+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
15+import GHCJS.DOM.NodeList (item, getLength)
16+import GHCJS.DOM.ParentNode (querySelectorAll)
17+import GHCJS.DOM.Types (Node(Node), castTo)
18+
19+getConfigs :: IO (Map Text Text)
20+getConfigs = do
21+ Just doc <- currentDocument
22+ Just hd <- getHead doc
23+ nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
24+ fmap Map.fromList $ for nodes $ \node -> do
25+ e <- castTo HTMLElement node >>= \case
26+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
27+ Just htmlE -> return htmlE
28+ dataset <- getDataset e
29+ (,)
30+ <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
31+ <*> getInnerHTML e
32+ where
33+ collToList es = do
34+ len <- getLength es
35+ list <- traverse (item es) [0..len-1]
36+ pure $ catMaybes list
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Control.Monad
5−import Control.Monad.Trans.Maybe
6−import Data.ByteString as BS
7−import Data.Text (Text)
8−import qualified Data.Text as T
9−import qualified Data.Text.IO as T
10−import qualified Data.Text.Encoding as T
11−import Language.Javascript.JSaddle.WKWebView
12−import System.FilePath.Posix ((</>))
13−import System.IO.Error
14−
15−get :: Text -> IO (Maybe Text)
16−get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
18−
19−catchDoesNotExist :: IO a -> IO (Maybe a)
20−catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
21− where
22− doesNotExist e = if isDoesNotExistError e then Just () else Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hsnew file+15
@@ -0,0 +1,15 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.Map (Map)
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
8+import Language.Javascript.JSaddle.WKWebView
9+
10+import Obelisk.ExecutableConfig.Internal.ConfigDirectory
11+
12+getConfigs :: IO (Map Text Text)
13+getConfigs = mainBundleResourcePath >>= \case
14+ Nothing -> error "Could not get bundle resource path."
15+ Just p -> getConfigsFromDirectory (T.unpack (T.decodeUtf8 p)) "config"
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/executable-config/lookup/src/Obelisk/ExecutableConfig/Lookup.hsnew file+9
@@ -0,0 +1,9 @@
1+module Obelisk.ExecutableConfig.Lookup where
2+
3+import Data.Map (Map)
4+import Data.Text (Text)
5+
6+import Obelisk.ExecutableConfig.Internal.ConfigDirectory
7+
8+getConfigs :: IO (Map Text Text)
9+getConfigs = getConfigsFromDirectory "." "config"
lib/frontend/obelisk-frontend.cabal+7−1
@@ -7,12 +7,16 @@ library
7 hs-source-dirs: src
7 hs-source-dirs: src
8 build-depends: base,
8 build-depends: base,
9 bytestring,
9 bytestring,
10+ containers,
11+ cookie,
10 dependent-sum,
12 dependent-sum,
11 ghcjs-dom,
13 ghcjs-dom,
12 jsaddle,
14 jsaddle,
13 lens,
15 lens,
14 mtl,
16 mtl,
17+ obelisk-executable-config-frontend,
15 obelisk-executable-config-inject,
18 obelisk-executable-config-inject,
19+ obelisk-executable-config-lookup,
16 obelisk-route,
20 obelisk-route,
17 primitive,
21 primitive,
18 ref-tf,
22 ref-tf,
@@ -20,5 +24,7 @@ library
20 reflex-dom-core,
24 reflex-dom-core,
21 text,
25 text,
22 transformers
26 transformers
23− exposed-modules: Obelisk.Frontend
27+ exposed-modules:
28+ Obelisk.Frontend
29+ Obelisk.Frontend.Cookie
24 ghc-options: -Wall
30 ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hs+81−45
@@ -1,4 +1,5 @@
1{-# LANGUAGE ConstraintKinds #-}
1{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE CPP #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE LambdaCase #-}
@@ -14,40 +15,43 @@ module Obelisk.Frontend
14 ( ObeliskWidget
15 ( ObeliskWidget
15 , Frontend (..)
16 , Frontend (..)
16 , runFrontend
17 , runFrontend
18+ , runFrontendWithConfigs
17 , renderFrontendHtml
19 , renderFrontendHtml
20+ , removeHTMLConfigs
21+ , module Obelisk.Frontend.Cookie
18 ) where
22 ) where
19
23
20import Prelude hiding ((.))
24import Prelude hiding ((.))
21
25
22import Control.Category
26import Control.Category
23−import Control.Concurrent
24import Control.Lens
27import Control.Lens
25−import Control.Monad hiding (sequence, sequence_)
26import Control.Monad.Fix
28import Control.Monad.Fix
27import Control.Monad.IO.Class
29import Control.Monad.IO.Class
28import Control.Monad.Primitive
30import Control.Monad.Primitive
29import Control.Monad.Reader
31import Control.Monad.Reader
30import Control.Monad.Ref
32import Control.Monad.Ref
31−import Control.Monad.Trans.Class
33+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
32import Data.ByteString (ByteString)
34import Data.ByteString (ByteString)
33−import Data.Dependent.Sum (DSum (..))
34import Data.Foldable (for_)
35import Data.Foldable (for_)
35import Data.Functor.Sum
36import Data.Functor.Sum
36−import Data.IORef
37+import Data.Map (Map)
38+import Data.Maybe (catMaybes)
37import Data.Monoid ((<>))
39import Data.Monoid ((<>))
38import Data.Text (Text)
40import Data.Text (Text)
39−import Data.Foldable (sequence_)
41+import GHCJS.DOM (currentDocument)
40−import Data.Traversable (sequence)
41−import GHCJS.DOM hiding (bracket, catch)
42−import GHCJS.DOM.Document
42+import GHCJS.DOM.Document (getHead)
43−import GHCJS.DOM.Node
43+import GHCJS.DOM.Node (removeChild_)
44−import qualified GHCJS.DOM.Types as DOM
44+import GHCJS.DOM.NodeList (item, getLength)
45+import GHCJS.DOM.ParentNode (querySelectorAll)
45import Language.Javascript.JSaddle (JSM)
46import Language.Javascript.JSaddle (JSM)
47+import Obelisk.Frontend.Cookie
46import Obelisk.Route.Frontend
48import Obelisk.Route.Frontend
47import Reflex.Dom.Core
49import Reflex.Dom.Core
48import Reflex.Host.Class
50import Reflex.Host.Class
49−import qualified Reflex.TriggerEvent.Base as TriggerEvent
51+import Obelisk.ExecutableConfig.Frontend
50import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
52import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
53+import Obelisk.ExecutableConfig.Lookup (getConfigs)
54+import Web.Cookie
51
55
52makePrisms ''Sum
56makePrisms ''Sum
53
57
@@ -70,8 +74,8 @@ type ObeliskWidget js t route m =
70 , Prerender js t m
74 , Prerender js t m
71 , PrebuildAgnostic t route m
75 , PrebuildAgnostic t route m
72 , PrebuildAgnostic t route (Client m)
76 , PrebuildAgnostic t route (Client m)
73− -- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
77+ , HasFrontendConfigs m
74− , MonadIO m
78+ , HasCookies m
75 , MonadIO (Performable m)
79 , MonadIO (Performable m)
76 )
80 )
77
81
@@ -79,6 +83,8 @@ type PrebuildAgnostic t route m =
79 ( SetRoute t route m
83 ( SetRoute t route m
80 , RouteToUrl route m
84 , RouteToUrl route m
81 , MonadFix m
85 , MonadFix m
86+ , HasFrontendConfigs m
87+ , HasFrontendConfigs (Performable m)
82 )
88 )
83
89
84data Frontend route = Frontend
90data Frontend route = Frontend
@@ -86,51 +92,81 @@ data Frontend route = Frontend
86 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
92 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
87 }
93 }
88
94
95+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
96+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
97+
98+removeHTMLConfigs :: JSM ()
99+removeHTMLConfigs = void $ runMaybeT $ do
100+ doc <- MaybeT currentDocument
101+ hd <- MaybeT $ getHead doc
102+ es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
103+ for_ es $ removeChild_ hd
104+ where
105+ collToList es = do
106+ len <- getLength es
107+ lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
108+ pure $ catMaybes lst
109+
110+runFrontend
111+ :: forall backendRoute route
89−runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
112+ . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
113+ -> Frontend (R route)
114+ -> JSM ()
90runFrontend validFullEncoder frontend = do
115runFrontend validFullEncoder frontend = do
116+ configs <- liftIO getConfigs
117+#ifdef ghcjs_HOST_OS
118+ removeHTMLConfigs
119+#endif
120+ runFrontendWithConfigs configs validFullEncoder frontend
121+
122+runFrontendWithConfigs
123+ :: forall backendRoute route
124+ . Map Text Text
125+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
126+ -> Frontend (R route)
127+ -> JSM ()
128+runFrontendWithConfigs configs validFullEncoder frontend = do
91 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
129 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
92 errorLeft = \case
130 errorLeft = \case
93 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
131 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
94 Right x -> Identity x
132 Right x -> Identity x
95− runMyRouteViewT
96− :: ( TriggerEvent t m
97− , PerformEvent t m
98− , MonadHold t m
99− , DOM.MonadJSM m
100− , DOM.MonadJSM (Performable m)
101− , MonadFix m
102− , MonadFix (Performable m)
103− )
104− => Event t ()
105− -> RoutedT t (R route) (SetRouteT t (R route) (RouteToUrlT (R route) m)) a
106− -> m a
107− runMyRouteViewT = runRouteViewT ve
108 runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
133 runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
109− rec switchover <- runMyRouteViewT switchover $ do
134+ rec switchover <- runRouteViewT ve switchover $ do
110− (switchover, fire) <- newTriggerEvent
135+ (switchover'', fire) <- newTriggerEvent
111− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
136+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runFrontendConfigsT configs))) $ do
137+ -- The order here is important - baseTag has to be before headWidget!
138+ baseTag
112 _frontend_head frontend
139 _frontend_head frontend
113− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
140+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runFrontendConfigsT configs))) $ do
114 _frontend_body frontend
141 _frontend_body frontend
115− switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
142+ switchover' <- lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
116 performEvent_ $ liftIO (fire ()) <$ switchover'
143 performEvent_ $ liftIO (fire ()) <$ switchover'
117− pure switchover
144+ pure switchover''
118 pure ()
145 pure ()
119
146
120renderFrontendHtml
147renderFrontendHtml
121− :: (t ~ DomTimeline)
148+ :: ( t ~ DomTimeline
149+ , MonadIO m
150+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (FrontendConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
151+ )
152+ => Map Text Text
153+ -> Cookies
122− => (r' -> Text)
154+ -> (r -> Text)
123 -> r
155 -> r
124− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
156+ -> Frontend r
125− -> RoutedT t r (SetRouteT t r' (RouteToUrlT r' (PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))))) ()
157+ -> widget ()
158+ -> widget ()
126− -> IO ByteString
159+ -> m ByteString
127−renderFrontendHtml urlEnc route headWidget bodyWidget = do
160+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
128 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
129− html <- fmap snd $ renderStatic $ fmap fst $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
162+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runFrontendConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
130 el "html" $ do
163 el "html" $ do
131 el "head" $ do
164 el "head" $ do
132− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
165+ baseTag
133− -- The order here is important - baseTag has to be before headWidget!
134− baseTag >> injectExecutableConfigs >> headWidget
166+ injectExecutableConfigs configs
167+ _frontend_head frontend
168+ headExtra
135− el "body" bodyWidget
169+ el "body" $ do
170+ _frontend_body frontend
171+ bodyExtra
136 return $ "<!DOCTYPE html>" <> html
172 return $ "<!DOCTYPE html>" <> html
lib/frontend/src/Obelisk/Frontend/Cookie.hsnew file+102
@@ -0,0 +1,102 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+
13+module Obelisk.Frontend.Cookie where
14+
15+import Control.Monad.Fix
16+import Control.Monad.Primitive
17+import Control.Monad.Ref
18+import Control.Monad.IO.Class
19+import Control.Monad.Trans.Class
20+import Control.Monad.Trans.Reader
21+import Data.Text.Encoding (encodeUtf8)
22+import Reflex
23+import Reflex.Host.Class
24+import Reflex.Dom.Core
25+import GHCJS.DOM.Document (getCookie, Document)
26+import GHCJS.DOM.Types (MonadJSM)
27+import Web.Cookie
28+
29+import Obelisk.ExecutableConfig.Frontend
30+import Obelisk.Route.Frontend
31+
32+class Monad m => HasCookies m where
33+ askCookies :: m Cookies
34+ default askCookies :: (HasCookies m', m ~ t m', MonadTrans t) => m Cookies
35+ askCookies = lift askCookies
36+
37+instance HasCookies m => HasCookies (BehaviorWriterT t w m)
38+instance HasCookies m => HasCookies (DynamicWriterT t w m)
39+instance HasCookies m => HasCookies (EventWriterT t w m)
40+instance HasCookies m => HasCookies (PostBuildT t m)
41+instance HasCookies m => HasCookies (QueryT t q m)
42+instance HasCookies m => HasCookies (ReaderT r m)
43+instance HasCookies m => HasCookies (RequesterT t request response m)
44+instance HasCookies m => HasCookies (RouteToUrlT t m)
45+instance HasCookies m => HasCookies (SetRouteT t r m)
46+instance HasCookies m => HasCookies (StaticDomBuilderT t m)
47+instance HasCookies m => HasCookies (TriggerEventT t m)
48+instance HasCookies m => HasCookies (RoutedT t r m)
49+instance HasCookies m => HasCookies (FrontendConfigsT m)
50+instance HasFrontendConfigs m => HasFrontendConfigs (CookiesT m)
51+
52+newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
53+ deriving
54+ ( Functor
55+ , Applicative
56+ , DomBuilder t
57+ , Monad
58+ , MonadFix
59+ , MonadHold t
60+ , MonadIO
61+#ifndef ghcjs_HOST_OS
62+ , MonadJSM
63+#endif
64+ , MonadRef
65+ , MonadReflexCreateTrigger t
66+ , MonadSample t
67+ , MonadTrans
68+ , NotReady t
69+ , PerformEvent t
70+ , PostBuild t
71+ , Prerender js t
72+ , TriggerEvent t
73+ , HasDocument
74+ )
75+
76+instance Adjustable t m => Adjustable t (CookiesT m) where
77+ runWithReplace a e = CookiesT $ runWithReplace (unCookiesT a) (unCookiesT <$> e)
78+ traverseDMapWithKeyWithAdjust f m e = CookiesT $ traverseDMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
79+ traverseIntMapWithKeyWithAdjust f m e = CookiesT $ traverseIntMapWithKeyWithAdjust (\k v -> unCookiesT $ f k v) m e
80+ traverseDMapWithKeyWithAdjustWithMove f m e = CookiesT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unCookiesT $ f k v) m e
81+
82+instance PrimMonad m => PrimMonad (CookiesT m) where
83+ type PrimState (CookiesT m) = PrimState m
84+ primitive = lift . primitive
85+
86+runCookiesT
87+ :: Cookies
88+ -> CookiesT m a
89+ -> m a
90+runCookiesT cs child = runReaderT (unCookiesT child) cs
91+
92+instance Monad m => HasCookies (CookiesT m) where
93+ askCookies = CookiesT ask
94+
95+mapCookiesT
96+ :: (forall x. m x -> n x)
97+ -> CookiesT m a
98+ -> CookiesT n a
99+mapCookiesT f (CookiesT x) = CookiesT $ mapReaderT f x
100+
101+instance (MonadJSM m, RawDocument (DomBuilderSpace (HydrationDomBuilderT s t m)) ~ Document) => HasCookies (HydrationDomBuilderT s t m) where
102+ askCookies = fmap (parseCookies . encodeUtf8) $ getCookie =<< askDocument
lib/route/obelisk-route.cabal+1
@@ -24,6 +24,7 @@ library
24 monad-control,
24 monad-control,
25 mtl,
25 mtl,
26 network-uri,
26 network-uri,
27+ obelisk-executable-config-frontend,
27 primitive,
28 primitive,
28 ref-tf,
29 ref-tf,
29 reflex,
30 reflex,
lib/route/src/Obelisk/Route.hs+1−1
@@ -159,7 +159,7 @@ import Data.Aeson (FromJSON, ToJSON)
159
159
160type R f = DSum f Identity --TODO: Better name
160type R f = DSum f Identity --TODO: Better name
161
161
162−--TODO: COMPLETE pragma
162+{-# COMPLETE (:/) #-}
163infixr 5 :/
163infixr 5 :/
164pattern (:/) :: f a -> a -> R f
164pattern (:/) :: f a -> a -> R f
165pattern a :/ b = a :=> Identity b
165pattern a :/ b = a :=> Identity b
lib/route/src/Obelisk/Route/Frontend.hs+57−17
@@ -3,6 +3,7 @@
3{-# LANGUAGE RecursiveDo #-}
3{-# LANGUAGE RecursiveDo #-}
4{-# LANGUAGE StandaloneDeriving #-}
4{-# LANGUAGE StandaloneDeriving #-}
5{-# LANGUAGE ConstraintKinds #-}
5{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE DefaultSignatures #-}
6{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleContexts #-}
7{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE FlexibleInstances #-}
8{-# LANGUAGE GADTs #-}
9{-# LANGUAGE GADTs #-}
@@ -37,6 +38,9 @@ module Obelisk.Route.Frontend
37 , maybeRoute
38 , maybeRoute
38 , maybeRoute_
39 , maybeRoute_
39 , maybeRouted
40 , maybeRouted
41+ , eitherRoute
42+ , eitherRoute_
43+ , eitherRouted
40 , runRouteViewT
44 , runRouteViewT
41 , SetRouteT(..)
45 , SetRouteT(..)
42 , SetRoute(..)
46 , SetRoute(..)
@@ -52,17 +56,18 @@ module Obelisk.Route.Frontend
52
56
53import Prelude hiding ((.), id)
57import Prelude hiding ((.), id)
54
58
59+import Obelisk.ExecutableConfig.Frontend
55import Obelisk.Route
60import Obelisk.Route
56
61
57import Control.Category (Category (..), (.))
62import Control.Category (Category (..), (.))
58import Control.Category.Cartesian
63import Control.Category.Cartesian
59import Control.Lens hiding (Bifunctor, bimap, universe, element)
64import Control.Lens hiding (Bifunctor, bimap, universe, element)
65+import Control.Monad ((<=<))
60import Control.Monad.Fix
66import Control.Monad.Fix
61import Control.Monad.Primitive
67import Control.Monad.Primitive
68+import Control.Monad.Reader
62import Control.Monad.Ref
69import Control.Monad.Ref
63−import Control.Monad.Trans
64import Control.Monad.Trans.Control
70import Control.Monad.Trans.Control
65−import Control.Monad.Trans.Reader
66import Data.Coerce
71import Data.Coerce
67import Data.Dependent.Sum (DSum (..))
72import Data.Dependent.Sum (DSum (..))
68import Data.GADT.Compare
73import Data.GADT.Compare
@@ -98,13 +103,21 @@ pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
98
103
99class Routed t r m | m -> t r where
104class Routed t r m | m -> t r where
100 askRoute :: m (Dynamic t r)
105 askRoute :: m (Dynamic t r)
106+ default askRoute :: (Monad m', MonadTrans f, Routed t r m', m ~ f m') => m (Dynamic t r)
107+ askRoute = lift askRoute
101
108
102instance Monad m => Routed t r (RoutedT t r m) where
109instance Monad m => Routed t r (RoutedT t r m) where
103 askRoute = RoutedT ask
110 askRoute = RoutedT ask
104
111
112+instance (Monad m, Routed t r m) => Routed t r (ReaderT r' m)
113+
105newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
114newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
106 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
115 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
107
116
117+instance MonadReader r' m => MonadReader r' (RoutedT t r m) where
118+ ask = lift ask
119+ local = mapRoutedT . local
120+
108instance HasJSContext m => HasJSContext (RoutedT t r m) where
121instance HasJSContext m => HasJSContext (RoutedT t r m) where
109 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
122 type JSContextPhantom (RoutedT t r m) = JSContextPhantom m
110 askJSContext = lift askJSContext
123 askJSContext = lift askJSContext
@@ -165,17 +178,20 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
165 askQueryResult = lift askQueryResult
178 askQueryResult = lift askQueryResult
166 queryIncremental = lift . queryIncremental
179 queryIncremental = lift . queryIncremental
167
180
168−instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m) where
181+instance HasFrontendConfigs m => HasFrontendConfigs (RoutedT t r m)
169− askRouteToUrl = lift askRouteToUrl
170
182
183+instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
184+
171−instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m) where
185+instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
172− setRoute = lift . setRoute
186+
173− modifyRoute = lift . modifyRoute
187+instance (Monad m, RouteToUrl r m) => RouteToUrl r (EventWriterT t w m)
188+
189+instance (Monad m, SetRoute t r m) => SetRoute t r (EventWriterT t w m)
174
190
175runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
191runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
176runRoutedT = runReaderT . unRoutedT
192runRoutedT = runReaderT . unRoutedT
177
193
178−mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
194+mapRoutedT :: (m a -> n b) -> RoutedT t r m a -> RoutedT t r n b
179mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
195mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
180
196
181withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
197withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
@@ -205,6 +221,20 @@ maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
205 runRoutedT (f c) r'
221 runRoutedT (f c) r'
206-}
222-}
207
223
224+eitherRoute_
225+ :: (MonadFix m, MonadHold t m, Adjustable t m)
226+ => RoutedT t l m ()
227+ -> RoutedT t r m ()
228+ -> RoutedT t (Either l r) m ()
229+eitherRoute_ l r = eitherRouted $ strictDynWidget_ $ either (runRoutedT l) (runRoutedT r)
230+
231+eitherRoute
232+ :: (MonadFix m, MonadHold t m, Adjustable t m)
233+ => RoutedT t l m a
234+ -> RoutedT t r m a
235+ -> RoutedT t (Either l r) m (Dynamic t a)
236+eitherRoute l r = eitherRouted $ strictDynWidget $ either (runRoutedT l) (runRoutedT r)
237+
208dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
238dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
209dsumValueCoercion Coercion = Coercion
239dsumValueCoercion Coercion = Coercion
210
240
@@ -221,6 +251,9 @@ maybeRouted r = RoutedT $ ReaderT $ \d -> do
221 d' <- maybeDyn d
251 d' <- maybeDyn d
222 runRoutedT r d'
252 runRoutedT r d'
223
253
254+eitherRouted :: (Reflex t, MonadFix m, MonadHold t m) => RoutedT t (Either (Dynamic t a) (Dynamic t b)) m c -> RoutedT t (Either a b) m c
255+eitherRouted r = RoutedT $ ReaderT $ runRoutedT r <=< eitherDyn
256+
224-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
257-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
225strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
258strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
226strictDynWidget f = RoutedT $ ReaderT $ \r -> do
259strictDynWidget f = RoutedT $ ReaderT $ \r -> do
@@ -257,13 +290,17 @@ runSetRouteT = runEventWriterT . unSetRouteT
257class Reflex t => SetRoute t r m | m -> t r where
290class Reflex t => SetRoute t r m | m -> t r where
258 setRoute :: Event t r -> m ()
291 setRoute :: Event t r -> m ()
259 modifyRoute :: Event t (r -> r) -> m ()
292 modifyRoute :: Event t (r -> r) -> m ()
293+ default modifyRoute :: (Monad m', MonadTrans f, SetRoute t r m', m ~ f m') => Event t (r -> r) -> m ()
294+ modifyRoute = lift . modifyRoute
295+
260 setRoute = modifyRoute . fmap const
296 setRoute = modifyRoute . fmap const
261
297
262instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
298instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
263 modifyRoute = SetRouteT . tellEvent . fmap Endo
299 modifyRoute = SetRouteT . tellEvent . fmap Endo
264
300
265−instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
301+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m)
266− modifyRoute = lift . modifyRoute
302+
303+instance (Monad m, SetRoute t r m) => SetRoute t r (ReaderT r' m)
267
304
268instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
305instance (PerformEvent t m, Prerender js t m, Monad m, Reflex t) => Prerender js t (SetRouteT t r m) where
269 type Client (SetRouteT t r m) = SetRouteT t r (Client m)
306 type Client (SetRouteT t r m) = SetRouteT t r (Client m)
@@ -280,9 +317,7 @@ instance Requester t m => Requester t (SetRouteT t r m) where
280 requesting = SetRouteT . requesting
317 requesting = SetRouteT . requesting
281 requesting_ = SetRouteT . requesting_
318 requesting_ = SetRouteT . requesting_
282
319
283−instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m) where
320+instance (Monad m, SetRoute t r m) => SetRoute t r (RequesterT t req rsp m)
284− setRoute = lift . setRoute
285− modifyRoute = lift . modifyRoute
286
321
287#ifndef ghcjs_HOST_OS
322#ifndef ghcjs_HOST_OS
288deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
323deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
@@ -307,6 +342,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
307 type PrimState (SetRouteT t r m) = PrimState m
342 type PrimState (SetRouteT t r m) = PrimState m
308 primitive = lift . primitive
343 primitive = lift . primitive
309
344
345+instance HasFrontendConfigs m => HasFrontendConfigs (SetRouteT t r m)
346+
310instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
347instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
311 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
348 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
312 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
349 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -320,6 +357,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
320
357
321class RouteToUrl r m | m -> r where
358class RouteToUrl r m | m -> r where
322 askRouteToUrl :: m (r -> Text)
359 askRouteToUrl :: m (r -> Text)
360+ default askRouteToUrl :: (Monad m', MonadTrans f, RouteToUrl r m', m ~ f m') => m (r -> Text)
361+ askRouteToUrl = lift askRouteToUrl
323
362
324newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
363newtype RouteToUrlT r m a = RouteToUrlT { unRouteToUrlT :: ReaderT (r -> Text) m a }
325 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
364 deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomRenderHook t)
@@ -337,13 +376,12 @@ instance Monad m => RouteToUrl r (RouteToUrlT r m) where
337 askRouteToUrl = RouteToUrlT ask
376 askRouteToUrl = RouteToUrlT ask
338
377
339instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
378instance (Monad m, RouteToUrl r m) => RouteToUrl r (SetRouteT t r' m) where
340− askRouteToUrl = lift askRouteToUrl
341
379
342instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
380instance (Monad m, RouteToUrl r m) => RouteToUrl r (RoutedT t r' m) where
343− askRouteToUrl = lift askRouteToUrl
344
381
345−instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m) where
382+instance (Monad m, RouteToUrl r m) => RouteToUrl r (ReaderT r' m) where
346− askRouteToUrl = lift askRouteToUrl
383+
384+instance (Monad m, RouteToUrl r m) => RouteToUrl r (RequesterT t req rsp m)
347
385
348instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
386instance HasJSContext m => HasJSContext (RouteToUrlT r m) where
349 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
387 type JSContextPhantom (RouteToUrlT r m) = JSContextPhantom m
@@ -403,6 +441,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
403 askQueryResult = lift askQueryResult
441 askQueryResult = lift askQueryResult
404 queryIncremental = lift . queryIncremental
442 queryIncremental = lift . queryIncremental
405
443
444+instance HasFrontendConfigs m => HasFrontendConfigs (RouteToUrlT t m)
445+
406runRouteViewT
446runRouteViewT
407 :: forall t m r a.
447 :: forall t m r a.
408 ( TriggerEvent t m
448 ( TriggerEvent t m
lib/run/obelisk-run.cabal+3−1
@@ -10,6 +10,8 @@ library
10 attoparsec
10 attoparsec
11 , base
11 , base
12 , bytestring
12 , bytestring
13+ , containers
14+ , cookie
13 , dependent-sum
15 , dependent-sum
14 , dependent-sum-template
16 , dependent-sum-template
15 , ghcjs-dom
17 , ghcjs-dom
@@ -24,7 +26,7 @@ library
24 , network
26 , network
25 , obelisk-asset-serve-snap
27 , obelisk-asset-serve-snap
26 , obelisk-backend
28 , obelisk-backend
27− , obelisk-executable-config
29+ , obelisk-executable-config-backend
28 , obelisk-frontend
30 , obelisk-frontend
29 , obelisk-route
31 , obelisk-route
30 , process
32 , process
lib/run/src/Obelisk/Run.hs+46−28
@@ -1,17 +1,12 @@
1{-# LANGUAGE ConstraintKinds #-}
1{-# LANGUAGE ConstraintKinds #-}
2−{-# LANGUAGE EmptyCase #-}
3−{-# LANGUAGE EmptyDataDecls #-}
4{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE FlexibleInstances #-}
6{-# LANGUAGE GADTs #-}
4{-# LANGUAGE GADTs #-}
7−{-# LANGUAGE KindSignatures #-}
8{-# LANGUAGE LambdaCase #-}
5{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE MultiParamTypeClasses #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
10{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
11{-# LANGUAGE Rank2Types #-}
8{-# LANGUAGE Rank2Types #-}
12−{-# LANGUAGE RecursiveDo #-}
13{-# LANGUAGE ScopedTypeVariables #-}
9{-# LANGUAGE ScopedTypeVariables #-}
14−{-# LANGUAGE TemplateHaskell #-}
15{-# LANGUAGE TypeFamilies #-}
10{-# LANGUAGE TypeFamilies #-}
16{-# LANGUAGE UndecidableInstances #-}
11{-# LANGUAGE UndecidableInstances #-}
17{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
12{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
@@ -31,9 +26,12 @@ import Data.Dependent.Sum (DSum (..))
31import Data.Functor.Identity
26import Data.Functor.Identity
32import Data.Functor.Sum
27import Data.Functor.Sum
33import Data.List (uncons)
28import Data.List (uncons)
29+import Data.Map (Map)
30+import qualified Data.Map as Map
34import Data.Maybe
31import Data.Maybe
35import Data.Semigroup ((<>))
32import Data.Semigroup ((<>))
36import Data.Streaming.Network (bindPortTCP)
33import Data.Streaming.Network (bindPortTCP)
34+import Data.String (fromString)
37import Data.Text (Text)
35import Data.Text (Text)
38import qualified Data.Text as T
36import qualified Data.Text as T
39import Language.Javascript.JSaddle.Run (syncPoint)
37import Language.Javascript.JSaddle.Run (syncPoint)
@@ -49,7 +47,8 @@ import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
49import Network.WebSockets (ConnectionOptions)
47import Network.WebSockets (ConnectionOptions)
50import Network.WebSockets.Connection (defaultConnectionOptions)
48import Network.WebSockets.Connection (defaultConnectionOptions)
51import qualified Obelisk.Asset.Serve.Snap as Snap
49import qualified Obelisk.Asset.Serve.Snap as Snap
52−import Obelisk.ExecutableConfig (get)
50+import Obelisk.Backend
51+import Obelisk.ExecutableConfig.Backend
53import Obelisk.Frontend
52import Obelisk.Frontend
54import Obelisk.Route.Frontend
53import Obelisk.Route.Frontend
55import Reflex.Dom.Core
54import Reflex.Dom.Core
@@ -61,7 +60,7 @@ import System.Exit (ExitCode(..))
61import Text.URI (URI)
60import Text.URI (URI)
62import qualified Text.URI as URI
61import qualified Text.URI as URI
63import Text.URI.Lens
62import Text.URI.Lens
64−import Obelisk.Backend
63+import Web.Cookie
65
64
66run
65run
67 :: Int -- ^ Port to run the backend
66 :: Int -- ^ Port to run the backend
@@ -76,23 +75,27 @@ run port serveStaticAsset backend frontend = do
76 case checkEncoder $ _backend_routeEncoder backend of
75 case checkEncoder $ _backend_routeEncoder backend of
77 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
76 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
78 Right validFullEncoder -> do
77 Right validFullEncoder -> do
78+ configs <- getComponentConfigs
79 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
79 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
80+ runBackendConfigsT (_componentConfigs_backend configs) $
80− _backend_run backend $ \serveRoute -> do
81+ _backend_run backend $ \serveRoute -> do
81− runSnapWithCommandLineArgs $ do
82+ runSnapWithCommandLineArgs $ do
82− getRouteWith validFullEncoder >>= \case
83+ getRouteWith validFullEncoder >>= \case
83− Identity r -> case r of
84+ Identity r -> case r of
84− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85+ InL backendRoute :=> Identity a ->
86+ runBackendConfigsT (_componentConfigs_backend configs) $
87+ serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a ->
88+ InR obeliskRoute :=> Identity a ->
86− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
89+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
87 let conf = defRunConfig { _runConfig_redirectPort = port }
90 let conf = defRunConfig { _runConfig_redirectPort = port }
88− runWidget conf frontend validFullEncoder `finally` killThread backendTid
91+ runWidget conf (_componentConfigs_frontend configs) frontend validFullEncoder `finally` killThread backendTid
89
92
90-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
93-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
91runServeAsset :: FilePath -> [Text] -> Snap ()
94runServeAsset :: FilePath -> [Text] -> Snap ()
92runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
95runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
93
96
94−getConfigRoute :: IO (Maybe URI)
97+getConfigRoute :: Map Text Text -> IO (Maybe URI)
95−getConfigRoute = get "config/common/route" >>= \case
98+getConfigRoute configs = case Map.lookup "config/common/route" configs of
96 Just r -> case URI.mkURI $ T.strip r of
99 Just r -> case URI.mkURI $ T.strip r of
97 Just route -> pure $ Just route
100 Just route -> pure $ Just route
98 Nothing -> do
101 Nothing -> do
@@ -103,9 +106,14 @@ getConfigRoute = get "config/common/route" >>= \case
103defAppUri :: URI
106defAppUri :: URI
104defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
107defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
105
108
109+runWidget
110+ :: RunConfig
111+ -> Map Text Text
112+ -> Frontend (R route)
106−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
113+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
114+ -> IO ()
107−runWidget conf frontend validFullEncoder = do
115+runWidget conf configs frontend validFullEncoder = do
108− uri <- fromMaybe defAppUri <$> getConfigRoute
116+ uri <- fromMaybe defAppUri <$> getConfigRoute configs
109 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
117 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
110 redirectHost = _runConfig_redirectHost conf
118 redirectHost = _runConfig_redirectHost conf
111 redirectPort = _runConfig_redirectPort conf
119 redirectPort = _runConfig_redirectPort conf
@@ -117,19 +125,22 @@ runWidget conf frontend validFullEncoder = do
117 close
125 close
118 (\skt -> do
126 (\skt -> do
119 man <- newManager defaultManagerSettings
127 man <- newManager defaultManagerSettings
120− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
128+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
121 runSettingsSocket settings skt app)
129 runSettingsSocket settings skt app)
122
130
123obeliskApp
131obeliskApp
124− :: forall route backendRoute. ConnectionOptions
132+ :: forall route backendRoute
133+ . Map Text Text
134+ -> ConnectionOptions
125 -> Frontend (R route)
135 -> Frontend (R route)
126 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
136 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
127 -> URI
137 -> URI
128 -> Application
138 -> Application
129 -> IO Application
139 -> IO Application
130−obeliskApp opts frontend validFullEncoder uri backend = do
140+obeliskApp configs opts frontend validFullEncoder uri backend = do
131 let entryPoint = do
141 let entryPoint = do
142+ removeHTMLConfigs
132− runFrontend validFullEncoder frontend
143+ runFrontendWithConfigs configs validFullEncoder frontend
133 syncPoint
144 syncPoint
134 jsaddlePath <- URI.mkPathPiece "jsaddle"
145 jsaddlePath <- URI.mkPathPiece "jsaddle"
135 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
146 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
@@ -143,15 +154,22 @@ obeliskApp opts frontend validFullEncoder uri backend = do
143 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
154 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
144 }
155 }
145 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
156 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
157+ let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
146− html <- renderJsaddleFrontend (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
158+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
147 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
159 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
148 _ -> backend req sendResponse
160 _ -> backend req sendResponse
149
161
150−renderJsaddleFrontend :: (route -> Text) -> route -> Frontend route -> IO ByteString
162+renderJsaddleFrontend
163+ :: Map Text Text
164+ -> Cookies
165+ -> (route -> Text)
166+ -> route
167+ -> Frontend route
168+ -> IO ByteString
151−renderJsaddleFrontend urlEnc r f =
169+renderJsaddleFrontend configs cookies urlEnc r f =
152 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
170 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
153 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
171 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
154− in renderFrontendHtml urlEnc r (_frontend_head f >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
172+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
155
173
156-- | like 'bindPortTCP' but reconnects on exception
174-- | like 'bindPortTCP' but reconnects on exception
157bindPortTCPRetry :: Settings
175bindPortTCPRetry :: Settings
lib/selftest/obelisk-selftest.cabal+1
@@ -16,6 +16,7 @@ library
16 , http-types
16 , http-types
17 , HUnit
17 , HUnit
18 , obelisk-cliapp
18 , obelisk-cliapp
19+ , obelisk-executable-config-lookup
19 , obelisk-run
20 , obelisk-run
20 , process
21 , process
21 , network
22 , network
lib/selftest/src/Obelisk/SelfTest.hs+4−2
@@ -34,6 +34,7 @@ import Test.HUnit.Base
34
34
35import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
35import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
36import qualified Obelisk.CliApp as CliApp
36import qualified Obelisk.CliApp as CliApp
37+import Obelisk.ExecutableConfig.Lookup (getConfigs)
37import Obelisk.Run (getConfigRoute)
38import Obelisk.Run (getConfigRoute)
38
39
39data ObRunState
40data ObRunState
@@ -126,8 +127,9 @@ main = do
126 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
127 ls tmp >>= liftIO . assertEqual "" []
128 ls tmp >>= liftIO . assertEqual "" []
128
129
129− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
130+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
131+ configs <- getConfigs
130− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
132+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute configs `shouldNotReturn` Nothing
131
133
132 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
134 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
133 -- when not run by 'nix-build --attr selftest'
135 -- when not run by 'nix-build --attr selftest'
release.nix+14−6
@@ -1,4 +1,5 @@
1+{ self-args ? {}
1−{ local-self ? import ./. {}
2+, local-self ? import ./. self-args
2}:
3}:
3
4
4let
5let
@@ -9,7 +10,7 @@ let
9 obeliskPackagesCommon = [
10 obeliskPackagesCommon = [
10 "obelisk-frontend"
11 "obelisk-frontend"
11 "obelisk-route"
12 "obelisk-route"
12− "obelisk-executable-config"
13+ "obelisk-executable-config-lookup"
13 ];
14 ];
14
15
15 obeliskPackagesBackend = obeliskPackagesCommon ++ [
16 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -59,17 +60,24 @@ let
59 (concatDepends ghcjs)
60 (concatDepends ghcjs)
60 ];
61 ];
61 command = local-self.command;
62 command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63+ serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
63− builtSkeleton = (import ./skeleton {}).all;
64+ androidSkeleton = (import ./skeleton { obelisk = local-self; }).android.frontend;
65+ iosObelisk = import ./. (self-args // {
66+ system = "x86_64-darwin";
67+ iosSdkVersion = "10.2";
68+ });
69+ iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
64 in {
70 in {
65 inherit
71 inherit
66 command
72 command
67 ghc ghcjs
73 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
74+ serverExeSkeleton
75+ iosSkeleton
76+ androidSkeleton;
69 cache = reflex-platform.pinBuildInputs
77 cache = reflex-platform.pinBuildInputs
70 "obelisk-${system}"
78 "obelisk-${system}"
71 cachePackages
79 cachePackages
72− [command serverExeSkeleton builtSkeleton];
80+ [command serverExeSkeleton iosSkeleton androidSkeleton];
73 });
81 });
74
82
75 metaCache = local-self.pinBuildInputs
83 metaCache = local-self.pinBuildInputs
skeleton/default.nix+11−4
@@ -1,10 +1,17 @@
1+{ obelisk ? import ./.obelisk/impl {
1−{ system ? builtins.currentSystem # TODO: Get rid of this system cruft
2+ system = builtins.currentSystem;
2−, iosSdkVersion ? "10.2"
3+ iosSdkVersion = "10.2";
4+ # You must accept the Android Software Development Kit License Agreement at
5+ # https://developer.android.com/studio/terms in order to build Android apps.
6+ # Uncomment and set this to `true` to indicate your acceptance:
7+ # config.android_sdk.accept_license = false;
8+ projectOverrides = {};
9+ }
3}:
10}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
11+with obelisk;
5project ./. ({ ... }: {
12project ./. ({ ... }: {
6 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
13 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
7 android.displayName = "Obelisk Minimal Example";
14 android.displayName = "Obelisk Minimal Example";
8 ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
15 ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
9 ios.bundleName = "Obelisk Minimal Example";
16 ios.bundleName = "Obelisk Minimal Example";
10−})
17+} // projectOverrides)