diff --git a/.packcheck.ignore b/.packcheck.ignore index 7908ab84..0fb2dcf8 100644 --- a/.packcheck.ignore +++ b/.packcheck.ignore @@ -17,7 +17,6 @@ hyperbole-oauth2/ .gitignore .hlint.yaml .packcheck.ignore -DOCTODO.md Dockerfile bin/dev bin/docgen diff --git a/README.md b/README.md index ca31b9b9..9ff1d331 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Create interactive HTML applications with type-safe serverside Haskell. Inspired module Main where import Data.Text (Text) -import Web.Atomic.CSS +import Web.Atomic.CSS (border, (~)) import Web.Hyperbole main :: IO () @@ -78,6 +78,7 @@ Add hyperbole and text as dependencies to the `.cabal` file: build-depends: base , hyperbole + , atomic-css , text default-language: GHC2021 @@ -97,8 +98,8 @@ Learn More -* [Local Development](./docs/dev.md) * [Comparison with Similar Frameworks](./docs/comparison.md) +* [Local Development](./docs/dev.md) * [Using NIX](./docs/nix.md) In the Wild diff --git a/bin/docgen b/bin/docgen deleted file mode 100755 index 4697c127..00000000 --- a/bin/docgen +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -e - -hpack demo -hpack docs -cabal run docs - -cd /tmp/hyperbole -cabal haddock diff --git a/cabal.project b/cabal.project index 2f98c0cf..99939e47 100644 --- a/cabal.project +++ b/cabal.project @@ -3,5 +3,4 @@ multi-repl: True packages: . ./demo/ - ./docs/ ./hyperbole-oauth2/ diff --git a/docs/Main.hs b/docs/Docgen.hs similarity index 67% rename from docs/Main.hs rename to docs/Docgen.hs index eef6d1bd..7e4f1605 100644 --- a/docs/Main.hs +++ b/docs/Docgen.hs @@ -1,31 +1,26 @@ -{-# LANGUAGE QuasiQuotes #-} - module Main where -import Control.Exception (SomeException, try) import Data.Char (isAlpha, isSpace) import Data.String.Conversions (cs) import Data.Text (Text) import Data.Text qualified as T import Data.Text.IO qualified as T -import Distribution.Simple.Utils (copyDirectoryRecursive) -import Distribution.Verbosity (verbose) import System.Directory +import System.Environment (getArgs) import System.FilePath -import Web.Hyperbole.Data.URI - --- import Control.Applicative ((<|>)) --- import Web.Hyperbole.Route (matchRoute) main :: IO () main = do - let tmpDir = "/tmp/hyperbole" - copyExtraFilesTo tmpDir - expandSourcesTo tmpDir - putStrLn $ "COPY RECURSIVE: " <> (tmpDir <> "docs") - copyDirectoryRecursive verbose "./docs" (tmpDir "docs") - copyDirectoryRecursive verbose "./demo" (tmpDir "demo") + args <- getArgs + case args of + [original, input, output] -> do + src <- readSource input + expanded <- expandFile src + let linePragma = T.pack $ "{-# LINE 1 \"" ++ original ++ "\" #-}" + let final = SourceCode [linePragma] <> expanded + T.writeFile output $ T.unlines final.lines + _ -> error "Usage (Hyperbole Internal Only): docgen src/MyModule.hs /tmp/input/MyModule.hs /build/output/Original.hs" test :: IO () @@ -35,35 +30,32 @@ test = do mapM_ print lns -expandSourcesTo :: FilePath -> IO () -expandSourcesTo tmpDir = do - allFiles <- relativeSourceFiles "./src" - -- mapM_ (putStrLn . ("SOURCE " <>)) allFiles - mapM_ (expandAndCopyFileTo tmpDir) allFiles - - -copyExtraFilesTo :: FilePath -> IO () -copyExtraFilesTo tmpDir = do - createDirectoryIfMissing True tmpDir - copyFile "./cabal.project" (tmpDir "cabal.project") - copyFile "./hyperbole.cabal" (tmpDir "hyperbole.cabal") - copyFile "./README.md" (tmpDir "README.md") - copyFile "./CHANGELOG.md" (tmpDir "CHANGELOG.md") - copyFile "./LICENSE" (tmpDir "LICENSE") - createDirectoryIfMissing True (tmpDir "client/dist") - copyFile "./client/dist/hyperbole.js" (tmpDir "client/dist/hyperbole.js") - copyFile "./client/dist/hyperbole.js.map" (tmpDir "client/dist/hyperbole.js.map") - createDirectoryIfMissing True (tmpDir "client/util") - copyFile "./client/util/live-reload.js" (tmpDir "client/util/live-reload.js") - - -expandAndCopyFileTo :: FilePath -> FilePath -> IO () -expandAndCopyFileTo tmpDir pth = do - putStrLn $ "EXPANDING " <> pth - src <- readSource pth - expanded <- expandFile src - writeSource tmpDir pth expanded - +-- expandSourcesTo :: FilePath -> IO () +-- expandSourcesTo tmpDir = do +-- allFiles <- relativeSourceFiles "./src" +-- -- mapM_ (putStrLn . ("SOURCE " <>)) allFiles +-- mapM_ (expandAndCopyFileTo tmpDir) allFiles + +-- copyExtraFilesTo :: FilePath -> IO () +-- copyExtraFilesTo tmpDir = do +-- createDirectoryIfMissing True tmpDir +-- copyFile "./cabal.project" (tmpDir "cabal.project") +-- copyFile "./hyperbole.cabal" (tmpDir "hyperbole.cabal") +-- copyFile "./README.md" (tmpDir "README.md") +-- copyFile "./CHANGELOG.md" (tmpDir "CHANGELOG.md") +-- copyFile "./LICENSE" (tmpDir "LICENSE") +-- createDirectoryIfMissing True (tmpDir "client/dist") +-- copyFile "./client/dist/hyperbole.js" (tmpDir "client/dist/hyperbole.js") +-- copyFile "./client/dist/hyperbole.js.map" (tmpDir "client/dist/hyperbole.js.map") +-- createDirectoryIfMissing True (tmpDir "client/util") +-- copyFile "./client/util/live-reload.js" (tmpDir "client/util/live-reload.js") + +-- expandAndCopyFileTo :: FilePath -> FilePath -> IO () +-- expandAndCopyFileTo tmpDir pth = do +-- putStrLn $ "EXPANDING " <> pth +-- src <- readSource pth +-- expanded <- expandFile src +-- writeSource tmpDir pth expanded readSource :: FilePath -> IO SourceCode readSource pth = do @@ -82,27 +74,26 @@ writeSource tmpDir relPath src = do dropWhile (== '/') . dropWhile (== '.') -relativeSourceFiles :: FilePath -> IO [FilePath] -relativeSourceFiles dir = do - contents <- tryDirectory dir - let folders = filter isFolder contents - let files = filter isSourceFile contents - - files' <- mapM (relativeSourceFiles . addDir) folders - - pure $ fmap addDir files <> mconcat files' - where - isSourceFile pth = takeExtension pth == ".hs" - isFolder pth = takeExtension pth == "" - addDir = (dir ) - tryDirectory pth = do - res <- try $ listDirectory pth - case res of - Left (_ :: SomeException) -> do - putStrLn $ "SKIPPED" <> pth - pure [] - Right files -> pure files - +-- relativeSourceFiles :: FilePath -> IO [FilePath] +-- relativeSourceFiles dir = do +-- contents <- tryDirectory dir +-- let folders = filter isFolder contents +-- let files = filter isSourceFile contents +-- +-- files' <- mapM (relativeSourceFiles . addDir) folders +-- +-- pure $ fmap addDir files <> mconcat files' +-- where +-- isSourceFile pth = takeExtension pth == ".hs" +-- isFolder pth = takeExtension pth == "" +-- addDir = (dir ) +-- tryDirectory pth = do +-- res <- try $ listDirectory pth +-- case res of +-- Left (_ :: SomeException) -> do +-- putStrLn $ "SKIPPED" <> pth +-- pure [] +-- Right files -> pure files data Macro = Embed @@ -111,6 +102,7 @@ data Macro } deriving (Eq) newtype SourceCode = SourceCode {lines :: [Text]} + deriving newtype (Monoid, Semigroup) instance Show Macro where -- show (Example p) = "Example " <> show p show (Embed mn def) = "Embed " <> show mn <> " " <> show def @@ -180,9 +172,8 @@ expandLine line = do -- Nothing -> fail $ "Could not find example: " <> cs (pathToText False p) -- Just r -> pure r -exampleBaseURI :: URI -exampleBaseURI = [uri|https://hyperbole.live|] - +-- exampleBaseURI :: URI +-- exampleBaseURI = [uri|https://hyperbole.live|] modulePath :: ModuleName -> FilePath modulePath (ModuleName mn) = cs $ T.replace "." "/" mn <> ".hs" @@ -191,7 +182,7 @@ modulePath (ModuleName mn) = cs $ T.replace "." "/" mn <> ".hs" expandEmbed :: ModuleName -> Text -> TopLevelDefinition -> IO [Text] expandEmbed mn pfx def = do let src = modulePath mn - putStrLn $ " embed: " <> src + -- putStrLn $ " embed: " <> src source <- T.readFile $ "./demo/" <> src expanded <- requireTopLevel def (SourceCode $ T.lines source) pure $ fmap markupLine expanded diff --git a/docs/dev.md b/docs/dev.md index c89ae73a..cbd9d01e 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -1,6 +1,9 @@ Local Development ================= + +# JavaScript Client Development + Download and install [NPM](https://nodejs.org/en/download). On a mac, can be installed via homebrew: ```sh @@ -38,11 +41,12 @@ cd client npm run dev ``` -Run examples + +# Haskell Server Development + +Run demo locally. Then visit ``` -# demo needs to have demo/static and client/dist as relative paths -cd /hyperbole cabal run demo ``` @@ -80,8 +84,16 @@ cabal test ### File watching -Run tests, then recompile everything on file change and restart examples +Run tests, then recompile both client and server on file change, then restart demo ``` bin/dev ``` + +### Haskell Language Server + +We use a custom preprocessor to embed compiler-checked examples into Haddock. Everything is automatic with Cabal, but HLS doesn't support it yet. Run this command to get HLS working: + +``` +cabal install hyperbole:docgen --overwrite-policy=always +``` diff --git a/docs/docgen.cabal b/docs/docgen.cabal deleted file mode 100644 index 40fb02ec..00000000 --- a/docs/docgen.cabal +++ /dev/null @@ -1,50 +0,0 @@ -cabal-version: 2.2 - --- This file has been generated from package.yaml by hpack version 0.37.0. --- --- see: https://github.com/sol/hpack - -name: docgen -version: 0.6.1 -synopsis: Interactive HTML apps using type-safe serverside Haskell -description: Interactive HTML applications using type-safe serverside Haskell. Inspired by HTMX, Elm, and Phoenix LiveView -category: Web, Network -homepage: https://github.com/seanhess/hyperbole -bug-reports: https://github.com/seanhess/hyperbole/issues -author: Sean Hess -maintainer: seanhess@gmail.com -license: BSD-3-Clause -build-type: Simple - -source-repository head - type: git - location: https://github.com/seanhess/hyperbole - -executable docgen - main-is: Main.hs - other-modules: - Paths_docgen - autogen-modules: - Paths_docgen - hs-source-dirs: - ./ - default-extensions: - OverloadedStrings - OverloadedRecordDot - DuplicateRecordFields - NoFieldSelectors - TypeFamilies - DataKinds - DerivingStrategies - DeriveAnyClass - ghc-options: -Wall -fdefer-typed-holes - build-depends: - Cabal - , base - , casing - , directory - , filepath - , hyperbole - , string-conversions - , text - default-language: GHC2021 diff --git a/docs/package.yaml b/docs/package.yaml deleted file mode 100644 index 2d77fc92..00000000 --- a/docs/package.yaml +++ /dev/null @@ -1,44 +0,0 @@ -name: docgen -version: 0.6.1 -synopsis: Interactive HTML apps using type-safe serverside Haskell -homepage: https://github.com/seanhess/hyperbole -github: seanhess/hyperbole -license: BSD-3-Clause -author: Sean Hess -maintainer: seanhess@gmail.com -category: Web, Network -description: Interactive HTML applications using type-safe serverside Haskell. Inspired by HTMX, Elm, and Phoenix LiveView - -language: GHC2021 - -ghc-options: - - -Wall - - -fdefer-typed-holes - -default-extensions: - - OverloadedStrings - - OverloadedRecordDot - - DuplicateRecordFields - - NoFieldSelectors - - TypeFamilies - - DataKinds - - DerivingStrategies - - DeriveAnyClass - -dependencies: - - base - - directory - - filepath - - text - - string-conversions - - Cabal - - hyperbole - - casing - -executables: - docgen: - main: Main.hs - source-dirs: - - . - dependencies: [] - diff --git a/flake.nix b/flake.nix index 7f899e64..8438d6fa 100644 --- a/flake.nix +++ b/flake.nix @@ -118,13 +118,14 @@ ]; }; - docgen-src = nix-filter.lib { + docs-hs-src = nix-filter.lib { root = ./docs; - include = [ - (nix-filter.lib.matchExt "hs") - (nix-filter.lib.matchExt "md") - ./docs/docgen.cabal - ]; + include = [ (nix-filter.lib.matchExt "hs") ]; + }; + + docs-md-src = nix-filter.lib { + root = ./docs; + include = [ (nix-filter.lib.matchExt "md") ]; }; oauth2-src = nix-filter.lib { @@ -139,12 +140,30 @@ # Merges filtered `demo` + `docs` sources into `$out`. # Needed to solve `demo/docs` -> `../docs` symlink issue Nix has before. - # Named "demo" so the store path is `/nix/store/-demo`. - # That's what the `demo` expects to resolve source files. Check `demo/App/Docs/Snippet.hs` -> `localFile` - demo-docs-src = pkgs.runCommand "demo" { } '' + # Name it "demo" to have a store path `/nix/store/-demo`. + # That's what `demo-with-docs-src` expects to resolve source files. Check `demo/App/Docs/Snippet.hs` -> `localFile` + demo-with-docs-src = pkgs.runCommand "demo" { } '' mkdir -p $out/docs cp -rL ${demo-src}/. $out/ - cp -rL ${docgen-src}/. $out/docs/ + cp -rL ${docs-md-src}/. $out/docs/ + ''; + + # Merges `src` + `demo` + `docs` (.hs only) for building the library. + hyperbole-with-demo-docs-src = pkgs.runCommand packageName { } '' + mkdir -p $out/demo $out/docs + cp -rL ${src}/. $out/ + cp -rL ${demo-src}/. $out/demo/ + cp -rL ${docs-hs-src}/. $out/docs/ + ''; + + # Minimal sources for building `docgen` executable only. + # Same `packageName` ("hyperbole") since it's an executable of the library itself. + # and to match the `name` in `hyperbole.cabal`. + docgen-exe-src = pkgs.runCommand packageName { } '' + mkdir -p $out/docs + cp ${./hyperbole.cabal} $out/hyperbole.cabal + cp ${./LICENSE} $out/LICENSE + cp ${docs-hs-src}/Docgen.hs $out/docs/Docgen.hs ''; ghcVersions = [ @@ -159,9 +178,27 @@ name = "ghc${ghcVer}"; value = ( pkgs.overriddenHaskellPackages."ghc${ghcVer}".extend ( - hfinal: hprev: { - ${demoName} = hfinal.callCabal2nix demoName demo-docs-src { }; - docgen = hfinal.callCabal2nix "docgen" docgen-src { }; + hfinal: hprev: + let + # Build `docgen` executable (but no library) + # to break the circular dependency being a preprocessor AND executable of SAME package + docgen-exe = pkgs.haskell.lib.justStaticExecutables ( + pkgs.haskell.lib.dontCheck ( + pkgs.haskell.lib.overrideCabal (hfinal.callCabal2nix packageName docgen-exe-src { }) (_: { + buildTarget = "docgen"; + }) + ) + ); + in + { + # `docgen-exe` needs to be available in PATH for ALL build phases (incl. `haddock`). + ${packageName} = pkgs.haskell.lib.addBuildTool (hfinal.callCabal2nix packageName + hyperbole-with-demo-docs-src + { } + ) docgen-exe; + # `docgen` is needed in `shell` and `checks` (search for `.docgen` to see it). + docgen = hfinal.${packageName}; + ${demoName} = hfinal.callCabal2nix demoName demo-with-docs-src { }; hyperbole-oauth2 = hfinal.callCabal2nix "hyperbole-oauth2" oauth2-src { }; } ) @@ -179,7 +216,7 @@ hlint.enable = true; fourmolu.enable = true; hpack.enable = false; - nixfmt-rfc-style.enable = true; + nixfmt.enable = true; flake-checker = { enable = true; args = [ "--no-telemetry" ]; @@ -224,6 +261,8 @@ ghcid pkgs.ghciwatch pkgs.hpack + # `docgen` is required by `hls`, also by running `nix flake check` to resolve `type docgen` + (pkgs.haskell.lib.justStaticExecutables ghcPkgs."ghc${version}".docgen) ]; withHoogle = true; doBenchmark = true; diff --git a/hyperbole.cabal b/hyperbole.cabal index 8af1d478..48d2bf0b 100644 --- a/hyperbole.cabal +++ b/hyperbole.cabal @@ -93,6 +93,58 @@ library DataKinds DerivingStrategies DeriveAnyClass + ghc-options: -Wall -fdefer-typed-holes -F -pgmF=docgen + build-tool-depends: + hyperbole:docgen + build-depends: + aeson >=2.1.2.1 && <2.3 + , atomic-css ==0.2.* + , attoparsec ==0.14.* + , attoparsec-aeson >=2.1 && <2.3 + , base >=4.16 && <5 + , bytestring >=0.11 && <0.13 + , casing >=0.1.2 && <0.2 + , containers >=0.6 && <1 + , cookie >=0.4 && <0.6 + , data-default ==0.8.* + , effectful >=2.4 && <3 + , file-embed >=0.0.10 && <0.1 + , filepath >=1.4 && <2 + , http-client ==0.7.* + , http-client-tls ==0.3.* + , http-types ==0.12.* + , network >=3.1 && <4 + , network-uri >=2.6.4.1 && <2.7 + , random >=1.2 && <2 + , resourcet >=1.2 && <2 + , string-conversions ==0.4.* + , string-interpolate ==0.3.* + , text >=1.2 && <3 + , time >=1.12 && <2 + , wai >=3.2 && <4 + , wai-extra >=3.1.10 && <3.2 + , wai-websockets >=3.0 && <4 + , warp >=3.3 && <4 + , websockets >=0.12 && <0.14 + default-language: GHC2021 + +executable docgen + main-is: Docgen.hs + other-modules: + Paths_hyperbole + autogen-modules: + Paths_hyperbole + hs-source-dirs: + docs + default-extensions: + OverloadedStrings + OverloadedRecordDot + DuplicateRecordFields + NoFieldSelectors + TypeFamilies + DataKinds + DerivingStrategies + DeriveAnyClass ghc-options: -Wall -fdefer-typed-holes build-depends: aeson >=2.1.2.1 && <2.3 @@ -105,6 +157,7 @@ library , containers >=0.6 && <1 , cookie >=0.4 && <0.6 , data-default ==0.8.* + , directory >=1.2 && <1.4 , effectful >=2.4 && <3 , file-embed >=0.0.10 && <0.1 , filepath >=1.4 && <2 diff --git a/package.yaml b/package.yaml index 57fa3112..d44a11cf 100644 --- a/package.yaml +++ b/package.yaml @@ -10,6 +10,7 @@ maintainer: seanhess@gmail.com category: Web, Network description: Interactive HTML applications with type-safe serverside Haskell. Inspired by HTMX, Elm, and Phoenix LiveView. + extra-doc-files: - README.md - CHANGELOG.md @@ -73,6 +74,9 @@ dependencies: - http-client-tls >= 0.3 && < 0.4 library: + build-tools: hyperbole:docgen + ghc-options: + - -F -pgmF=docgen source-dirs: - src @@ -89,3 +93,10 @@ tests: dependencies: - hyperbole - skeletest + +executables: + docgen: + main: Docgen.hs + source-dirs: docs + dependencies: + - directory >= 1.2 && < 1.4