on master help sign in
Merge branch 'aa/asset-drv-reload' into develop

* aa/asset-drv-reload: static: improve logging of static derivation rebuild in ob run static: Reload static asset derivations when they change static: use symlink to built staticFiles when it's a derivation asset-manifest: Fix ob run issue caused by generated module importing generating package asset-manifest: Don't leave links to the generated src lying around asset-manifest: Provide access to the deprecated version as a user setting asset-manifest: Reduce the amount of code in the generated module asset-manifest: Use template-haskell to generate asset paths

commitc5bff73279fcd06b310667e149f2d673295ddb45 authorAli Abrar <aliabrar@gmail.com> dateSat 19 Dec 2020, 12:14 (6 years ago) parent0dead33f56ce13
14 files changed+287−73
ChangeLog.md+9 default.nix+10−4 lib/asset/README.md+2−2 lib/asset/manifest/obelisk-asset-manifest.cabal+11−1 lib/asset/manifest/src-bin/static-th.hs+41 lib/asset/manifest/src/Obelisk/Asset/Cabal.hs+42 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+13−13 lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−27 lib/asset/manifest/src/Obelisk/Asset/TH.hs+25−3 lib/cliapp/src/Obelisk/CliApp/Logging.hs+5−1 lib/command/obelisk-command.cabal+3 lib/command/src/Obelisk/Command/Project.hs+87−8 lib/command/src/Obelisk/Command/Run.hs+28−10 skeleton/frontend/src/Frontend.hs+3−4
ChangeLog.md+9
@@ -2,6 +2,15 @@
22
33This 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.
44
5+## Unreleased
6+
7+* Use TemplateHaskell to determine asset file paths
8+ * Migration: All uses of `static @"some/path"` become `$(static "some/path")`. Instead of requiring `TypeApplications` and `DataKinds`, modules calling `static` must now enable `TemplateHaskell`.
9+ * Deprecation: Deprecate static asset modules generated via 'obelisk-asset-manifest-generate' in favor of modules generated via 'obelisk-asset-th-generate'. The new executable takes the same arguments as the old and should be a drop-in replacement. To preserve the old behavior, set `__deprecated.useObeliskAssetManifestGenerate = true;` in your obelisk project configuration.
10+ * Feature: Files added to the static directory while `ob run` is active no longer require `ob run` to be restarted
11+* Feature: When `staticFiles` is a derivation, as opposed to a regular directory, produce a symlink to the result of that derivation at `static.out` and have `ob run` serve static assets from that symlink. This makes is possible for the static asset derivation to be rebuilt and the new results served without restarting `ob run`.
12+* Feature: Rebuild static asset derivations while `ob run` is active as long as the change to the derivation is within the project folder. `ob run` now displays a message ("Static assets rebuilt and symlinked to static.out") whenever static assets have been rebuilt and the new static assets are being served.
13+
514## v0.9.1.0
615
716* [#801](https://github.com/obsidiansystems/obelisk/pull/801): Remove errors and warning for local packages without a library component
default.nix+10−4
@@ -65,7 +65,7 @@ in rec {
6565 '';
6666 nullIfAbsent = p: if lib.pathExists p then p else null;
6767 #TODO: Avoid copying files within the nix store. Right now, obelisk-asset-manifest-generate copies files into a big blob so that the android/ios static assets can be imported from there; instead, we should get everything lined up right before turning it into an APK, so that copies, if necessary, only exist temporarily.
68− processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
68+ processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static", exe ? "obelisk-asset-th-generate" }: pkgs.runCommand "asset-manifest" {
6969 inherit src;
7070 outputs = [ "out" "haskellManifest" "symlinked" ];
7171 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
@@ -73,7 +73,7 @@ in rec {
7373 set -euo pipefail
7474 touch "$out"
7575 mkdir -p "$symlinked"
76− obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
76+ ${exe} "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
7777 '';
7878
7979 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
@@ -237,13 +237,14 @@ in rec {
237237 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
238238 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
239239 , __withGhcide ? false
240+ , __deprecated ? {}
240241 }:
241242 let
242243 allConfig = nixpkgs.lib.makeExtensible (self: {
243244 base = base';
244245 inherit args;
245246 userSettings = {
246− inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
247+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide __deprecated;
247248 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
248249 };
249250 frontendName = "frontend";
@@ -251,7 +252,12 @@ in rec {
251252 commonName = "common";
252253 staticName = "obelisk-generated-static";
253254 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
254− processedStatic = processAssets { src = self.userSettings.staticFiles; };
255+ processedStatic = processAssets {
256+ src = self.userSettings.staticFiles;
257+ exe = if lib.attrByPath ["userSettings" "__deprecated" "useObeliskAssetManifestGenerate"] false self
258+ then builtins.trace "obelisk-asset-manifest-generate is deprecated. Use obelisk-asset-th-generate instead." "obelisk-asset-manifest-generate"
259+ else "obelisk-asset-th-generate";
260+ };
255261 # The packages whose names and roles are defined by this package
256262 predefinedPackages = lib.filterAttrs (_: x: x != null) {
257263 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
lib/asset/README.md+2−2
@@ -5,7 +5,7 @@
55
66**Efficiently served, aggressively cached static web resources.**
77
8−The `assets.nix` file contains nix expressions that are a mutually recursive set of attributes used to create static asset directories with hashable file encodings. This file should be incorporated within your project's `default.nix` file. (See `example/default.nix`, line 2)
8+The `assets.nix` file contains nix expressions that are a mutually recursive set of attributes used to create static asset directories with hashable file encodings. This file should be incorporated within your project's `default.nix` file.
99
1010```nix
1111{ assets ? import ./../assets.nix { inherit nixpkgs; } }:
@@ -19,7 +19,7 @@ In this example, `mkAssets` is used on a directory that has a `.png` file inside
1919myAssets = assets.mkAssets ./static;
2020```
2121
22−Once you have successfully incorporated `assets.nix` into your project's nix file(s), use `nix-build` to generate a symlink of your `mkAssets` expression. The following command from `example/run-example` (line 3) will generate an immutable symlink that holds hashed static assets.
22+Once you have successfully incorporated `assets.nix` into your project's nix file(s), use `nix-build` to generate a symlink of your `mkAssets` expression. The following command will generate an immutable symlink that holds hashed static assets.
2323
2424```bash
2525nix-build -o static.assets -A myAssets
lib/asset/manifest/obelisk-asset-manifest.cabal+11−1
@@ -27,6 +27,7 @@ library
2727 , unix-compat
2828 , vector
2929 exposed-modules:
30+ Obelisk.Asset.Cabal
3031 Obelisk.Asset.Gather
3132 Obelisk.Asset.Promoted
3233 Obelisk.Asset.Symlink
@@ -35,7 +36,7 @@ library
3536 other-extensions: TemplateHaskell
3637 ghc-options:
3738 -Wall -Werror -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O2
38− -fno-warn-unused-do-bind -funbox-strict-fields -fprof-auto-calls
39+ -fno-warn-unused-do-bind -funbox-strict-fields -fprof-auto-calls
3940
4041executable obelisk-asset-manifest-generate
4142 hs-source-dirs: src-bin
@@ -44,3 +45,12 @@ executable obelisk-asset-manifest-generate
4445 base
4546 , obelisk-asset-manifest
4647 , text
48+
49+executable obelisk-asset-th-generate
50+ hs-source-dirs: src-bin
51+ main-is: static-th.hs
52+ build-depends:
53+ base
54+ , obelisk-asset-manifest
55+ , filepath
56+ , text
lib/asset/manifest/src-bin/static-th.hsnew file+41
@@ -0,0 +1,41 @@
1+import qualified Data.Text as T
2+import Obelisk.Asset.Cabal
3+import Obelisk.Asset.Gather
4+import Obelisk.Asset.Symlink
5+import System.Environment
6+import System.FilePath
7+
8+main :: IO ()
9+main = do
10+ --TODO: Usage
11+ [root, haskellTarget, packageName, moduleName, fileTarget] <- getArgs
12+ paths <- gatherHashedPaths root
13+ writeCabalProject haskellTarget $ SimplePkg
14+ { _simplePkg_name = T.pack packageName
15+ , _simplePkg_moduleName = T.pack moduleName
16+ , _simplePkg_dependencies = map T.pack
17+ [ "base"
18+ , "obelisk-asset-manifest"
19+ , "template-haskell"
20+ ]
21+ , _simplePkg_moduleContents = T.pack $ unlines
22+ [ "{-# Language CPP #-}"
23+ , "{-|"
24+ , " Description:"
25+ , " Automatically generated module that provides the 'static' TH function"
26+ , " to generate paths to static assets."
27+ , "-}"
28+ , "module " <> moduleName <> " ( static ) where"
29+ , ""
30+ , "import Obelisk.Asset.TH"
31+ , "import Language.Haskell.TH"
32+ , ""
33+ , "static :: FilePath -> Q Exp"
34+ , "#ifdef OBELISK_ASSET_PASSTHRU"
35+ , "static = staticAssetRaw"
36+ , "#else"
37+ , "static = staticAssetHashed " <> show root
38+ , "#endif"
39+ ]
40+ }
41+ copyAndSymlink paths root fileTarget
lib/asset/manifest/src/Obelisk/Asset/Cabal.hsnew file+42
@@ -0,0 +1,42 @@
1+{-# Language OverloadedStrings #-}
2+module Obelisk.Asset.Cabal where
3+
4+import qualified Data.List as L
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import qualified Data.Text.IO as T
8+import System.Directory
9+import System.FilePath
10+
11+-- | A generated single-module package
12+data SimplePkg = SimplePkg
13+ { _simplePkg_name :: Text
14+ , _simplePkg_moduleName :: Text
15+ , _simplePkg_moduleContents :: Text
16+ , _simplePkg_dependencies :: [Text]
17+ }
18+
19+cabalFile :: SimplePkg -> Text
20+cabalFile (SimplePkg packageName moduleName _ deps) = T.unlines
21+ [ "name: " <> packageName
22+ , "version: 0"
23+ , "cabal-version: >= 1.2"
24+ , "build-type: Simple"
25+ , ""
26+ , "library"
27+ , " hs-source-dirs: src"
28+ , " build-depends:"
29+ , " " <> T.intercalate ", " deps
30+ , " exposed-modules: " <> moduleName
31+ ]
32+
33+writeCabalProject :: FilePath -> SimplePkg -> IO ()
34+writeCabalProject target pkg = do
35+ createDirectoryIfMissing True target
36+ T.writeFile (target </> T.unpack (_simplePkg_name pkg) <.> "cabal") $ cabalFile pkg
37+ let moduleName = _simplePkg_moduleName pkg
38+ (modName', moduleDirPath) = case L.uncons (reverse $ T.splitOn "." moduleName) of
39+ Nothing -> error $ "writeStaticProject: invalid module name " <> T.unpack moduleName
40+ Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
41+ createDirectoryIfMissing True moduleDirPath
42+ T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") $ _simplePkg_moduleContents pkg
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+13−13
@@ -6,21 +6,21 @@ module Obelisk.Asset.Gather
66 , toHashedPath
77 ) where
88
9−import Control.DeepSeq
9+import Control.DeepSeq (force)
1010import Control.Monad (forM)
11−import Data.Bits
12−import qualified Data.ByteString.Lazy as LBS
13−import qualified Data.ByteString.Lazy.Builder as LBS
14−import Data.Char
15−import Data.Digest.Pure.SHA
11+import Data.Bits (shift, (.|.), (.&.))
12+import qualified Data.ByteString.Lazy as LBS (readFile, toStrict, ByteString, length, index)
13+import qualified Data.ByteString.Lazy.Builder as LBS (toLazyByteString, word8)
14+import Data.Char (ord)
15+import Data.Digest.Pure.SHA (bytestringDigest, sha256)
1616import Data.Map (Map)
17−import qualified Data.Map as Map
18−import qualified Data.Text as T
19−import Data.Text.Encoding
20−import qualified Data.Vector.Unboxed as UV
21−import Data.Word
22−import System.FilePath.Posix
23−import System.Directory
17+import qualified Data.Map as Map (singleton)
18+import qualified Data.Text as T (unpack)
19+import Data.Text.Encoding (decodeUtf8)
20+import qualified Data.Vector.Unboxed as UV ((!), fromList)
21+import Data.Word (Word8)
22+import System.FilePath.Posix ((</>), splitFileName, normalise)
23+import System.Directory (listDirectory, doesFileExist)
2424
2525-- | Given a path, recursively explore it, creating hashed paths for all files found
2626gatherHashedPaths
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−27
@@ -10,7 +10,6 @@ module Obelisk.Asset.Promoted
1010import Obelisk.Asset.Gather
1111
1212import Data.Foldable
13−import qualified Data.List as L
1413import Language.Haskell.TH (runQ, pprint)
1514import Language.Haskell.TH.Syntax hiding (lift)
1615import GHC.TypeLits
@@ -18,12 +17,11 @@ import Data.Sequence (Seq)
1817import qualified Data.Sequence as Seq
1918import Control.Monad.Trans.Writer
2019import System.FilePath
21−import System.Directory
2220import Data.Text (Text)
2321import qualified Data.Text as T
24−import qualified Data.Text.IO as T
2522import Data.Map (Map)
2623import qualified Data.Map as Map
24+import Obelisk.Asset.Cabal (writeCabalProject, SimplePkg(..))
2725
2826data StaticConfig = StaticConfig
2927 { _staticConfig_packageName :: Text --TODO: Better type
@@ -32,31 +30,14 @@ data StaticConfig = StaticConfig
3230
3331writeStaticProject :: Map FilePath FilePath -> FilePath -> StaticConfig -> IO ()
3432writeStaticProject paths target cfg = do
35− createDirectoryIfMissing True target
36− T.writeFile (target </> T.unpack (_staticConfig_packageName cfg) <.> "cabal") $ staticCabalFile cfg
3733 let modName = _staticConfig_moduleName cfg
38− (modName', moduleDirPath) = case L.uncons (reverse $ T.splitOn "." modName) of
39− Nothing -> error $ "writeStaticProject: invalid module name " <> T.unpack modName
40− Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
41− createDirectoryIfMissing True moduleDirPath
4234 modContents <- staticModuleFile modName paths
43− T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") modContents
44−
45−staticCabalFile :: StaticConfig -> Text
46−staticCabalFile cfg = T.unlines
47− [ "name: " <> _staticConfig_packageName cfg
48− , "version: 0"
49− , "cabal-version: >= 1.2"
50− , "build-type: Simple"
51− , ""
52− , "library"
53− , " hs-source-dirs: src"
54− , " build-depends:"
55− , " base,"
56− , " ghc-prim,"
57− , " text"
58− , " exposed-modules: " <> _staticConfig_moduleName cfg
59− ]
35+ writeCabalProject target $ SimplePkg
36+ { _simplePkg_name = _staticConfig_packageName cfg
37+ , _simplePkg_moduleName = _staticConfig_moduleName cfg
38+ , _simplePkg_moduleContents = modContents
39+ , _simplePkg_dependencies = ["base", "ghc-prim", "text"]
40+ }
6041
6142staticModuleFile :: Text -> Map FilePath FilePath -> IO Text
6243staticModuleFile moduleName paths = do
@@ -69,7 +50,7 @@ staticModuleFile moduleName paths = do
6950 , "{-# LANGUAGE OverloadedStrings #-}"
7051 , "{-# LANGUAGE ScopedTypeVariables #-}"
7152 , "{-# LANGUAGE TypeApplications #-}"
72− , "module " <> moduleName <> " where"
53+ , "module " <> moduleName <> " {-# DEPRECATED \"Generate this module with the 'obelisk-asset-th-generate' executable instead.\" #-} where"
7354 , ""
7455 , "import qualified GHC.Types"
7556 , "import Data.Text (Text)"
lib/asset/manifest/src/Obelisk/Asset/TH.hs+25−3
@@ -1,14 +1,36 @@
1+{-|
2+Description:
3+ Template Haskell for generating asset paths.
4+-}
15module Obelisk.Asset.TH
26 ( assetPath
7+ , staticAssetRaw
8+ , staticAssetHashed
39 ) where
410
511import Obelisk.Asset.Gather
612
13+import Language.Haskell.TH
714import Language.Haskell.TH.Syntax
815import System.FilePath.Posix
916
17+-- | Produces the hashed path of a file
18+hashedAssetFilePath :: FilePath -> FilePath -> Q FilePath
19+hashedAssetFilePath root relativePath = do
20+ qAddDependentFile $ root </> relativePath
21+ runIO (toHashedPath root relativePath)
22+
1023-- | Produces a string literal with the hashed path of the file
1124assetPath :: FilePath -> FilePath -> Q Exp
12−assetPath root relativePath = do
13− qAddDependentFile $ root </> relativePath
14− LitE . StringL <$> runIO (toHashedPath root relativePath)
25+assetPath root relativePath =
26+ LitE . StringL <$> hashedAssetFilePath root relativePath
27+
28+staticPrefix :: FilePath
29+staticPrefix = "/static"
30+
31+staticAssetRaw :: FilePath -> Q Exp
32+staticAssetRaw fp = returnQ $ LitE $ StringL $ staticPrefix </> fp
33+
34+staticAssetHashed :: FilePath -> FilePath -> Q Exp
35+staticAssetHashed root fp = do
36+ LitE . StringL . (staticPrefix </>) <$> hashedAssetFilePath root fp
lib/cliapp/src/Obelisk/CliApp/Logging.hs+5−1
@@ -44,7 +44,7 @@ import Data.Text (Text)
4444import qualified Data.Text as T
4545import qualified Data.Text.IO as T
4646import GHC.IO.Encoding.Types
47−import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
47+import System.Console.ANSI (Color (..), ColorIntensity (Vivid),
4848 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
4949 SGR (SetColor, SetConsoleIntensity), clearLine)
5050import System.Exit (ExitCode (..))
@@ -184,6 +184,8 @@ writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure (
184184 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185185 | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186186 | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity == Notice = TS.putStrWithSGR noticeColors h withNewLine s
188+ | not noColor && severity == Informational = TS.putStrWithSGR infoColors h withNewLine s
187189 | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188190 | otherwise = liftIO $ putFn s
189191
@@ -191,6 +193,8 @@ writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure (
191193 h = if severity <= Error then stderr else stdout
192194 errorColors = [SetColor Foreground Vivid Red]
193195 warningColors = [SetColor Foreground Vivid Yellow]
196+ infoColors = [SetColor Foreground Vivid Green]
197+ noticeColors = [SetColor Foreground Vivid Blue]
194198 debugColors = [SetConsoleIntensity FaintIntensity]
195199
196200-- | Allow the user to immediately switch to verbose logging upon pressing a particular key.
lib/command/obelisk-command.cabal+3
@@ -20,6 +20,7 @@ library
2020 , exceptions
2121 , extra
2222 , filepath
23+ , fsnotify
2324 , git
2425 , github
2526 , here
@@ -39,6 +40,8 @@ library
3940 , optparse-applicative
4041 , placeholders
4142 , process
43+ , reflex
44+ , reflex-fsnotify
4245 , shell-escape
4346 , temporary
4447 , terminal-size
lib/command/src/Obelisk/Command/Project.hs+87−8
@@ -1,6 +1,9 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+
47module Obelisk.Command.Project
58 ( InitSource (..)
69 , findProjectObeliskCommand
@@ -16,12 +19,17 @@ module Obelisk.Command.Project
1619 , toObeliskDir
1720 , withProjectRoot
1821 , bashEscape
22+ , getHaskellManifestProjectPath
23+ , AssetSource(..)
24+ , describeImpureAssetSource
25+ , watchStaticFilesDerivation
1926 ) where
2027
2128import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
22−import Control.Lens ((.~), (?~), (<&>))
29+import Control.Lens ((.~), (?~), (<&>), (^.), _3)
2330import Control.Monad
2431import Control.Monad.IO.Class (liftIO)
32+import Control.Monad.Log
2533import Control.Monad.State
2634import qualified Data.Aeson as Json
2735import qualified Data.ByteString.UTF8 as BSU
@@ -34,20 +42,25 @@ import Data.Text (Text)
3442import qualified Data.Text as T
3543import Data.Text.Encoding (decodeUtf8, encodeUtf8)
3644import Data.Traversable (for)
45+import Reflex
46+import Reflex.FSNotify
47+import Reflex.Host.Headless
3748import System.Directory
3849import System.Environment (lookupEnv)
3950import System.FilePath
51+import System.FSNotify (defaultConfig)
4052import System.IO.Temp
4153import System.IO.Unsafe (unsafePerformIO)
4254import System.PosixCompat.Files
4355import System.PosixCompat.Types
4456import System.PosixCompat.User
57+import qualified System.Process as Proc
4558import Text.ShellEscape (bash, bytes)
4659
4760import GitHub.Data.GitData (Branch)
4861import GitHub.Data.Name (Name)
4962
50−import Obelisk.App (MonadObelisk)
63+import Obelisk.App (MonadObelisk, runObelisk, getObelisk)
5164import Obelisk.CliApp
5265import Obelisk.Command.Nix
5366import Obelisk.Command.Thunk
@@ -334,7 +347,21 @@ nixShellWithHoogle root isPure shell' command = do
334347 \})).project.shells.${shell}"
335348 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
336349
337−findProjectAssets :: MonadObelisk m => FilePath -> m Text
350+-- | Describes the provenance of static assets (i.e., are they the result of a derivation
351+-- that was built, or just a folder full of files.
352+data AssetSource = AssetSource_Derivation
353+ | AssetSource_Files
354+ deriving (Eq)
355+
356+-- | Some log messages to make it easier to tell where static files are coming from
357+describeImpureAssetSource :: AssetSource -> Text -> Text
358+describeImpureAssetSource src path = case src of
359+ AssetSource_Files -> "Assets impurely loaded from: " <> path
360+ AssetSource_Derivation -> "Assets derivation built and impurely loaded from: " <> path
361+
362+-- | Determine where the static files of a project are and whether they're plain files or a derivation.
363+-- If they are a derivation, that derivation will be built.
364+findProjectAssets :: MonadObelisk m => FilePath -> m (AssetSource, Text)
338365findProjectAssets root = do
339366 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
340367 proc nixExePath
@@ -346,10 +373,62 @@ findProjectAssets root = do
346373 ]
347374 -- Check whether the impure static files are a derivation (and so must be built)
348375 if isDerivation == "1"
349− then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
350− proc nixBuildExePath
351− [ "--no-out-link"
376+ then do
377+ _ <- buildStaticFilesDerivationAndSymlink root
378+ pure (AssetSource_Derivation, T.pack $ root </> "static.out")
379+ else fmap (AssetSource_Files,) $ readProcessAndLogStderr Debug $ setCwd (Just root) $
380+ proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
381+
382+-- | Get the nix store path to the generated static asset manifest module (e.g., "obelisk-generated-static")
383+getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text
384+getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
385+ proc nixBuildExePath
386+ [ "--no-out-link"
387+ , "-E"
388+ , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)"
389+ ]
390+
391+-- | Watch the project directory for file changes and check whether those file changes
392+-- cause changes in the static files nix derivation. If so, rebuild it.
393+watchStaticFilesDerivation
394+ :: (MonadIO m, MonadObelisk m)
395+ => FilePath
396+ -> m ()
397+watchStaticFilesDerivation root = do
398+ ob <- getObelisk
399+ drv0 <- showDerivation
400+ liftIO $ runHeadlessApp $ do
401+ pb <- getPostBuild
402+ checkForChanges <- watchDirectoryTree defaultConfig (root <$ pb) (const True)
403+ drv <- performEvent $ ffor checkForChanges $ \_ ->
404+ liftIO $ runObelisk ob showDerivation
405+ drvs <- foldDyn (\new (_, old, _) -> (old, new, old /= new)) (drv0, drv0, False) drv
406+ out <- throttleBatchWithLag
407+ (\e -> performEvent $ ffor e $ \_ -> liftIO $ runObelisk ob $
408+ buildStaticFilesDerivationAndSymlink root >> pure ())
409+ (void $ ffilter (^._3) $ updated drvs)
410+ performEvent_ $ ffor out $ \_ -> runObelisk ob $
411+ putLog Notice "Static assets rebuilt and symlinked to static.out"
412+ pure never
413+ where
414+ showDerivation :: MonadObelisk m => m Text
415+ showDerivation = readProcessAndLogStderr Debug $
416+ setCwd (Just root) $ ProcessSpec
417+ { _processSpec_createProcess = Proc.proc nixExePath
418+ [ "show-derivation"
419+ , "-f", "."
420+ , "passthru.staticFilesImpure"
421+ ]
422+ , _processSpec_overrideEnv = Nothing
423+ }
424+
425+buildStaticFilesDerivationAndSymlink :: MonadObelisk m => FilePath -> m Text
426+buildStaticFilesDerivationAndSymlink root = readProcessAndLogStderr Debug $
427+ setCwd (Just root) $ ProcessSpec
428+ { _processSpec_createProcess = Proc.proc
429+ nixBuildExePath
430+ [ "-o", "static.out"
352431 , "-E", "(import ./. {}).passthru.staticFilesImpure"
353432 ]
354− else readProcessAndLogStderr Debug $ setCwd (Just root) $
355− proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
433+ , _processSpec_overrideEnv = Nothing
434+ }
lib/command/src/Obelisk/Command/Run.hs+28−10
@@ -14,7 +14,8 @@ module Obelisk.Command.Run where
1414import Control.Arrow ((&&&))
1515import Control.Exception (Exception, bracket)
1616import Control.Lens (ifor, (.~), (&))
17−import Control.Monad (filterM)
17+import Control.Concurrent (forkIO)
18+import Control.Monad (filterM, void)
1819import Control.Monad.Except (runExceptT, throwError)
1920import Control.Monad.IO.Class (liftIO)
2021import Control.Monad.Reader (MonadIO)
@@ -63,21 +64,23 @@ import System.FilePath
6364import qualified System.Info
6465import System.IO.Temp (withSystemTempDirectory)
6566
66−import Obelisk.App (MonadObelisk)
67+import Obelisk.App (MonadObelisk, getObelisk, runObelisk)
6768import Obelisk.CliApp (
6869 Severity (..),
6970 failWith,
71+ getCliConfig,
7072 proc,
7173 putLog,
7274 readCreateProcessWithExitCode,
7375 readProcessAndLogStderr,
76+ runCli,
7477 runProcess_,
7578 setCwd,
7679 setDelegateCtlc,
7780 withSpinner,
7881 )
7982import Obelisk.Command.Nix
80−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets, bashEscape)
83+import Obelisk.Command.Project
8184import Obelisk.Command.Thunk (attrCacheFileName)
8285import Obelisk.Command.Utils (findExePath, ghcidExePath)
8386
@@ -144,8 +147,8 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
144147 , _target_attr = Just "__unstable__.profiledObRun"
145148 , _target_expr = Nothing
146149 }
147− assets <- findProjectAssets root
148− putLog Debug $ "Assets impurely loaded from: " <> assets
150+ (assetType, assets) <- findProjectAssets root
151+ putLog Debug $ describeImpureAssetSource assetType assets
149152 time <- liftIO getCurrentTime
150153 let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
151154 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
@@ -163,9 +166,17 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
163166run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
164167run root interpretPaths = do
165168 pkgs <- getParsedLocalPkgs root interpretPaths
166− assets <- findProjectAssets root
167− putLog Debug $ "Assets impurely loaded from: " <> assets
168− ghciArgs <- getGhciSessionSettings pkgs root True
169+ (assetType, assets) <- findProjectAssets root
170+ manifestPkg <- parsePackagesOrFail . (:[]) . T.unpack =<< getHaskellManifestProjectPath root
171+ putLog Debug $ describeImpureAssetSource assetType assets
172+ case assetType of
173+ AssetSource_Derivation -> do
174+ cliConfig <- getCliConfig
175+ ob <- getObelisk
176+ putLog Debug "Starting static file derivation watcher..."
177+ void $ liftIO $ forkIO $ runCli cliConfig $ runObelisk ob $ watchStaticFilesDerivation root
178+ _ -> pure ()
179+ ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkg) root True
169180 freePort <- getFreePort
170181 withGhciScriptArgs pkgs $ \dotGhciArgs -> do
171182 runGhcid root True (ghciArgs <> dotGhciArgs) pkgs $ Just $ unwords
@@ -222,7 +233,12 @@ getLocalPkgs root interpretPaths = do
222233 let rootsAndExclusions = calcIntepretFinds "" interpretPaths
223234
224235 fmap fold $ for (MMap.toAscList rootsAndExclusions) $ \(interpretPathRoot, exclusions) ->
225− let allExclusions = obeliskPackageExclusions <> exclusions <> Set.singleton ("*" </> attrCacheFileName)
236+ let allExclusions = obeliskPackageExclusions
237+ <> exclusions
238+ <> Set.singleton ("*" </> attrCacheFileName)
239+ <> Set.singleton ("*" </> "lib/asset/manifest") -- NB: obelisk-asset-manifest is excluded because it generates
240+ -- a module that in turn imports it. This will cause ob run to
241+ -- fail in its current implementation.
226242 in fmap (Set.fromList . map normalise) $ runFind $
227243 ["-L", interpretPathRoot, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
228244 <> concat [["-not", "-path", p </> "*"] | p <- toList allExclusions]
@@ -400,7 +416,8 @@ withGhciScriptArgs
400416 => f CabalPackageInfo -- ^ List of packages to load into ghci
401417 -> ([String] -> m ()) -- ^ Action to run with the extra ghci arguments
402418 -> m ()
403−withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp -> f ["-XNoImplicitPrelude", "-ghci-script", fp]
419+withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp ->
420+ f ["-XNoImplicitPrelude", "-ghci-script", fp]
404421 where
405422 -- These lines must be first and allow the session to support a custom Prelude when @-XNoImplicitPrelude@
406423 -- is passed to the ghci session.
@@ -458,6 +475,7 @@ getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
458475
459476 pure
460477 $ baseGhciOptions
478+ <> ["-DOBELISK_ASSET_PASSTHRU"] -- For passthrough static assets
461479 <> ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
462480 <> concatMap (\p -> ["-optF", p]) pkgFiles
463481 <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
skeleton/frontend/src/Frontend.hs+3−4
@@ -1,6 +1,5 @@
1−{-# LANGUAGE DataKinds #-}
21{-# LANGUAGE OverloadedStrings #-}
3−{-# LANGUAGE TypeApplications #-}
2+{-# LANGUAGE TemplateHaskell #-}
43
54module Frontend where
65
@@ -27,7 +26,7 @@ frontend :: Frontend (R FrontendRoute)
2726frontend = Frontend
2827 { _frontend_head = do
2928 el "title" $ text "Obelisk Minimal Example"
30− elAttr "link" ("href" =: static @"main.css" <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
29+ elAttr "link" ("href" =: $(static "main.css") <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
3130 , _frontend_body = do
3231 el "h1" $ text "Welcome to Obelisk!"
3332 el "p" $ text $ T.pack commonStuff
@@ -38,7 +37,7 @@ frontend = Frontend
3837 -- print "Hello, World!" on the client.
3938 prerender_ blank $ liftJSM $ void $ eval ("console.log('Hello, World!')" :: T.Text)
4039
41− elAttr "img" ("src" =: static @"obelisk.jpg") blank
40+ elAttr "img" ("src" =: $(static "obelisk.jpg")) blank
4241 el "div" $ do
4342 exampleConfig <- getConfig "common/example"
4443 case exampleConfig of