diff --git a/.github/mergify.yml b/.github/mergify.yml
index fe7cbd69..6e8b56c9 100644
--- a/.github/mergify.yml
+++ b/.github/mergify.yml
@@ -11,7 +11,7 @@ pull_request_rules:
conditions:
- label=merge me
- 'check-success=Frontend_tests'
- - 'check-success=Backend_tests'
+ - 'check-success~=.*Backend_tests.*'
# - '#approved-reviews-by>=1'
# merge+squash strategy
- actions:
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index e0ebcd97..9c66f428 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -52,12 +52,12 @@ jobs:
- name: Set up Haskell
id: setup-haskell
- uses: haskell/actions/setup@v2
+ uses: haskell-actions/setup@v2
with:
ghc-version: "${{ matrix.ghc }}"
cabal-version: "latest"
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: "18"
cache: "yarn"
@@ -71,6 +71,7 @@ jobs:
echo "$HOME/.cabal/bin" >> $GITHUB_PATH
echo "$HOME/.local/bin" >> $GITHUB_PATH
echo "$HOME/node_modules/.bin" >> $GITHUB_PATH
+ sudo apt install libsodium-dev
source ./environment.ci.sh
touch ~/.pgpass
chmod 0600 ~/.pgpass
@@ -83,7 +84,8 @@ jobs:
uses: actions/cache@v3.3.2
with:
path: ${{ steps.setup-haskell.outputs.cabal-store }}
- key: ghc-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
+ key: ${{ runner.os }}-ghc-${{ matrix.ghc }}-cabal-${{ hashFiles('**/plan.json') }}
+ restore-keys: ${{ runner.os }}-ghc-${{ matrix.ghc }}-
- name: Build
run: |
@@ -100,6 +102,6 @@ jobs:
migrate init "${FLORA_DB_CONNSTRING}"
migrate migrate "${FLORA_DB_CONNSTRING}" migrations
cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000"
- cabal test
+ make test
env:
PGPASSWORD: "postgres"
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index ac1daead..0f0eb1b5 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -1,12 +1,8 @@
name: Publish Docker Image
on:
- pull_request:
- paths:
- - Dockerfile
- - docker-compose.yml
- - scripts/.zshrc
push:
+ branches: ["development"]
paths:
- Dockerfile
- docker-compose.yml
@@ -18,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml
index 99c64aff..84f08c53 100644
--- a/.github/workflows/frontend.yml
+++ b/.github/workflows/frontend.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
node-version: "18"
cache: "yarn"
diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml
index 6a88119a..5b925a44 100644
--- a/.github/workflows/nix-check.yml
+++ b/.github/workflows/nix-check.yml
@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: cachix/install-nix-action@v23
+ - uses: cachix/install-nix-action@v24
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- - uses: cachix/cachix-action@v12
+ - uses: cachix/cachix-action@v13
with:
name: flora-pm
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
diff --git a/.github/workflows/test-docker-image.yml b/.github/workflows/test-docker-image.yml
new file mode 100644
index 00000000..bc31d652
--- /dev/null
+++ b/.github/workflows/test-docker-image.yml
@@ -0,0 +1,17 @@
+name: Test Docker Image
+
+on:
+ pull_request:
+ branches: ["main", "development"]
+ paths:
+ - Dockerfile
+ - docker-compose.yml
+ - scripts/.zshrc
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build the hello-docker Docker image
+ run: make docker-build
diff --git a/.gitignore b/.gitignore
index f2702754..79f6e9c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@
.hspec-failures
.vscode/
/.stack-work/
-01-index
+01-index*
<
Session.vim
_data/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bef97265..02f0aaff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# CHANGELOG
+## 1.0.14 -- 2023-12-13
+* Colourise in red deprecation markers on the package page ([#438](https://github.com/flora-pm/flora-server/pull/439))
+* Added more matches to the natural language processing category ([#440](https://github.com/flora-pm/flora-server/pull/440))
+* Allow package imports from multiple repositories ([#444](https://github.com/flora-pm/flora-server/pull/444))
+* Add a page on namespaces in the documentation ([#451](https://github.com/flora-pm/flora-server/pull/451))
+* Add initial support for hosting package tarballs ([#452](https://github.com/flora-pm/flora-server/pull/452))
+* Show depended on components in dependencies page ([#464](https://github.com/flora-pm/flora-server/pull/464))
+* Add search bar for reverse dependencies ([#476](https://github.com/flora-pm/flora-server/pull/476))
+* Support non Hackage repo URLs ([#479](https://github.com/flora-pm/flora-server/pull/479))
+* Add description field in package index ([#486](https://github.com/flora-pm/flora-server/pull/486))
+* Introduce search bar modifiers ([#487](https://github.com/flora-pm/flora-server/pull/487))
+
## 1.0.13 -- 2023-09-17
* Exclude deprecated releases from latest versions and search ([#373](https://github.com/flora-pm/flora-server/pull/373))
* Add namespace browsing ([#375](https://github.com/flora-pm/flora-server/pull/375))
@@ -11,6 +23,7 @@
* Fix the margins of the search bar in mobile view ([#430](https://github.com/flora-pm/flora-server/pull/430))
* Have proper breadcrumbs for the package page title ([#431](https://github.com/flora-pm/flora-server/pull/431))
* Configure the API gateway ([#432](https://github.com/flora-pm/flora-server/pull/432))
+* Store and show the latest revision date of releases ([#437](https://github.com/flora-pm/flora-server/pull/437))
## 1.0.12 -- 2023-04-04
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5647c554..88603571 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,7 +10,7 @@ The compiler version used is described in the `cabal.project` file.
The following Haskell command-line tools will have to be installed:
* `postgresql-migration`: To perform schema migrations
-* `fourmolu`: To style the code base. Minimum version is 0.12.0.0
+* `fourmolu`: To style the code base. Version is 0.12.0.0
* `hlint` & `apply-refact`: To enforce certain patterns in the code base ("lint")
* `cabal-fmt` and `nixfmt`: To style the cabal and nix files
* `ghcid`: To automatically reload the Haskell code base upon source changes
@@ -18,8 +18,8 @@ The following Haskell command-line tools will have to be installed:
(Some of the above packages have incompatible dependencies, so don't try to install them all at once with `cabal install`)
-You will need the [Soufflé datalog engine v2.3](https://github.com/souffle-lang/souffle/releases/tag/2.3)
-
+* [Soufflé datalog engine v2.3](https://github.com/souffle-lang/souffle/releases/tag/2.3): The datalog engine for package classification
+* `libsodium-1.0.18`: The system library that powers most of the cryptography happening in flora
* `yarn`: The tool that handles the JavaScript code bases
* `esbuild`: The tool that handles asset bundling
@@ -181,18 +181,35 @@ $ source environment.docker.sh
# You'll be in a tmux session, everything should be launched
# Visit localhost:8084 from your web browser to see if it all works.
```
+### Provisioning the database
-To provision the development database, type:
+After everything is set up, (locally or via Docker), you can start populating the database:
```bash
-$ make docker-enter
-(docker)$ source environment.docker.sh
-(docker)$ make db-drop # password is 'postgres' by default
-(docker)$ make db-setup # password is 'postgres' by default
-(docker)$ make db-provision
-# And you should be good!
+$ make db-setup
+$ make db-provision
+$ cabal run -- flora-cli create-user --admin --can-login --username "admin" \
+ --email "admin@localhost" --password "password123"
+$ make db-provision-test-packages
+```
+
+### Importing a package index
+
+The previous paragraph shows how to import test packages, but you may want to import a whole package index, for shit and giggles.
+
+You can do so with:
+
+```bash
+$ cabal run flora-cli -- import-index ~/.cabal/packages/hackage.haskell.org/01-index.tar.gz \
+ --repository hackage.haskell.org
```
+Similarly if you have the [cardano packages index](https://input-output-hk.github.io/cardano-haskell-packages/) configured, run:
+
+```bash
+$ cabal run flora-cli -- import-index ~/.cabal/packages/cardano/01-index.tar.gz \
+ --repository "cardano"
+```
### Nix
diff --git a/Dockerfile b/Dockerfile
index 40884c14..ca49a0c2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ ARG GID=1000
ARG UID=1000
ARG ghc_version=9.4.5
-ARG cabal_version=3.10.1.0
+ARG cabal_version=3.10.2.0
# generate a working directory
USER "root"
@@ -41,7 +41,6 @@ RUN chmod ugo+x /home/$USER/.cabal
USER ${USER}
RUN git config --global --add safe.directory "*"
-RUN ls -lh /home/$USER/.cabal
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
@@ -84,6 +83,7 @@ RUN make souffle
# copy and build the assets
COPY --chown=${USER} assets ./assets
+COPY --chown=${USER} docs ./docs
RUN make build-assets
USER root
diff --git a/Makefile b/Makefile
index 1414b447..56874d0d 100644
--- a/Makefile
+++ b/Makefile
@@ -42,9 +42,15 @@ db-migrate: ## Apply database migrations
db-reset: db-drop db-setup db-provision ## Reset the dev database
-db-provision: build ## Load the development data in the database
+db-provision: ## Create categories and repositories
@cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000"
@cabal run -- flora-cli provision categories
+ @cabal run -- flora-cli provision-repository --name "hackage" --url https://hackage.haskell.org \
+ --description "Central package repository"
+ @cabal run -- flora-cli provision-repository --name "cardano" --url https://input-output-hk.github.io/cardano-haskell-packages \
+ --description "Packages of the Cardano project"
+
+db-provision-test-packages: ## Load development data in the database
@cabal run -- flora-cli provision test-packages
import-from-hackage: ## Imports every cabal file from the ./index-01 directory
@@ -102,6 +108,8 @@ tags: ## Generate ctags for the project with `ghc-tags`
design-system: ## Generate the HTML components used by the design system
@cabal run -- flora-cli gen-design-system
+start-design-sysytem: ## Start storybook.js
+ @cd design; yarn storybook
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.* ?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
diff --git a/README.md b/README.md
index 26f92068..4b220740 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -18,6 +18,16 @@
+
+
+
+
+
+
+
+
+
+
@@ -32,18 +42,22 @@
-**Read More**
+## ⚡ Features
-* [Code of Conduct](./CODE_OF_CONDUCT.md)
-* [Contribution Guide](./CONTRIBUTING.md)
-* [Development Wiki](https://github.com/flora-pm/flora-server/wiki)
+* 📁 Curated category model, with elimination of duplicates
+* 🏛️ Package namespaces, so that packages with the same name can live without conflict
+* 🌓 Dark and light modes
+* 📱 Mobile user interface
-### Importing everything from Hackage
+## 🤝 Contributing
-1. Download the archive containing all packages [here](https://hackage.haskell.org/01-index.tar)
-2. Extract it in Flora's root directory. You should now have a `01-index` folder
-3. Run `make import-from-hackage`
+We welcome new contributors! Join the [Matrix chatroom](https://app.element.io/#/room/#flora-pm:matrix.org) or open a [Discussion](https://github.com/flora-pm/flora-server/discussions/new/choose).
----
+To setup a local installation, see [CONTRIBUTING.md#project-setup](https://github.com/flora-pm/flora-server/blob/development/CONTRIBUTING.md#project-setup)
-You can explore the Makefile rules by typing `make` in your shell. I promise you it's worth it.
+## 📖 Read More
+
+* [Code of Conduct](./CODE_OF_CONDUCT.md)
+* [Development Wiki](https://github.com/flora-pm/flora-server/wiki)
+
+---
diff --git a/app/cli/DesignSystem.hs b/app/cli/DesignSystem.hs
index 46f22eca..c1c80f39 100644
--- a/app/cli/DesignSystem.hs
+++ b/app/cli/DesignSystem.hs
@@ -25,6 +25,7 @@ import Flora.Model.Category
import Flora.Model.Category qualified as Category
import Flora.Model.Package
import Flora.Search
+import FloraWeb.Components.Alert qualified as Component
import FloraWeb.Components.CategoryCard qualified as Component
import FloraWeb.Components.PackageListItem qualified as Component
import FloraWeb.Components.PaginationNav qualified as Component
@@ -69,6 +70,7 @@ components =
)
, ("category-card", ComponentTitle "Category", ComponentName "CategoryCard", categoryCardExample)
, ("pagination-area", ComponentTitle "Pagination Area", ComponentName "Pagination", paginationExample)
+ , ("alerts", ComponentTitle "Alerts", ComponentName "Alert", alertsExample)
]
-----------------------
@@ -76,8 +78,9 @@ components =
-----------------------
storyTemplate :: ComponentTitle -> ComponentName -> TL.Text -> ByteString
-storyTemplate (ComponentTitle title) (ComponentName name) html =
- [fmt|
+storyTemplate (ComponentTitle title) (ComponentName name) unprocessedHtml =
+ let html = TL.replace "\n" " " unprocessedHtml
+ in [fmt|
export default {{
title: "Components/{title}"
}};
@@ -124,3 +127,11 @@ paginationExample = div_ $ do
div_ $ do
h4_ "Next button"
Component.paginationNav 32 1 (SearchPackages "text")
+
+alertsExample :: FloraHTML
+alertsExample = div_ $ do
+ div_ $ do
+ h4_ "Info alert"
+ Component.info "Info alert"
+ h4_ "Error alert"
+ Component.exception "Error alert!"
diff --git a/app/cli/Main.hs b/app/cli/Main.hs
index 01f93a51..2002fecb 100644
--- a/app/cli/Main.hs
+++ b/app/cli/Main.hs
@@ -1,15 +1,22 @@
module Main where
+import Codec.Compression.GZip qualified as GZip
+import Data.ByteString.Lazy.Char8 qualified as BS
import Data.Maybe
import Data.Password.Types
import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.Text.Display (display)
import DesignSystem (generateComponents)
+import Distribution.Version (Version)
import Effectful
import Effectful.Fail
+import Effectful.Log
import Effectful.PostgreSQL.Transact.Effect
import Effectful.Reader.Static (Reader, runReader)
-import Flora.Model.User.Query qualified as Query
+import Effectful.Time (Time, runTime)
import GHC.Generics (Generic)
+import Log qualified
import Log.Backend.StandardOutput qualified as Log
import Optics.Core
import Options.Applicative
@@ -18,7 +25,14 @@ import Flora.Environment
import Flora.Environment.Config (PoolConfig (..))
import Flora.Import.Categories (importCategories)
import Flora.Import.Package.Bulk (importAllFilesInRelativeDirectory, importFromIndex)
+import Flora.Model.BlobIndex.Update qualified as Update
+import Flora.Model.BlobStore.API
+import Flora.Model.Package (PackageName)
+import Flora.Model.PackageIndex.Query qualified as Query
+import Flora.Model.PackageIndex.Types
+import Flora.Model.PackageIndex.Update qualified as Update
import Flora.Model.User
+import Flora.Model.User.Query qualified as Query
import Flora.Model.User.Update
data Options = Options
@@ -30,8 +44,10 @@ data Command
= Provision ProvisionTarget
| CreateUser UserCreationOptions
| GenDesignSystemComponents
- | ImportPackages FilePath (Maybe Text)
- | ImportIndex FilePath (Maybe Text)
+ | ImportPackages FilePath Text
+ | ImportIndex FilePath Text
+ | ProvisionRepository Text Text Text
+ | ImportPackageTarball PackageName Version FilePath
deriving stock (Show, Eq)
data ProvisionTarget
@@ -56,6 +72,11 @@ main = do
. runReader env.dbConfig
. runDB env.pool
. runFailIO
+ . runTime
+ . ( case env.features.blobStoreImpl of
+ Just (BlobStoreFS fp) -> runBlobStoreFS fp
+ _ -> runBlobStorePure
+ )
$ runOptions result
parseOptions :: Parser Options
@@ -70,6 +91,12 @@ parseCommand =
<> command "gen-design-system" (parseGenDesignSystem `withInfo` "Generate Design System components from the code")
<> command "import-packages" (parseImportPackages `withInfo` "Import cabal packages from a directory")
<> command "import-index" (parseImportIndex `withInfo` "Import cabal packages from the index tarball")
+ <> command "provision-repository" (parseProvisionRepository `withInfo` "Create a package repository")
+ <> command
+ "import-package-tarball"
+ ( parseImportPackageTarball
+ `withInfo` "Import a single package tarball, useful for testing"
+ )
parseProvision :: Parser Command
parseProvision =
@@ -95,27 +122,40 @@ parseImportPackages :: Parser Command
parseImportPackages =
ImportPackages
<$> argument str (metavar "PATH")
- <*> optional
- ( strOption $
- long "repository"
- <> metavar ""
- <> help "Which repository we're importing from"
- )
+ <*> option str (long "repository" <> metavar "" <> help "Which repository we're importing from (hackage, cardano…)")
parseImportIndex :: Parser Command
parseImportIndex =
ImportIndex
<$> argument str (metavar "PATH")
- <*> optional
- ( strOption $
- long "repository"
- <> metavar ""
- <> help "Which repository we're importing from"
- )
-
-runOptions :: (Reader PoolConfig :> es, DB :> es, Fail :> es, IOE :> es) => Options -> Eff es ()
+ <*> option str (long "repository" <> metavar "" <> help "Which repository we're importing from (hackage, cardano…)")
+
+parseProvisionRepository :: Parser Command
+parseProvisionRepository =
+ ProvisionRepository
+ <$> option str (long "name" <> metavar "" <> help "Name of the repository")
+ <*> option str (long "url" <> metavar "" <> help "Link to the package repository")
+ <*> option str (long "description" <> metavar "" <> help "Description of the package repository" <> value "" <> showDefault)
+
+parseImportPackageTarball :: Parser Command
+parseImportPackageTarball =
+ ImportPackageTarball
+ <$> argument str (metavar "PACKAGE_NAME")
+ <*> argument str (metavar "VERSION")
+ <*> argument str (metavar "PATH")
+
+runOptions
+ :: ( Reader PoolConfig :> es
+ , DB :> es
+ , Time :> es
+ , Fail :> es
+ , IOE :> es
+ , BlobStoreAPI :> es
+ )
+ => Options
+ -> Eff es ()
runOptions (Options (Provision Categories)) = importCategories
-runOptions (Options (Provision TestPackages)) = importFolderOfCabalFiles "./test/fixtures/Cabal/" Nothing
+runOptions (Options (Provision TestPackages)) = importFolderOfCabalFiles "./test/fixtures/Cabal/" "hackage"
runOptions (Options (CreateUser opts)) = do
let username = opts ^. #username
email = opts ^. #email
@@ -135,16 +175,44 @@ runOptions (Options (CreateUser opts)) = do
runOptions (Options GenDesignSystemComponents) = generateComponents
runOptions (Options (ImportPackages path repository)) = importFolderOfCabalFiles path repository
runOptions (Options (ImportIndex path repository)) = importIndex path repository
+runOptions (Options (ProvisionRepository name url description)) = provisionRepository name url description
+runOptions (Options (ImportPackageTarball pname version path)) = importPackageTarball pname version path
+
+provisionRepository :: (DB :> es, IOE :> es) => Text -> Text -> Text -> Eff es ()
+provisionRepository name url description = do
+ Update.createPackageIndex name url description Nothing
-importFolderOfCabalFiles :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Maybe Text -> Eff es ()
+importFolderOfCabalFiles :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Text -> Eff es ()
importFolderOfCabalFiles path repository = Log.withStdOutLogger $ \appLogger -> do
user <- fromJust <$> Query.getUserByUsername "hackage-user"
- importAllFilesInRelativeDirectory appLogger (user ^. #userId) repository path True
+ mPackageIndex <- Query.getPackageIndexByName repository
+ case mPackageIndex of
+ Nothing -> error $ Text.unpack $ "Package index " <> repository <> " not found in the database!"
+ Just packageIndex ->
+ importAllFilesInRelativeDirectory appLogger (user ^. #userId) (repository, packageIndex.url) path True
-importIndex :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Maybe Text -> Eff es ()
+importIndex :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Text -> Eff es ()
importIndex path repository = Log.withStdOutLogger $ \logger -> do
user <- fromJust <$> Query.getUserByUsername "hackage-user"
- importFromIndex logger (user ^. #userId) repository path True
+ mPackageIndex <- Query.getPackageIndexByName repository
+ case mPackageIndex of
+ Nothing -> error $ Text.unpack $ "Package index " <> repository <> " not found in the database!"
+ Just packageIndex ->
+ importFromIndex logger (user ^. #userId) (repository, packageIndex.url) path True
+
+importPackageTarball
+ :: (BlobStoreAPI :> es, Time :> es, IOE :> es, DB :> es)
+ => PackageName
+ -> Version
+ -> FilePath
+ -> Eff es ()
+importPackageTarball pname version path = Log.withStdOutLogger $ \logger -> do
+ contents <- liftIO $ GZip.decompress <$> BS.readFile path
+ runLog "flora-cli" logger Log.LogTrace $ do
+ res <- Update.insertTar pname version contents
+ case res of
+ Right hash -> Log.logInfo_ $ "Insert tarball with root hash: " <> display hash
+ Left err -> Log.logAttention_ $ display err
withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc
diff --git a/assets/css/1-core/2-variables.css b/assets/css/1-core/2-variables.css
index 8bba517e..3473979f 100644
--- a/assets/css/1-core/2-variables.css
+++ b/assets/css/1-core/2-variables.css
@@ -31,6 +31,15 @@
--green-30: hsl(140 100% 30%);
--green-40: hsl(140 100% 40%);
--red-60: hsl(358 80% 60%);
+
+ /* Light backgrounds */
+ --light-blue-background: hsl(210 100% 96%);
+ --light-red-background: hsl(355 73% 97%);
+ --light-green: hsl(206 41% 97%);
+
+ /* Dark foregrounds to go with backgrounds */
+ --dark-blue: hsl(220 64% 33%);
+ --dark-red: hsl(0 69% 36%);
--background-color: var(--gray-100);
--brand-border: var(--gray-100);
--category-card-name-color: var(--gray-30);
@@ -50,6 +59,7 @@
--package-list-item-synopsis-color: black;
--package-list-item-metadata-color: black;
--package-list-item-version-color: var(--green-30);
+ --package-list-item-component-color: var(--gray-80);
--search-bar-color: hsl(221 39% 11%);
--search-bar-background-color: var(--gray-100);
--search-bar-background-hover-color: white;
diff --git a/assets/css/2-components/5-primary-search.css b/assets/css/2-components/5-primary-search.css
new file mode 100644
index 00000000..46ad7145
--- /dev/null
+++ b/assets/css/2-components/5-primary-search.css
@@ -0,0 +1,61 @@
+/* stylelint-disable selector-class-pattern */
+/* stylelint-disable declaration-block-no-redundant-longhand-properties */
+
+.main-search {
+ background-color: var(--search-bar-background-color);
+ border-radius: 0.75rem;
+ border-width: 2px;
+ display: flex;
+ font-size: 1.5rem;
+ justify-content: center;
+ line-height: 2rem;
+ max-width: 28rem;
+ outline-offset: -2px;
+ overflow: hidden;
+ padding: 0.5rem;
+
+ .search-bar {
+ background-color: var(--search-bar-background-color);
+ color: var(--search-bar-color);
+ display: block;
+ margin-left: 0.5rem;
+ font-size: 1.5rem;
+ line-height: 2rem;
+ padding: 0.5rem;
+ flex-grow: 1;
+ min-width: 0;
+ }
+
+ .search-bar:hover {
+ background-color: var(--search-bar-background-hover-color);
+ }
+
+ .search-bar:focus {
+ background-color: var(--search-bar-background-focus-color);
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ }
+}
+
+.main-search:focus-within {
+ border-color: var(--search-bar-focus-border-color);
+ transition-property: box-shadow;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 400ms;
+
+ /* offset-x | offset-y | blur-radius | spread-radius | color */
+ box-shadow: 5px 5px 5px 2px var(--search-bar-focus-border-color);
+}
+
+.main-search button {
+ margin-bottom: 1.25rem;
+ margin-right: 1rem;
+ margin-top: 1.25rem;
+
+ svg {
+ width: 1.5rem;
+ height: 1.5rem;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+}
diff --git a/assets/css/2-components/6-secondary-search.css b/assets/css/2-components/6-secondary-search.css
new file mode 100644
index 00000000..6bef98a1
--- /dev/null
+++ b/assets/css/2-components/6-secondary-search.css
@@ -0,0 +1,60 @@
+/* stylelint-disable selector-class-pattern */
+/* stylelint-disable declaration-block-no-redundant-longhand-properties */
+
+.secondary-search {
+ background-color: var(--search-bar-background-color);
+ border-radius: 0.75rem;
+ border-width: 2px;
+ display: flex;
+ font-size: 1rem;
+ justify-content: center;
+ line-height: 2rem;
+ max-width: 20rem;
+ outline-offset: -2px;
+ overflow: hidden;
+ padding: 0.3rem;
+
+ .search-bar {
+ background-color: var(--search-bar-background-color);
+ color: var(--search-bar-color);
+ display: block;
+ font-size: 1.5rem;
+ line-height: 2rem;
+ padding: 0.5rem;
+ flex-grow: 1;
+ min-width: 0;
+ }
+
+ .search-bar:hover {
+ background-color: var(--search-bar-background-hover-color);
+ }
+
+ .search-bar:focus {
+ background-color: var(--search-bar-background-focus-color);
+ outline: transparent solid 2px;
+ outline-offset: 2px;
+ }
+}
+
+.secondary-search:focus-within {
+ border-color: var(--search-bar-focus-border-color);
+ transition-property: box-shadow;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 300ms;
+
+ /* offset-x | offset-y | blur-radius | spread-radius | color */
+ box-shadow: 4px 4px 4px 1px var(--search-bar-focus-border-color);
+}
+
+.secondary-search button {
+ margin-bottom: 1.25rem;
+ margin-right: 1rem;
+ margin-top: 1.25rem;
+
+ svg {
+ width: 1em;
+ height: 1em;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+}
diff --git a/assets/css/2-components/7-button.css b/assets/css/2-components/7-button.css
new file mode 100644
index 00000000..3cd25020
--- /dev/null
+++ b/assets/css/2-components/7-button.css
@@ -0,0 +1,32 @@
+/* stylelint-disable selector-class-pattern */
+/* stylelint-disable declaration-block-no-redundant-longhand-properties */
+
+.button {
+ background-color: var(--main-page-button-background);
+ border-radius: 50rem;
+ border-width: 1px;
+ color: var(--text-color);
+ font-weight: bolder;
+ padding-bottom: 1rem;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ padding-top: 1rem;
+}
+
+.button:hover {
+ border-color: var(--main-page-button-focus-border-color);
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+
+ /* offset-x | offset-y | blur-radius | spread-radius | color */
+ box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color);
+}
+
+.button:active {
+ border-color: var(--main-page-button-focus-border-color);
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+
+ /* offset-x | offset-y | blur-radius | spread-radius | color */
+ box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color);
+}
diff --git a/assets/css/2-components/8-alert.css b/assets/css/2-components/8-alert.css
new file mode 100644
index 00000000..63dec2d5
--- /dev/null
+++ b/assets/css/2-components/8-alert.css
@@ -0,0 +1,27 @@
+.alert {
+ padding: 1rem;
+ border-radius: 0.5rem;
+ align-items: center;
+ display: flex;
+ margin-bottom: 1rem;
+}
+
+.alert-info {
+ color: var(--dark-blue);
+ background-color: var(--light-blue-background);
+}
+
+.alert-error {
+ color: var(--dark-red);
+ background-color: var(--light-red-background);
+}
+
+svg.alert-icon {
+ flex-shrink: 0;
+ width: 1em;
+ height: 1em;
+}
+
+.alert-message {
+ margin-inline-start: 0.75rem;
+}
diff --git a/assets/css/3-screens/1-package/1-package.css b/assets/css/3-screens/1-package/1-package.css
index 06b15333..0bda37af 100644
--- a/assets/css/3-screens/1-package/1-package.css
+++ b/assets/css/3-screens/1-package/1-package.css
@@ -24,6 +24,10 @@
line-height: 1.75rem;
}
+.release-deprecated {
+ color: var(--deprecated-version);
+}
+
.package-body {
justify-content: center;
display: grid;
@@ -45,10 +49,6 @@
margin-bottom: 0.75rem;
}
- .release-deprecated {
- color: var(--deprecated-version);
- }
-
.package-right-column {
order: 3;
}
@@ -119,6 +119,50 @@
}
}
+.release {
+ a:hover {
+ text-decoration: underline;
+ }
+}
+
+span.revised-date::before {
+ content: attr(data-text); /* here's the magic */
+ position: absolute;
+ font-size: 0.85em;
+
+ /* vertically center */
+ top: 50%;
+ transform: translateY(-50%);
+
+ /* move to right */
+ left: 100%;
+
+ /* basic styles */
+ background: #000;
+ border-radius: 10px;
+ box-shadow: 0 1px 8px rgb(0 0 0 / 50%);
+ color: #fff;
+ padding: 5px;
+ text-align: center;
+ width: 200px;
+ display: none; /* hide by default */
+}
+
+span.revised-date:hover::before {
+ display: block;
+}
+
+.revised-date {
+ svg {
+ display: inline;
+ width: 1rem;
+ height: 1rem;
+ }
+
+ position: relative; /* making the .tooltip span a container for the tooltip text */
+ border-bottom: 1px dashed #000; /* little indicater to indicate it's hoverable */
+}
+
.instruction-tooltip {
svg {
display: inline;
@@ -140,7 +184,6 @@
/* move to right */
left: 100%;
- margin-left: 15px; /* and add a small left margin */
/* basic styles */
background: #000;
diff --git a/assets/css/3-screens/4-front-page.css b/assets/css/3-screens/4-front-page.css
index 9515e534..1cc00908 100644
--- a/assets/css/3-screens/4-front-page.css
+++ b/assets/css/3-screens/4-front-page.css
@@ -13,65 +13,6 @@
text-align: center;
}
-.main-search {
- background-color: var(--search-bar-background-color);
- border-radius: 0.75rem;
- border-width: 2px;
- display: flex;
- font-size: 1.5rem;
- justify-content: center;
- line-height: 2rem;
- max-width: 28rem;
- outline-offset: -2px;
- overflow: hidden;
- padding: 0.5rem;
-
- .search-bar {
- background-color: var(--search-bar-background-color);
- color: var(--search-bar-color);
- display: block;
- margin-left: 0.5rem;
- font-size: 1.5rem;
- line-height: 2rem;
- padding: 0.5rem;
- flex-grow: 1;
- min-width: 0;
- }
-
- .search-bar:hover {
- background-color: var(--search-bar-background-hover-color);
- }
-
- .search-bar:focus {
- background-color: var(--search-bar-background-focus-color);
- outline: 2px solid transparent;
- outline-offset: 2px;
- }
-}
-
-.main-search:focus-within {
- border-color: var(--search-bar-focus-border-color);
- transition-property: box-shadow;
- transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
- transition-duration: 400ms;
-
- /* offset-x | offset-y | blur-radius | spread-radius | color */
- box-shadow: 5px 5px 5px 2px var(--search-bar-focus-border-color);
-}
-
-.main-search button {
- margin-bottom: 1.25rem;
- margin-right: 1rem;
- margin-top: 1.25rem;
-
- svg {
- width: 1.5rem;
- height: 1.5rem;
- margin-top: auto;
- margin-bottom: auto;
- }
-}
-
section#main-page-buttons {
border-top: 1px solid var(--text-color);
border-color: var(--navbar-background-color);
diff --git a/assets/css/styles.css b/assets/css/styles.css
index caabebeb..631dfa54 100644
--- a/assets/css/styles.css
+++ b/assets/css/styles.css
@@ -8,6 +8,10 @@
@import "2-components/2-package-component.css";
@import "2-components/3-breadcrumb.css";
@import "2-components/4-license.css";
+@import "2-components/5-primary-search.css";
+@import "2-components/6-secondary-search.css";
+@import "2-components/7-button.css";
+@import "2-components/8-alert.css";
@import "3-screens/1-package/1-package.css";
@import "3-screens/1-package/2-release-changelog.css";
@@ -52,6 +56,46 @@
padding: 0.5rem;
width: 100%;
}
+
+ .password {
+ margin-bottom: 1rem;
+ }
+
+ .totp-zone {
+ display: none;
+ }
+
+ input[type="checkbox"]:checked + div.totp-zone {
+ display: block;
+ }
+
+ .login-button {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin-top: 1rem;
+ }
+
+ div.login-button button {
+ background-color: var(--main-page-button-background);
+ border-radius: 50rem;
+ border-width: 1px;
+ color: var(--text-color);
+ font-weight: bolder;
+ padding-bottom: 1rem;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ padding-top: 1rem;
+ }
+
+ div.login-button button:hover {
+ border-color: var(--main-page-button-focus-border-color);
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+
+ /* offset-x | offset-y | blur-radius | spread-radius | color */
+ box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color);
+ }
}
.version-list-item {
@@ -80,13 +124,13 @@
a {
display: block;
+ font-size: 1.25rem;
+ line-height: 1.75rem;
margin-top: 1rem;
margin-bottom: 1rem;
padding-left: 1rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
- font-size: 1.25rem;
- line-height: 1.75rem;
}
a:hover {
@@ -100,12 +144,12 @@
.package-list-item__name {
display: inline;
- margin-right: 0.5rem;
color: var(--package-list-item-name-color);
}
.package-list-item__synopsis {
display: inline;
+ margin-left: 10px;
color: var(--package-list-item-synopsis-color);
}
@@ -127,6 +171,11 @@
font-size: 0.875rem;
line-height: 1.25rem;
}
+
+ .package-list-item__component {
+ display: inline;
+ color: var(--package-list-item-component-color);
+ }
}
.category a:hover {
@@ -149,10 +198,6 @@
}
}
-.release a:hover {
- text-decoration: underline;
-}
-
.theme-button--light {
display: none;
}
@@ -221,8 +266,10 @@
text-decoration: underline;
}
+/* offset-x | offset-y | blur-radius | spread-radius | color */
.exact-match {
box-shadow: 0 5px 10px 0 rgb(0 0 0 / 50%);
+ border-radius: 6px;
}
.package-count {
diff --git a/assets/package.json b/assets/package.json
index 5bad6d99..409d32ae 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -15,7 +15,7 @@
"esbuild-copy-static-files": "^0.1.0",
"esbuild-plugin-assets-manifest": "^1.0.7",
"esbuild-style-plugin": "^1.6.0",
- "postcss": "^8.4.20",
+ "postcss": "^8.4.31",
"postcss-cli": "^9.0.2",
"postcss-copy": "^7.1.0",
"postcss-hash": "^3.0.0",
diff --git a/assets/yarn.lock b/assets/yarn.lock
index b3f03417..c4e879d4 100644
--- a/assets/yarn.lock
+++ b/assets/yarn.lock
@@ -2,41 +2,54 @@
# yarn lockfile v1
+"@alloc/quick-lru@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
+ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
+
"@babel/code-frame@^7.0.0":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
- integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244"
+ integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==
dependencies:
- "@babel/highlight" "^7.18.6"
+ "@babel/highlight" "^7.23.4"
+ chalk "^2.4.2"
-"@babel/helper-validator-identifier@^7.18.6":
- version "7.19.1"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
- integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
+"@babel/helper-validator-identifier@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
+ integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
-"@babel/highlight@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
- integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+"@babel/highlight@^7.23.4":
+ version "7.23.4"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
+ integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
dependencies:
- "@babel/helper-validator-identifier" "^7.18.6"
- chalk "^2.0.0"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ chalk "^2.4.2"
js-tokens "^4.0.0"
-"@csstools/css-parser-algorithms@^2.3.0":
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141"
- integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA==
+"@babel/runtime@^7.21.0":
+ version "7.23.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db"
+ integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==
+ dependencies:
+ regenerator-runtime "^0.14.0"
-"@csstools/css-tokenizer@^2.1.1":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e"
- integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA==
+"@csstools/css-parser-algorithms@^2.3.1":
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.2.tgz#1e0d581dbf4518cb3e939c3b863cb7180c8cedad"
+ integrity sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==
-"@csstools/media-query-list-parser@^2.1.2":
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728"
- integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ==
+"@csstools/css-tokenizer@^2.2.0":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.1.tgz#9dc431c9a5f61087af626e41ac2a79cce7bb253d"
+ integrity sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==
+
+"@csstools/media-query-list-parser@^2.1.4":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.5.tgz#94bc8b3c3fd7112a40b7bf0b483e91eba0654a0f"
+ integrity sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==
"@csstools/selector-specificity@^3.0.0":
version "3.0.0"
@@ -61,6 +74,50 @@
dependencies:
purgecss "^4.1.3"
+"@isaacs/cliui@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
+ integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+ dependencies:
+ string-width "^5.1.2"
+ string-width-cjs "npm:string-width@^4.2.0"
+ strip-ansi "^7.0.1"
+ strip-ansi-cjs "npm:strip-ansi@^6.0.1"
+ wrap-ansi "^8.1.0"
+ wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
+
+"@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.20"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
+ integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -82,10 +139,15 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@pkgjs/parseargs@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
+ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
+
"@ryangjchandler/alpine-clipboard@^2.1.0":
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz#92e54d02bb7ff9213b23d6069454e0be725a2ea9"
- integrity sha512-2kKHd2mA6K7RuYlC+1fikIUPVJeJLQlY2w9rNGrOgVfzXUZRotjTP+EjxouDizTEvqNRkVTJnmmNle32Uhb4zw==
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.3.0.tgz#44d7a9e8c4446fd24ece2d6be0d23fac7dd59b20"
+ integrity sha512-r1YL/LL851vSemjgcca+M6Yz9SNtA9ATul8nJ0n0sAS1W3V1GUWvH0Od2XdQF1r36YJF+/4sUc0eHF/Zexw7dA==
"@tailwindcss/nesting@^0.0.0-insiders.565cd3e":
version "0.0.0-insiders.565cd3e"
@@ -100,24 +162,26 @@
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/less@^3.0.3":
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.3.tgz#f9451dbb9548d25391107d65d6401a0cfb15db92"
- integrity sha512-1YXyYH83h6We1djyoUEqTlVyQtCfJAFXELSKW2ZRtjHD4hQ82CC4lvrv5D0l0FLcKBaiPbXyi3MpMsI9ZRgKsw==
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.6.tgz#279b51245ba787c810a0d286226c5900cd5e6765"
+ integrity sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw==
"@types/minimist@^1.2.2":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e"
+ integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==
"@types/node@*":
- version "18.15.11"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f"
- integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==
+ version "20.10.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.3.tgz#4900adcc7fc189d5af5bb41da8f543cea6962030"
+ integrity sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==
+ dependencies:
+ undici-types "~5.26.4"
"@types/normalize-package-data@^2.4.0":
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
- integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+ version "2.4.4"
+ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
+ integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==
"@types/sass@^1.43.1":
version "1.45.0"
@@ -126,10 +190,10 @@
dependencies:
sass "*"
-"@types/stylus@^0.48.37":
- version "0.48.38"
- resolved "https://registry.yarnpkg.com/@types/stylus/-/stylus-0.48.38.tgz#6e62a59f9350f53a253aa42b038b6aa44a642c5b"
- integrity sha512-B5otJekvD6XM8iTrnO6e2twoTY2tKL9VkL/57/2Lo4tv3EatbCaufdi68VVtn/h4yjO+HVvYEyrNQd0Lzj6riw==
+"@types/stylus@^0.48.38":
+ version "0.48.42"
+ resolved "https://registry.yarnpkg.com/@types/stylus/-/stylus-0.48.42.tgz#8fa7d99b48556bb8fe85a052aaba8c1e59a97e2f"
+ integrity sha512-CPGlr5teL4sqdap+EOowMifLuNGeIoLwc0VQ7u/BPxo+ocqiNa5jeVt0H0IVBblEh6ZwX1sGpIQIFnSSr8NBQA==
dependencies:
"@types/node" "*"
@@ -164,9 +228,9 @@ ajv@^8.0.1:
uri-js "^4.2.2"
alpinejs@^3.12.0:
- version "3.12.0"
- resolved "https://registry.yarnpkg.com/alpinejs/-/alpinejs-3.12.0.tgz#ff84d788231e7cc3fc38e363b7ebbc4f9d7031b2"
- integrity sha512-YENcRBA9dlwR8PsZNFMTHbmdlTNwd1BkCeivPvOzzCKHas6AfwNRsDK9UEFmE5dXTMEZjnnpCTxV8vkdpWiOCw==
+ version "3.13.3"
+ resolved "https://registry.yarnpkg.com/alpinejs/-/alpinejs-3.13.3.tgz#92eb7e869b99ff548e7a55044e45660597cf530b"
+ integrity sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==
dependencies:
"@vue/reactivity" "~3.1.1"
@@ -175,6 +239,11 @@ ansi-regex@^5.0.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+ansi-regex@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
+ integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
+
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -189,6 +258,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
+ansi-styles@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -268,13 +342,13 @@ atob@^2.1.2:
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autoprefixer@^10.2.4, autoprefixer@^10.4.0:
- version "10.4.14"
- resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
- integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==
+ version "10.4.16"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
+ integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==
dependencies:
- browserslist "^4.21.5"
- caniuse-lite "^1.0.30001464"
- fraction.js "^4.2.0"
+ browserslist "^4.21.10"
+ caniuse-lite "^1.0.30001538"
+ fraction.js "^4.3.6"
normalize-range "^0.1.2"
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
@@ -350,15 +424,15 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
-browserslist@^4.0.0, browserslist@^4.21.4, browserslist@^4.21.5:
- version "4.21.5"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7"
- integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==
+browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.21.4:
+ version "4.22.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b"
+ integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==
dependencies:
- caniuse-lite "^1.0.30001449"
- electron-to-chromium "^1.4.284"
- node-releases "^2.0.8"
- update-browserslist-db "^1.0.10"
+ caniuse-lite "^1.0.30001565"
+ electron-to-chromium "^1.4.601"
+ node-releases "^2.0.14"
+ update-browserslist-db "^1.0.13"
cache-base@^1.0.1:
version "1.0.1"
@@ -410,12 +484,12 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464:
- version "1.0.30001472"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz#3f484885f2a2986c019dc416e65d9d62798cdd64"
- integrity sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565:
+ version "1.0.30001566"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz#61a8e17caf3752e3e426d4239c549ebbb37fef0d"
+ integrity sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==
-chalk@^2.0.0, chalk@^2.4.1:
+chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -512,7 +586,7 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-color-name@^1.1.4, color-name@~1.1.4:
+color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
@@ -538,9 +612,9 @@ commander@^8.0.0:
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
component-emitter@^1.2.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
- integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17"
+ integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==
concat-map@0.0.1:
version "0.0.1"
@@ -567,19 +641,19 @@ copy-descriptor@^0.1.0:
integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
cosmiconfig@^8.2.0:
- version "8.2.0"
- resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd"
- integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==
+ version "8.3.6"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3"
+ integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==
dependencies:
- import-fresh "^3.2.1"
+ import-fresh "^3.3.0"
js-yaml "^4.1.0"
- parse-json "^5.0.0"
+ parse-json "^5.2.0"
path-type "^4.0.0"
cpx2@^4.2.0:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-4.2.2.tgz#bcb442a4c28312a6acf2cab053875aff9a87e5c7"
- integrity sha512-pFYHCivwNALi+yM4kATIA4XQvA72lhjcBBlwHRHtrvTcHWlMQfpgyyUHhus+iC7pHVBYnoGpza7ZrWBsJJAg5Q==
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-4.2.3.tgz#cf7fc1321e396858ffefdbcfe935a2475a0a0435"
+ integrity sha512-UM7Iza+OM8FZ2ntTml/mdb3RmSLK5I2DqFqDdMihlGyKZCAAnDP++H973Oyc/2TQpEMtg5JHeRNfewclE330EA==
dependencies:
debounce "^1.2.0"
debug "^4.1.1"
@@ -588,22 +662,31 @@ cpx2@^4.2.0:
glob-gitignore "^1.0.14"
glob2base "0.0.12"
ignore "^5.1.8"
- minimatch "^7.4.2"
+ minimatch "^8.0.2"
p-map "^4.0.0"
resolve "^1.12.0"
safe-buffer "^5.2.0"
shell-quote "^1.8.0"
subarg "^1.0.0"
+cross-spawn@^7.0.0:
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+ integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
css-declaration-sorter@^6.3.1:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad"
- integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"
+ integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==
-css-functions-list@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b"
- integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w==
+css-functions-list@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.1.tgz#2eb205d8ce9f9ce74c5c1d7490b66b77c45ce3ea"
+ integrity sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==
css-select@^4.1.3:
version "4.3.0"
@@ -707,9 +790,11 @@ csso@^4.2.0:
css-tree "^1.1.2"
date-fns@^2.16.1:
- version "2.29.3"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
- integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
+ integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
debounce@^1.2.0:
version "1.2.1"
@@ -832,16 +917,26 @@ duplexer@^0.1.1:
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
-electron-to-chromium@^1.4.284:
- version "1.4.342"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.342.tgz#3c7e199c3aa89c993df4b6f5223d6d26988f58e6"
- integrity sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA==
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
+electron-to-chromium@^1.4.601:
+ version "1.4.603"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.603.tgz#446907c21d333b55d0beaba1cb5b48430775a8a7"
+ integrity sha512-Dvo5OGjnl7AZTU632dFJtWj0uJK835eeOVQIuRcmBmsFsTNn3cL05FqOyHAfGQDIoHfLhyJ1Tya3PJ0ceMz54g==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -925,21 +1020,21 @@ esbuild-openbsd-64@0.13.15:
integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
esbuild-plugin-assets-manifest@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/esbuild-plugin-assets-manifest/-/esbuild-plugin-assets-manifest-1.0.7.tgz#a896616bbfff86427251177936ff0447b1bdfa74"
- integrity sha512-UIuXaCeyWW8ydfJjU5h4/NMLHGvi8nFgDhTVbvcfab03DAkXrMP1/5hnLZ5FpAC63nnOnwmOZk4pLx4amWAyaA==
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/esbuild-plugin-assets-manifest/-/esbuild-plugin-assets-manifest-1.0.8.tgz#af597aa2753f2e087c04a31f712a54ab5ffbdab1"
+ integrity sha512-2goWUmBLpqaHASkMQIgsSqcAs3Y1rDMKcOjk8cWOTtAzmbHYQLh77MEjuQYDx3bBEQ9dVZCaU0/cBIjhvdJf+w==
esbuild-style-plugin@^1.6.0:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/esbuild-style-plugin/-/esbuild-style-plugin-1.6.1.tgz#9a0f6f47587890c56d10a4ae3fc4ce5697baaa28"
- integrity sha512-t5ZtQyGKNiM8DLedz+iwFH0LPWLnMmaEQ2RnnP1ppFHq35najxqJHAhUVVSbTFm5cR2ZumGdBMqnmuKBRBMvDw==
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/esbuild-style-plugin/-/esbuild-style-plugin-1.6.3.tgz#123c994047f1393bee6896c1c37bd4f1942c425f"
+ integrity sha512-XPEKf4FjLjEVLv/dJH4UxDzXCrFHYpD93DBO8B+izdZARW5b7nNKQbnKv3J+7VDWJbgCU+hzfgIh2AuIZzlmXQ==
dependencies:
"@types/less" "^3.0.3"
"@types/sass" "^1.43.1"
- "@types/stylus" "^0.48.37"
- glob "^8.0.1"
- postcss "^8.4.12"
- postcss-modules "^4.3.1"
+ "@types/stylus" "^0.48.38"
+ glob "^10.2.2"
+ postcss "^8.4.31"
+ postcss-modules "^6.0.0"
esbuild-sunos-64@0.13.15:
version "0.13.15"
@@ -1041,10 +1136,10 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0"
- integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==
+fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
+ integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -1064,12 +1159,12 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
-file-entry-cache@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
- integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+file-entry-cache@^7.0.0:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-7.0.2.tgz#2d61bb70ba89b9548e3035b7c9173fe91deafff0"
+ integrity sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==
dependencies:
- flat-cache "^3.0.4"
+ flat-cache "^3.2.0"
fill-range@^4.0.0:
version "4.0.0"
@@ -1101,28 +1196,37 @@ find-up@^5.0.0:
locate-path "^6.0.0"
path-exists "^4.0.0"
-flat-cache@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
- integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+flat-cache@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee"
+ integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
dependencies:
- flatted "^3.1.0"
+ flatted "^3.2.9"
+ keyv "^4.5.3"
rimraf "^3.0.2"
-flatted@^3.1.0:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
- integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
+flatted@^3.2.9:
+ version "3.2.9"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf"
+ integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
-fraction.js@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
- integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==
+foreground-child@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
+ integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
+ dependencies:
+ cross-spawn "^7.0.0"
+ signal-exit "^4.0.1"
+
+fraction.js@^4.3.6:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
+ integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
fragment-cache@^0.2.1:
version "0.2.1"
@@ -1141,9 +1245,9 @@ fs-extra@^10.0.0:
universalify "^2.0.0"
fs-extra@^11.1.0:
- version "11.1.1"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
- integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==
+ version "11.2.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b"
+ integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
@@ -1165,14 +1269,14 @@ fs.realpath@^1.0.0:
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
generic-names@^4.0.0:
version "4.0.0"
@@ -1241,6 +1345,17 @@ glob@7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
+glob@^10.2.2:
+ version "10.3.10"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
+ integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
+ dependencies:
+ foreground-child "^3.1.0"
+ jackspeak "^2.3.5"
+ minimatch "^9.0.1"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+ path-scurry "^1.10.1"
+
glob@^7.1.3, glob@^7.1.7:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -1253,17 +1368,6 @@ glob@^7.1.3, glob@^7.1.7:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^8.0.1:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
- integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^5.0.1"
- once "^1.3.0"
-
global-modules@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -1360,12 +1464,12 @@ has-values@^1.0.0:
is-number "^3.0.0"
kind-of "^4.0.0"
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+hasown@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+ integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
dependencies:
- function-bind "^1.1.1"
+ function-bind "^1.1.2"
hosted-git-info@^4.0.1:
version "4.1.0"
@@ -1379,27 +1483,22 @@ html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
-icss-replace-symbols@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
- integrity sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==
-
-icss-utils@^5.0.0:
+icss-utils@^5.0.0, icss-utils@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
ignore@^5.0.5, ignore@^5.1.8, ignore@^5.1.9, ignore@^5.2.0, ignore@^5.2.4:
- version "5.2.4"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
- integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
+ integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
immutable@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
- integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f"
+ integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==
-import-fresh@^3.2.1:
+import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -1445,19 +1544,12 @@ ini@^1.3.5:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
-is-accessor-descriptor@^0.1.6:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
- integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==
- dependencies:
- kind-of "^3.0.2"
-
-is-accessor-descriptor@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
- integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+is-accessor-descriptor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4"
+ integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==
dependencies:
- kind-of "^6.0.0"
+ hasown "^2.0.0"
is-arrayish@^0.2.1:
version "0.2.1"
@@ -1476,44 +1568,35 @@ is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-core-module@^2.5.0, is-core-module@^2.9.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
- integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+is-core-module@^2.13.0, is-core-module@^2.5.0:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
+ integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
- has "^1.0.3"
-
-is-data-descriptor@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
- integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==
- dependencies:
- kind-of "^3.0.2"
+ hasown "^2.0.0"
-is-data-descriptor@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
- integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+is-data-descriptor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb"
+ integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==
dependencies:
- kind-of "^6.0.0"
+ hasown "^2.0.0"
is-descriptor@^0.1.0:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
- integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33"
+ integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==
dependencies:
- is-accessor-descriptor "^0.1.6"
- is-data-descriptor "^0.1.4"
- kind-of "^5.0.0"
+ is-accessor-descriptor "^1.0.1"
+ is-data-descriptor "^1.0.1"
is-descriptor@^1.0.0, is-descriptor@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
- integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306"
+ integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==
dependencies:
- is-accessor-descriptor "^1.0.0"
- is-data-descriptor "^1.0.0"
- kind-of "^6.0.2"
+ is-accessor-descriptor "^1.0.1"
+ is-data-descriptor "^1.0.1"
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
@@ -1600,10 +1683,19 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
-jiti@^1.17.2:
- version "1.18.2"
- resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd"
- integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==
+jackspeak@^2.3.5:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
+ integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+ optionalDependencies:
+ "@pkgjs/parseargs" "^0.11.0"
+
+jiti@^1.19.1:
+ version "1.21.0"
+ resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
+ integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
js-tokens@^4.0.0:
version "4.0.0"
@@ -1617,6 +1709,11 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
json-parse-even-better-errors@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
@@ -1636,6 +1733,13 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
+keyv@^4.5.3:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+ dependencies:
+ json-buffer "3.0.1"
+
kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -1650,26 +1754,26 @@ kind-of@^4.0.0:
dependencies:
is-buffer "^1.1.5"
-kind-of@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
- integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
-
-kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
+kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-known-css-properties@^0.27.0:
- version "0.27.0"
- resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5"
- integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==
+known-css-properties@^0.29.0:
+ version "0.29.0"
+ resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f"
+ integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==
-lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6:
+lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
+lilconfig@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc"
+ integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==
+
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@@ -1729,6 +1833,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+"lru-cache@^9.1.1 || ^10.0.0":
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
+ integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
+
make-array@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/make-array/-/make-array-1.0.5.tgz#326a7635c756a9f61ce0b2a6fdd5cc3460419bcb"
@@ -1833,17 +1942,17 @@ minimatch@^3.0.4, minimatch@^3.1.1:
dependencies:
brace-expansion "^1.1.7"
-minimatch@^5.0.1:
- version "5.1.6"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
- integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
+minimatch@^8.0.2:
+ version "8.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229"
+ integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==
dependencies:
brace-expansion "^2.0.1"
-minimatch@^7.4.2:
- version "7.4.3"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.3.tgz#012cbf110a65134bb354ae9773b55256cdb045a2"
- integrity sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A==
+minimatch@^9.0.1:
+ version "9.0.3"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
+ integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
@@ -1861,6 +1970,11 @@ minimist@^1.1.0, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0":
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c"
+ integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==
+
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -1895,10 +2009,10 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
-nanoid@^3.3.6:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
- integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+nanoid@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+ integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
nanomatch@^1.2.9:
version "1.2.13"
@@ -1917,10 +2031,10 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
-node-releases@^2.0.8:
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
- integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
+node-releases@^2.0.14:
+ version "2.0.14"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"
+ integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==
normalize-package-data@^3.0.2:
version "3.0.3"
@@ -2022,7 +2136,7 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
-parse-json@^5.0.0, parse-json@^5.2.0:
+parse-json@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
@@ -2052,11 +2166,24 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+path-scurry@^1.10.1:
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698"
+ integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==
+ dependencies:
+ lru-cache "^9.1.1 || ^10.0.0"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -2083,9 +2210,9 @@ pify@^3.0.0:
integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
pirates@^4.0.1:
- version "4.0.5"
- resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
- integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
+ integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
posix-character-classes@^0.1.0:
version "0.1.1"
@@ -2174,7 +2301,7 @@ postcss-hash@^3.0.0:
dependencies:
mkdirp "^0.5.1"
-postcss-import@^14.0.2, postcss-import@^14.1.0:
+postcss-import@^14.0.2:
version "14.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==
@@ -2183,14 +2310,23 @@ postcss-import@^14.0.2, postcss-import@^14.1.0:
read-cache "^1.0.0"
resolve "^1.1.7"
-postcss-js@^4.0.0:
+postcss-import@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
+ integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
+ dependencies:
+ postcss-value-parser "^4.0.0"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
dependencies:
camelcase-css "^2.0.1"
-postcss-load-config@^3.0.0, postcss-load-config@^3.1.4:
+postcss-load-config@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855"
integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
@@ -2198,6 +2334,14 @@ postcss-load-config@^3.0.0, postcss-load-config@^3.1.4:
lilconfig "^2.0.5"
yaml "^1.10.2"
+postcss-load-config@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3"
+ integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==
+ dependencies:
+ lilconfig "^3.0.0"
+ yaml "^2.3.4"
+
postcss-merge-longhand@^5.1.7:
version "5.1.7"
resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16"
@@ -2254,9 +2398,9 @@ postcss-modules-extract-imports@^3.0.0:
integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
postcss-modules-local-by-default@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
- integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524"
+ integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==
dependencies:
icss-utils "^5.0.0"
postcss-selector-parser "^6.0.2"
@@ -2276,13 +2420,13 @@ postcss-modules-values@^4.0.0:
dependencies:
icss-utils "^5.0.0"
-postcss-modules@^4.3.1:
- version "4.3.1"
- resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.3.1.tgz#517c06c09eab07d133ae0effca2c510abba18048"
- integrity sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==
+postcss-modules@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118"
+ integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ==
dependencies:
generic-names "^4.0.0"
- icss-replace-symbols "^1.1.0"
+ icss-utils "^5.1.0"
lodash.camelcase "^4.3.0"
postcss-modules-extract-imports "^3.0.0"
postcss-modules-local-by-default "^4.0.0"
@@ -2290,13 +2434,6 @@ postcss-modules@^4.3.1:
postcss-modules-values "^4.0.0"
string-hash "^1.1.1"
-postcss-nested@6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735"
- integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==
- dependencies:
- postcss-selector-parser "^6.0.10"
-
postcss-nested@^5.0.5:
version "5.0.6"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc"
@@ -2304,6 +2441,13 @@ postcss-nested@^5.0.5:
dependencies:
postcss-selector-parser "^6.0.6"
+postcss-nested@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
+ integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
+ dependencies:
+ postcss-selector-parser "^6.0.11"
+
postcss-normalize-charset@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed"
@@ -2408,7 +2552,7 @@ postcss-safe-parser@^6.0.0:
resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1"
integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==
-postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9:
+postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9:
version "6.0.13"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b"
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
@@ -2450,19 +2594,19 @@ postcss@^6.0.3:
source-map "^0.6.1"
supports-color "^5.4.0"
-postcss@^8.0.9, postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.20, postcss@^8.4.24:
- version "8.4.25"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f"
- integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw==
+postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.28, postcss@^8.4.31:
+ version "8.4.32"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
+ integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
dependencies:
- nanoid "^3.3.6"
+ nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.0.2"
prettier@^2.7.1:
- version "2.8.7"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
- integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==
+ version "2.8.8"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+ integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-hrtime@^1.0.3:
version "1.0.3"
@@ -2470,9 +2614,9 @@ pretty-hrtime@^1.0.3:
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
punycode@^2.1.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
- integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
purgecss@^4.1.3:
version "4.1.3"
@@ -2535,6 +2679,11 @@ redent@^4.0.0:
indent-string "^5.0.0"
strip-indent "^4.0.0"
+regenerator-runtime@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
+ integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
+
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -2578,12 +2727,12 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
-resolve@^1.1.7, resolve@^1.12.0, resolve@^1.22.1:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+resolve@^1.1.7, resolve@^1.12.0, resolve@^1.22.2:
+ version "1.22.8"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
+ integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
- is-core-module "^2.9.0"
+ is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
@@ -2631,9 +2780,9 @@ safe-regex@^1.1.0:
ret "~0.1.10"
sass@*:
- version "1.60.0"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.60.0.tgz#657f0c23a302ac494b09a5ba8497b739fb5b5a81"
- integrity sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==
+ version "1.69.5"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde"
+ integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -2656,15 +2805,27 @@ set-value@^2.0.0, set-value@^2.0.1:
is-plain-object "^2.0.3"
split-string "^3.0.1"
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
shell-quote@^1.8.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
- integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
+ integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
signal-exit@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967"
- integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
slash@^3.0.0:
version "3.0.0"
@@ -2773,9 +2934,9 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
- version "3.0.13"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5"
- integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==
+ version "3.0.16"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f"
+ integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
@@ -2802,7 +2963,7 @@ string-hash@^1.1.1:
resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"
integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -2811,13 +2972,29 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+string-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
+strip-ansi@^7.0.1:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
+ dependencies:
+ ansi-regex "^6.0.1"
+
strip-indent@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853"
@@ -2856,23 +3033,23 @@ stylelint-config-standard@^26.0.0:
stylelint-config-recommended "^8.0.0"
stylelint@^15.10.1:
- version "15.10.1"
- resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.1.tgz#93f189958687e330c106b010cbec0c41dcae506d"
- integrity sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ==
+ version "15.11.0"
+ resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.11.0.tgz#3ff8466f5f5c47362bc7c8c9d382741c58bc3292"
+ integrity sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==
dependencies:
- "@csstools/css-parser-algorithms" "^2.3.0"
- "@csstools/css-tokenizer" "^2.1.1"
- "@csstools/media-query-list-parser" "^2.1.2"
+ "@csstools/css-parser-algorithms" "^2.3.1"
+ "@csstools/css-tokenizer" "^2.2.0"
+ "@csstools/media-query-list-parser" "^2.1.4"
"@csstools/selector-specificity" "^3.0.0"
balanced-match "^2.0.0"
colord "^2.9.3"
cosmiconfig "^8.2.0"
- css-functions-list "^3.1.0"
+ css-functions-list "^3.2.1"
css-tree "^2.3.1"
debug "^4.3.4"
- fast-glob "^3.3.0"
+ fast-glob "^3.3.1"
fastest-levenshtein "^1.0.16"
- file-entry-cache "^6.0.1"
+ file-entry-cache "^7.0.0"
global-modules "^2.0.0"
globby "^11.1.0"
globjoin "^0.1.4"
@@ -2881,13 +3058,13 @@ stylelint@^15.10.1:
import-lazy "^4.0.0"
imurmurhash "^0.1.4"
is-plain-object "^5.0.0"
- known-css-properties "^0.27.0"
+ known-css-properties "^0.29.0"
mathml-tag-names "^2.1.3"
meow "^10.1.5"
micromatch "^4.0.5"
normalize-path "^3.0.0"
picocolors "^1.0.0"
- postcss "^8.4.24"
+ postcss "^8.4.28"
postcss-resolve-nested-selector "^0.1.1"
postcss-safe-parser "^6.0.0"
postcss-selector-parser "^6.0.13"
@@ -2908,11 +3085,12 @@ subarg@^1.0.0:
dependencies:
minimist "^1.1.0"
-sucrase@^3.29.0:
- version "3.31.0"
- resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.31.0.tgz#daae4fd458167c5d4ba1cce6aef57b988b417b33"
- integrity sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ==
+sucrase@^3.32.0:
+ version "3.34.0"
+ resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f"
+ integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==
dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "7.1.6"
lines-and-columns "^1.1.6"
@@ -2984,34 +3162,32 @@ table@^6.8.1:
strip-ansi "^6.0.1"
tailwindcss@^3.0.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.0.tgz#8cab40e5a10a10648118c0859ba8bfbc744a761e"
- integrity sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f"
+ integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==
dependencies:
+ "@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
- color-name "^1.1.4"
didyoumean "^1.2.2"
dlv "^1.1.3"
- fast-glob "^3.2.12"
+ fast-glob "^3.3.0"
glob-parent "^6.0.2"
is-glob "^4.0.3"
- jiti "^1.17.2"
- lilconfig "^2.0.6"
+ jiti "^1.19.1"
+ lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
- postcss "^8.0.9"
- postcss-import "^14.1.0"
- postcss-js "^4.0.0"
- postcss-load-config "^3.1.4"
- postcss-nested "6.0.0"
+ postcss "^8.4.23"
+ postcss-import "^15.1.0"
+ postcss-js "^4.0.1"
+ postcss-load-config "^4.0.1"
+ postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
- postcss-value-parser "^4.2.0"
- quick-lru "^5.1.1"
- resolve "^1.22.1"
- sucrase "^3.29.0"
+ resolve "^1.22.2"
+ sucrase "^3.32.0"
thenby@^1.3.4:
version "1.3.4"
@@ -3096,6 +3272,11 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
+undici-types@~5.26.4:
+ version "5.26.5"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
+ integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
+
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -3107,9 +3288,9 @@ union-value@^1.0.0:
set-value "^2.0.1"
universalify@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
- integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
+ integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unset-value@^1.0.0:
version "1.0.0"
@@ -3119,10 +3300,10 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
-update-browserslist-db@^1.0.10:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
- integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+update-browserslist-db@^1.0.13:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
+ integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
@@ -3169,7 +3350,14 @@ which@^1.3.1:
dependencies:
isexe "^2.0.0"
-wrap-ansi@^7.0.0:
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -3178,6 +3366,15 @@ wrap-ansi@^7.0.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+ dependencies:
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"
+
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -3206,6 +3403,11 @@ yaml@^1.10.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+yaml@^2.3.4:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
+ integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
+
yargs-parser@^20.2.2, yargs-parser@^20.2.9:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
@@ -3230,9 +3432,9 @@ yargs@^16.2.0:
yargs-parser "^20.2.2"
yargs@^17.0.0:
- version "17.7.1"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967"
- integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
diff --git a/cabal.project b/cabal.project
index 0f0dcfeb..9221f5fa 100644
--- a/cabal.project
+++ b/cabal.project
@@ -1,6 +1,8 @@
-packages: ./
+packages:
+ ./
+ https://hackage.haskell.org/package/sel-0.0.1.0/candidate/sel-0.0.1.0.tar.gz
-with-compiler: ghc-9.4.5
+with-compiler: ghc-9.4.7
tests: True
@@ -62,3 +64,8 @@ source-repository-package
type: git
location: https://github.com/saurabhnanda/odd-jobs
tag: 51c7443
+
+source-repository-package
+ type: git
+ location: https://github.com/haskell-cryptography/one-time-password
+ tag: 2ca2313
diff --git a/cabal.project.freeze b/cabal.project.freeze
index 5d943a81..6d2928a8 100644
--- a/cabal.project.freeze
+++ b/cabal.project.freeze
@@ -4,20 +4,23 @@ constraints: any.Cabal ==3.8.1.0,
any.HUnit ==1.6.2.0,
any.OneTuple ==0.4.1.1,
any.Only ==0.1,
- any.PyF ==0.11.1.1,
+ any.PyF ==0.11.2.1,
PyF -python_test,
any.QuickCheck ==2.14.3,
QuickCheck -old-random +templatehaskell,
+ any.RSA ==2.4.1,
+ any.SHA ==1.6.4.4,
+ SHA -exe,
any.StateVar ==1.2.2,
any.abstract-deque ==0.3,
abstract-deque -usecas,
any.adjunctions ==4.4.2,
any.aeson ==2.1.2.1,
aeson -cffi +ordered-keymap,
- any.aeson-pretty ==0.8.9,
+ any.aeson-pretty ==0.8.10,
aeson-pretty -lib-only,
- any.ansi-terminal ==0.11.5,
- ansi-terminal -example +win32-2-13-1,
+ any.ansi-terminal ==1.0,
+ ansi-terminal -example,
any.ansi-terminal-types ==0.11.5,
any.appar ==0.1.8,
any.array ==0.5.4.0,
@@ -32,13 +35,16 @@ constraints: any.Cabal ==3.8.1.0,
atomic-primops -debug,
any.attoparsec ==0.14.4,
attoparsec -developer,
- any.attoparsec-iso8601 ==1.1.0.0,
+ any.attoparsec-aeson ==2.1.0.0,
+ any.attoparsec-iso8601 ==1.1.0.1,
+ any.authenticate-oauth ==1.7,
any.auto-update ==0.1.6,
- any.barbies ==2.0.4.0,
- any.base ==4.17.1.0,
- any.base-compat ==0.12.2,
- any.base-compat-batteries ==0.12.2,
- any.base-orphans ==0.9.0,
+ any.barbies ==2.0.5.0,
+ any.base ==4.17.2.0,
+ any.base-compat ==0.12.3,
+ any.base-compat-batteries ==0.12.3,
+ any.base-orphans ==0.9.1,
+ any.base16 ==1.0,
any.base16-bytestring ==1.0.2.0,
any.base64 ==0.4.2.4,
any.base64-bytestring ==1.2.1.0,
@@ -46,16 +52,21 @@ constraints: any.Cabal ==3.8.1.0,
any.bifunctors ==5.5.15,
bifunctors +semigroups +tagged,
any.binary ==0.8.9.1,
- any.bitvec ==1.1.4.0,
- bitvec -libgmp,
- any.blaze-builder ==0.4.2.2,
+ any.bitvec ==1.1.5.0,
+ bitvec +simd,
+ any.blaze-builder ==0.4.2.3,
any.blaze-html ==0.9.1.2,
- any.blaze-markup ==0.8.2.8,
+ any.blaze-markup ==0.8.3.0,
any.boring ==0.2.1,
boring +tagged,
any.bsb-http-chunked ==0.0.0.4,
+ any.bytebuild ==0.3.14.0,
+ bytebuild -checked,
any.byteorder ==1.0.4,
- any.bytestring ==0.11.4.0,
+ any.byteslice ==0.2.11.1,
+ byteslice +avoid-rawmemchr,
+ any.bytesmith ==0.3.10.0,
+ any.bytestring ==0.11.5.2,
any.bytestring-builder ==0.10.8.2.0,
bytestring-builder +bytestring_has_builder,
any.cabal-doctest ==1.0.9,
@@ -63,29 +74,36 @@ constraints: any.Cabal ==3.8.1.0,
any.case-insensitive ==1.2.1.0,
any.cereal ==0.5.8.3,
cereal -bytestring-builder,
- any.clock ==0.8.3,
+ any.chronos ==1.1.5.1,
+ any.clock ==0.8.4,
clock -llvm,
- any.cmark-gfm ==0.2.5,
+ any.cmark-gfm ==0.2.6,
cmark-gfm -pkgconfig,
any.cmdargs ==0.10.22,
cmdargs +quotation -testprog,
any.colour ==2.3.6,
any.colourista ==0.1.0.2,
- any.commonmark ==0.2.2,
- any.commonmark-extensions ==0.2.3.4,
+ any.commonmark ==0.2.4,
+ any.commonmark-extensions ==0.2.4,
any.comonad ==5.0.8,
comonad +containers +distributive +indexed-traversable,
- any.concurrent-output ==1.10.18,
+ any.concurrent-output ==1.10.20,
any.conduit ==1.3.5,
any.conduit-extra ==1.3.6,
any.constraints ==0.13.4,
any.containers ==0.6.7,
+ any.contiguous ==0.6.4.0,
any.contravariant ==1.5.5,
contravariant +semigroups +statevar +tagged,
any.cookie ==0.4.6,
+ any.crypto-api ==0.13.3,
+ crypto-api -all_cpolys,
+ any.crypto-pubkey-types ==0.4.3,
any.cryptohash-md5 ==0.11.101.0,
any.cryptohash-sha1 ==0.11.101.0,
- any.crypton ==0.31,
+ any.cryptohash-sha256 ==0.11.102.1,
+ cryptohash-sha256 -exe +use-cbits,
+ any.crypton ==0.34,
crypton -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq +support_pclmuldq +support_rdrand -support_sse +use_target_attributes,
any.crypton-connection ==0.3.1,
any.crypton-x509 ==1.7.6,
@@ -95,7 +113,6 @@ constraints: any.Cabal ==3.8.1.0,
any.cryptonite ==0.30,
cryptonite -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq -support_pclmuldq +support_rdrand -support_sse +use_target_attributes,
any.cryptonite-conduit ==0.2.2,
- any.daemons ==0.3.0,
any.data-default ==0.7.1.1,
any.data-default-class ==0.1.2.0,
any.data-default-instances-containers ==0.0.1,
@@ -106,6 +123,7 @@ constraints: any.Cabal ==3.8.1.0,
any.data-sketches-core ==0.1.0.0,
any.dec ==0.0.5,
any.deepseq ==1.4.8.0,
+ any.deriving-aeson ==0.2.9,
any.directory ==1.3.7.1,
any.distributive ==0.6.2.1,
distributive +semigroups +tagged,
@@ -116,13 +134,15 @@ constraints: any.Cabal ==3.8.1.0,
effectful -benchmark-foreign-libraries,
any.effectful-core ==2.2.2.2,
any.either ==5.0.2,
- any.emojis ==0.1.2,
+ any.emojis ==0.1.3,
any.entropy ==0.4.1.10,
entropy -donotgetentropy,
any.envparse ==0.5.0,
any.erf ==2.0.0.0,
any.exceptions ==0.10.5,
- any.fast-logger ==3.2.1,
+ any.extensible-exceptions ==0.1.1.4,
+ any.extra ==1.7.14,
+ any.fast-logger ==3.2.2,
any.file-embed ==0.0.15.0,
any.filepath ==1.4.2.2,
any.filtrable ==0.1.6.0,
@@ -134,52 +154,68 @@ constraints: any.Cabal ==3.8.1.0,
any.free ==5.1.10,
any.friendly-time ==0.4.1,
any.fusion-plugin-types ==0.1.0,
- any.generic-deriving ==1.14.4,
+ any.generic-deriving ==1.14.5,
generic-deriving +base-4-9,
any.generically ==0.1.1,
- any.ghc ==9.4.5,
+ any.generics-sop ==0.5.1.3,
+ any.ghc ==9.4.7,
any.ghc-bignum ==1.3,
- any.ghc-boot ==9.4.5,
- any.ghc-boot-th ==9.4.5,
- any.ghc-heap ==9.4.5,
- any.ghc-prim ==0.9.0,
- any.ghci ==9.4.5,
+ any.ghc-boot ==9.4.7,
+ any.ghc-boot-th ==9.4.7,
+ any.ghc-heap ==9.4.7,
+ any.ghc-prim ==0.9.1,
+ any.ghci ==9.4.7,
any.haddock-library ==1.11.0,
any.happy ==1.20.1.1,
- any.hashable ==1.4.2.0,
+ any.hashable ==1.4.3.0,
hashable +integer-gmp -random-initial-seed,
any.haskell-lexer ==1.1.1,
+ any.haskell-src-exts ==1.23.1,
+ any.haskell-src-meta ==0.8.13,
+ any.hdaemonize ==0.5.7,
any.heaps ==0.4,
- any.hedgehog ==1.2,
+ any.hedgehog ==1.4,
any.hostname ==1.0,
any.hourglass ==0.2.12,
any.hpc ==0.6.1.0,
- any.hsc2hs ==0.68.9,
+ any.hsc2hs ==0.68.10,
hsc2hs -in-ghc-tree,
+ any.hspec ==2.11.7,
+ any.hspec-core ==2.11.7,
+ any.hspec-discover ==2.11.7,
+ any.hspec-expectations ==0.8.4,
+ any.hsyslog ==5.0.2,
+ hsyslog -install-examples,
any.http-api-data ==0.5,
http-api-data -use-text-show,
- any.http-client ==0.7.13.1,
+ any.http-client ==0.7.15,
http-client +network-uri,
- any.http-client-tls ==0.3.6.2,
- any.http-conduit ==2.3.8.2,
+ any.http-client-tls ==0.3.6.3,
+ any.http-conduit ==2.3.8.3,
http-conduit +aeson,
any.http-date ==0.0.11,
- any.http-media ==0.8.0.0,
+ any.http-media ==0.8.1.1,
any.http-types ==0.12.3,
- any.http2 ==4.1.4,
+ any.http2 ==4.2.2,
http2 -devel -h2spec,
- any.indexed-profunctors ==0.1.1,
- any.indexed-traversable ==0.1.2.1,
+ any.indexed-profunctors ==0.1.1.1,
+ any.indexed-traversable ==0.1.3,
any.indexed-traversable-instances ==0.1.1.2,
+ any.insert-ordered-containers ==0.2.5.3,
+ any.integer-conversion ==0.1.0.1,
any.integer-gmp ==1.1,
any.integer-logarithms ==1.0.3.1,
integer-logarithms -check-bounds +integer-gmp,
- any.invariant ==0.6.1,
+ any.invariant ==0.6.2,
any.iproute ==1.7.12,
any.iso8601-time ==0.1.5,
iso8601-time +new-time,
any.kan-extensions ==5.2.5,
- any.lifted-async ==0.10.2.4,
+ any.lens ==5.2.3,
+ lens -benchmark-uniplate -dump-splices +inlining -j +test-hunit +test-properties +test-templates +trustworthy,
+ any.libsodium-bindings ==0.0.1.0,
+ libsodium-bindings -homebrew-libsodium -use-pkg-config,
+ any.lifted-async ==0.10.2.5,
any.lifted-base ==0.2.3.12,
any.lockfree-queue ==0.2.4,
any.log-base ==0.12.0.1,
@@ -187,13 +223,17 @@ constraints: any.Cabal ==3.8.1.0,
any.lucid ==2.11.20230408,
any.lucid-alpine ==0.1.0.7,
any.lucid-svg ==0.7.1.1,
- any.math-functions ==0.3.4.2,
+ any.math-functions ==0.3.4.3,
math-functions +system-erf +system-expm1,
+ any.megaparsec ==9.6.1,
+ megaparsec -dev,
any.memory ==0.18.0,
memory +support_bytestring +support_deepseq,
any.microlens ==0.4.13.1,
- any.mime-types ==0.1.1.0,
+ any.mime-types ==0.1.2.0,
any.mmorph ==1.2.0,
+ any.modern-uri ==0.3.6.1,
+ modern-uri -dev,
any.monad-control ==1.0.3.1,
any.monad-logger ==0.3.40,
monad-logger +template_haskell,
@@ -203,20 +243,30 @@ constraints: any.Cabal ==3.8.1.0,
any.monad-time-effectful ==1.0.0.0,
any.mono-traversable ==1.0.15.3,
any.mtl ==2.2.2,
+ any.mtl-compat ==0.2.2,
+ mtl-compat -two-point-one -two-point-two,
any.mwc-random ==0.15.0.2,
+ any.natural-arithmetic ==0.1.4.0,
any.network ==3.1.4.0,
network -devel,
- any.network-byte-order ==0.1.6,
+ any.network-byte-order ==0.1.7,
any.network-info ==0.2.1,
any.network-uri ==2.6.4.2,
any.odd-jobs ==0.2.3,
any.old-locale ==1.0.0.7,
any.old-time ==1.1.0.3,
- any.optics-core ==0.4.1,
+ any.one-time-password ==3.0.0.0,
+ any.openapi3 ==3.2.4,
+ any.optics-core ==0.4.1.1,
optics-core -explicit-generic-labels,
+ any.optics-extra ==0.4.2.1,
+ any.optics-th ==0.4.1,
any.optparse-applicative ==0.18.1.0,
optparse-applicative +process,
+ any.parallel ==3.2.2.0,
any.parsec ==3.1.16.1,
+ any.parser-combinators ==1.3.0,
+ parser-combinators -dev,
any.password ==3.0.2.1,
password +argon2 +bcrypt +pbkdf2 +scrypt,
any.password-types ==1.0.0.0,
@@ -226,35 +276,47 @@ constraints: any.Cabal ==3.8.1.0,
pg-entity -book -prod,
any.pg-transact ==0.3.2.0,
any.pg-transact-effectful ==0.0.1.0,
- any.pipes ==4.3.16,
- any.poolboy ==0.2.1.0,
+ any.poolboy ==0.2.2.0,
any.postgresql-libpq ==0.9.5.0,
postgresql-libpq -use-pkg-config,
any.postgresql-migration ==0.2.1.7,
- any.postgresql-simple ==0.6.5,
+ any.postgresql-simple ==0.6.5.1,
any.pretty ==1.1.3.6,
any.pretty-show ==1.10,
any.prettyprinter ==1.7.1,
prettyprinter -buildreadme +text,
any.prettyprinter-ansi-terminal ==1.1.3,
any.primitive ==0.8.0.0,
- any.process ==1.6.16.0,
+ any.primitive-addr ==0.1.0.2,
+ any.primitive-offset ==0.2.0.0,
+ any.primitive-unlifted ==2.1.0.0,
+ any.process ==1.6.17.0,
any.profunctors ==5.6.2,
any.prometheus-client ==1.1.0,
any.prometheus-metrics-ghc ==1.0.1.2,
any.prometheus-proc ==0.1.5.0,
- any.psqueues ==0.2.7.3,
+ any.psqueues ==0.2.8.0,
+ any.quickcheck-io ==0.2.0,
any.random ==1.2.1.1,
any.raven-haskell ==0.1.4.1,
+ raven-haskell -tests,
any.recv ==0.1.0,
+ any.reflection ==2.1.7,
+ reflection -slow +template-haskell,
any.regex-applicative ==0.3.4,
+ any.req ==3.13.1,
+ req -dev,
any.resource-pool ==0.4.0.0,
any.resourcet ==1.3.0,
+ any.retry ==0.9.3.1,
+ retry -lib-werror,
any.rts ==1.0.2,
+ any.run-st ==0.1.3.2,
any.safe ==0.3.19,
- any.safe-exceptions ==0.1.7.3,
+ any.safe-exceptions ==0.1.7.4,
any.scientific ==0.3.7.0,
scientific -bytestring-builder -integer-simple,
+ any.sel ==0.0.1.0,
any.semialign ==1.3,
semialign +semigroupoids,
any.semigroupoids ==5.3.7,
@@ -267,6 +329,7 @@ constraints: any.Cabal ==3.8.1.0,
any.servant-client-core ==0.19,
any.servant-effectful ==0.0.1.0,
any.servant-lucid ==0.9.0.6,
+ any.servant-openapi3 ==2.0.1.6,
any.servant-server ==0.19.2,
any.servant-static-th ==1.0.0.0,
servant-static-th -buildexample,
@@ -275,12 +338,12 @@ constraints: any.Cabal ==3.8.1.0,
any.singleton-bool ==0.1.6,
any.slugify ==0.1.0.1,
any.socks ==0.6.1,
- any.some ==1.0.5,
+ any.some ==1.0.6,
some +newtype-unsafe,
any.sop-core ==0.5.0.2,
any.souffle-haskell ==3.5.1,
- any.split ==0.2.3.5,
- any.splitmix ==0.1.0.4,
+ any.split ==0.2.4,
+ any.splitmix ==0.1.0.5,
splitmix -optimised-mixer,
any.stm ==2.5.1.0,
any.stm-chans ==3.0.0.9,
@@ -294,39 +357,49 @@ constraints: any.Cabal ==3.8.1.0,
any.string-conv ==0.2.0,
string-conv -lib-werror,
any.string-conversions ==0.4.0.1,
- any.tagged ==0.8.7,
+ any.syb ==0.7.2.4,
+ any.tagged ==0.8.8,
tagged +deepseq +transformers,
- any.tasty ==1.4.3,
+ any.tar ==0.5.1.1,
+ tar -old-bytestring -old-time,
+ any.tasty ==1.5,
tasty +unix,
- any.tasty-hunit ==0.10.0.3,
+ any.tasty-hunit ==0.10.1,
any.template-haskell ==2.19.0.0,
any.temporary ==1.3,
any.terminal-size ==0.3.4,
any.terminfo ==0.4.1.5,
any.text ==2.0.2,
any.text-conversions ==0.3.1.1,
- any.text-display ==0.0.5.0,
+ any.text-display ==0.0.5.1,
text-display -book,
any.text-manipulate ==0.3.1.0,
any.text-short ==0.1.5,
text-short -asserts,
+ any.tf-random ==0.5,
any.th-abstraction ==0.5.0.0,
any.th-compat ==0.1.4,
+ any.th-expand-syns ==0.4.11.0,
+ any.th-lift ==0.8.4,
+ any.th-orphans ==0.13.14,
+ any.th-reify-many ==0.1.10,
any.these ==1.2,
any.time ==1.12.2,
any.time-compat ==1.9.6.1,
time-compat -old-locale,
- any.time-manager ==0.0.0,
+ any.time-manager ==0.0.1,
any.timing-convenience ==0.1,
- any.tls ==1.7.0,
+ any.tls ==1.9.0,
tls +compat -hans +network,
+ any.torsor ==0.1,
any.transformers ==0.5.6.2,
any.transformers-base ==0.4.6,
transformers-base +orphaninstances,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
+ any.tuples ==0.1.0.0,
any.type-equality ==1,
- any.typed-process ==0.2.11.0,
+ any.typed-process ==0.2.11.1,
any.unicode-data ==0.4.0.1,
unicode-data -ucd2haskell,
any.unicode-transforms ==0.4.0.1,
@@ -335,17 +408,17 @@ constraints: any.Cabal ==3.8.1.0,
any.unix-compat ==0.7,
unix-compat -old-time,
any.unix-memory ==0.1.2,
- any.unix-time ==0.4.9,
+ any.unix-time ==0.4.11,
any.unliftio ==0.2.25.0,
any.unliftio-core ==0.2.1.0,
any.unordered-containers ==0.2.19.1,
unordered-containers -debug,
any.utf8-string ==1.0.2,
any.uuid ==1.3.15,
- any.uuid-types ==1.0.5,
+ any.uuid-types ==1.0.5.1,
any.vault ==0.3.1.5,
vault +useghc,
- any.vector ==0.13.0.0,
+ any.vector ==0.13.1.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-algorithms ==0.9.0.1,
vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks,
@@ -353,21 +426,24 @@ constraints: any.Cabal ==3.8.1.0,
any.void ==0.7.3,
void -safe,
any.wai ==3.2.3,
- any.wai-app-static ==3.1.7.4,
- wai-app-static +cryptonite -print,
+ any.wai-app-static ==3.1.8,
+ wai-app-static +crypton -print,
any.wai-extra ==3.1.13.0,
wai-extra -build-example,
any.wai-log ==0.4.0.1,
any.wai-logger ==2.4.0,
any.wai-middleware-heartbeat ==0.0.1.0,
any.wai-middleware-prometheus ==1.0.0.1,
- any.warp ==3.3.28,
+ any.warp ==3.3.30,
warp +allow-sendfilefd -network-bytestring -warp-debug -x509,
+ any.wide-word ==0.1.6.0,
any.witherable ==0.4.2,
any.wl-pprint-annotated ==0.1.0.1,
any.word8 ==0.1.3,
- any.xml-conduit ==1.9.1.2,
- any.xml-conduit-writer ==0.1.1.2,
+ any.xml-conduit ==1.9.1.3,
+ any.xml-conduit-writer ==0.1.1.4,
any.xml-types ==0.3.8,
+ any.zigzag ==0.0.1.0,
any.zlib ==0.6.3.0,
zlib -bundled-c-zlib -non-blocking-ffi -pkg-config
+index-state: hackage.haskell.org 2023-11-22T07:29:39Z
diff --git a/cbits/categorise.dl b/cbits/categorise.dl
index c8584fe0..a02b01cf 100644
--- a/cbits/categorise.dl
+++ b/cbits/categorise.dl
@@ -181,7 +181,11 @@ normalise_category("Database", "Databases").
normalise_category("PostgreSQL", "Databases").
normalise_category("NLP", "Natural Language Processing").
+normalise_category("Japanese Natural Language Processing", "Natural Language Processing").
+normalise_category("Natural Language", "Natural Language Processing").
+normalise_category("Natural Language Processing", "Natural Language Processing").
normalise_category("Stemming", "Natural Language Processing").
+normalise_category("Natural-language-processing", "Natural Language Processing").
normalise_category("Containers", "Data Structures").
diff --git a/design/package-lock.json b/design/package-lock.json
index de4f50e7..300fae26 100644
--- a/design/package-lock.json
+++ b/design/package-lock.json
@@ -100,12 +100,12 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
- "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"dependencies": {
- "@babel/types": "^7.22.15",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -211,22 +211,22 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"dependencies": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -388,9 +388,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz",
- "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -448,9 +448,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.22.16",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
- "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -1946,19 +1946,19 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz",
- "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.22.15",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.22.16",
- "@babel/types": "^7.22.19",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -1967,13 +1967,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz",
- "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.19",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -10017,9 +10017,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.29",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
- "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
@@ -12095,9 +12095,9 @@
}
},
"node_modules/vite": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
- "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.12.tgz",
+ "integrity": "sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.18.10",
@@ -12517,12 +12517,12 @@
}
},
"@babel/generator": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
- "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dev": true,
"requires": {
- "@babel/types": "^7.22.15",
+ "@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -12601,19 +12601,19 @@
}
},
"@babel/helper-environment-visitor": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
- "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"dev": true
},
"@babel/helper-function-name": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
- "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dev": true,
"requires": {
- "@babel/template": "^7.22.5",
- "@babel/types": "^7.22.5"
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
@@ -12727,9 +12727,9 @@
"dev": true
},
"@babel/helper-validator-identifier": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz",
- "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==",
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -12772,9 +12772,9 @@
}
},
"@babel/parser": {
- "version": "7.22.16",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz",
- "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
},
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
@@ -13775,31 +13775,31 @@
}
},
"@babel/traverse": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz",
- "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==",
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.22.13",
- "@babel/generator": "^7.22.15",
- "@babel/helper-environment-visitor": "^7.22.5",
- "@babel/helper-function-name": "^7.22.5",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
- "@babel/parser": "^7.22.16",
- "@babel/types": "^7.22.19",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
- "version": "7.22.19",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz",
- "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==",
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.22.5",
- "@babel/helper-validator-identifier": "^7.22.19",
+ "@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},
@@ -19549,9 +19549,9 @@
}
},
"postcss": {
- "version": "8.4.29",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz",
- "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==",
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",
@@ -21106,9 +21106,9 @@
"dev": true
},
"vite": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz",
- "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==",
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.12.tgz",
+ "integrity": "sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==",
"dev": true,
"requires": {
"esbuild": "^0.18.10",
diff --git a/design/stories/alerts.stories.js b/design/stories/alerts.stories.js
new file mode 100644
index 00000000..e4c582d5
--- /dev/null
+++ b/design/stories/alerts.stories.js
@@ -0,0 +1,6 @@
+
+export default {
+ title: "Components/Alerts"
+};
+
+export const Alert = () => "Info alert
Error alert
"
diff --git a/docs/docs/intro.md b/docs/docs/intro.md
index 35d24642..1c856170 100644
--- a/docs/docs/intro.md
+++ b/docs/docs/intro.md
@@ -2,3 +2,8 @@
title: Flora.pm
slug: /
---
+
+Read more about:
+
+* [Namespaces](/namespaces)
+* [Search features](/search-features)
diff --git a/docs/docs/namespaces.md b/docs/docs/namespaces.md
new file mode 100644
index 00000000..ab581bc7
--- /dev/null
+++ b/docs/docs/namespaces.md
@@ -0,0 +1,44 @@
+---
+title: Flora Namespaces
+slug: namespaces
+---
+
+In Flora, packages are categorised in a namespace to mark their provenance. They start with a `@` and allow us to refer to packages unambiguously or mark their importance.
+
+## @haskell Packages
+
+Some packages are foundational to the ecosystem and maintained by either the Core Libraries Committee or the GHC team, and this makes them unique in terms of the expectations we have from them.
+
+These packages live in the [`@haskell`] namespace to show that they are stable and reliable. Some example of packages are `base`, `text`, `bytestring`, and `mtl`.
+
+## @hackage Packages
+
+This is where most third-party packages live, which you will find on [Hackage](https://hackage.haskell.org).
+
+## @cardano Packages
+
+Flora also indexes the [Cardano Haskell Packages (CHaP)][CHaP], an index of packages by the Cardano project.
+These packages live under the [`@cardano`] namespace.
+
+To use them in your own project, insert the following configuration in your `cabal.project` file:
+
+```
+repository cardano
+ url: https://input-output-hk.github.io/cardano-haskell-packages
+ secure: True
+ root-keys:
+ 3e0cce471cf09815f930210f7827266fd09045445d65923e6d0238a6cd15126f
+ 443abb7fb497a134c343faf52f0b659bd7999bc06b7f63fa76dc99d631f9bea1
+ a86a1f6ce86c449c46666bda44268677abf29b5b2d2eb5ec7af903ec2f117a82
+ bcec67e8e99cabfa7764d75ad9b158d72bfacf70ca1d0ec8bc6b4406d1bf8413
+ c00aae8461a256275598500ea0e187588c35a5d5d7454fb57eac18d9edb86a56
+ d4a35cd3121aa00d18544bb0ac01c3e1691d618f462c46129271bccf39f7e8ee
+```
+and run `cabal update`.
+
+[`@haskell`]: https://flora.pm/packages/@haskell
+[`@cardano`]: https://flora.pm/packages/@cardano
+[`@hackage`]: https://flora.pm/packages/@hackage
+[`@hackage/servant-server`]: https://flora.pm/packages/@hackage/servant-server
+[`@haskell/text`]: https://flora.pm/packages/@haskell/text
+[CHaP]: https://input-output-hk.github.io/cardano-haskell-packages
diff --git a/docs/docs/search.md b/docs/docs/search.md
new file mode 100644
index 00000000..448cadc5
--- /dev/null
+++ b/docs/docs/search.md
@@ -0,0 +1,14 @@
+---
+title: Search features
+slug: search-features
+---
+
+While searching for packages you may want to refine the search terms with modifiers.
+Currently, the following modifiers are available:
+
+* `depends:<@namespace>/`: Shows the dependents page for a package
+* `in:<@namespace> `: Searches for a package name in the specified namespace
+* `in:<@namespace>`: Lists packages in a namespace
+
+These modifiers must be placed at the very beginning of the search query, otherwise they will
+be interpreted as a search term.
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 7c7a172c..7177115b 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -153,6 +153,14 @@
dependencies:
"@babel/highlight" "^7.22.5"
+"@babel/code-frame@^7.22.13":
+ version "7.22.13"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
+ integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ dependencies:
+ "@babel/highlight" "^7.22.13"
+ chalk "^2.4.2"
+
"@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
@@ -201,7 +209,7 @@
json5 "^2.2.2"
semver "^6.3.1"
-"@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.22.7", "@babel/generator@^7.22.9":
+"@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d"
integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==
@@ -211,6 +219,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
+"@babel/generator@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
+ integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
+ dependencies:
+ "@babel/types" "^7.23.0"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
"@babel/helper-annotate-as-pure@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
@@ -271,6 +289,11 @@
lodash.debounce "^4.0.8"
resolve "^1.14.2"
+"@babel/helper-environment-visitor@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
+ integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+
"@babel/helper-environment-visitor@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
@@ -284,6 +307,14 @@
"@babel/template" "^7.22.5"
"@babel/types" "^7.22.5"
+"@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
+
"@babel/helper-hoist-variables@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
@@ -377,6 +408,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+"@babel/helper-validator-identifier@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
+ integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
+
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
@@ -405,6 +441,15 @@
"@babel/traverse" "^7.22.6"
"@babel/types" "^7.22.5"
+"@babel/highlight@^7.22.13":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
+ integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.22.20"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+
"@babel/highlight@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031"
@@ -419,6 +464,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==
+"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
+ integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e"
@@ -1204,19 +1254,28 @@
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
-"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.4.5":
- version "7.22.8"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e"
- integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==
+"@babel/template@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
+ integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
dependencies:
- "@babel/code-frame" "^7.22.5"
- "@babel/generator" "^7.22.7"
- "@babel/helper-environment-visitor" "^7.22.5"
- "@babel/helper-function-name" "^7.22.5"
+ "@babel/code-frame" "^7.22.13"
+ "@babel/parser" "^7.22.15"
+ "@babel/types" "^7.22.15"
+
+"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.4.5":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
+ integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.0"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
- "@babel/parser" "^7.22.7"
- "@babel/types" "^7.22.5"
+ "@babel/parser" "^7.23.0"
+ "@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
@@ -1229,6 +1288,15 @@
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.15", "@babel/types@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
+ integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -2798,7 +2866,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
-bn.js@^5.0.0, bn.js@^5.1.1:
+bn.js@^5.0.0, bn.js@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
@@ -2922,7 +2990,7 @@ browserify-des@^1.0.0:
inherits "^2.0.1"
safe-buffer "^5.1.2"
-browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
+browserify-rsa@^4.0.0, browserify-rsa@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
@@ -2931,19 +2999,19 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
randombytes "^2.0.1"
browserify-sign@^4.0.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
- integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e"
+ integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==
dependencies:
- bn.js "^5.1.1"
- browserify-rsa "^4.0.1"
+ bn.js "^5.2.1"
+ browserify-rsa "^4.1.0"
create-hash "^1.2.0"
create-hmac "^1.1.7"
- elliptic "^6.5.3"
+ elliptic "^6.5.4"
inherits "^2.0.4"
- parse-asn1 "^5.1.5"
- readable-stream "^3.6.0"
- safe-buffer "^5.2.0"
+ parse-asn1 "^5.1.6"
+ readable-stream "^3.6.2"
+ safe-buffer "^5.2.1"
browserify-zlib@^0.2.0:
version "0.2.0"
@@ -3069,7 +3137,7 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
-chalk@^2.0.0:
+chalk@^2.0.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -3962,7 +4030,7 @@ electron-to-chromium@^1.4.477:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.480.tgz#40e32849ca50bc23ce29c1516c5adb3fddac919d"
integrity sha512-IXTgg+bITkQv/FLP9FjX6f9KFCs5hQWeh5uNSKxB9mqYj/JXhHDbu+ekS43LVvbkL3eW6/oZy4+r9Om6lan1Uw==
-elliptic@^6.5.3:
+elliptic@^6.5.3, elliptic@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
@@ -6178,7 +6246,7 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
-parse-asn1@^5.0.0, parse-asn1@^5.1.5:
+parse-asn1@^5.0.0, parse-asn1@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
@@ -6640,9 +6708,9 @@ postcss-zindex@^5.1.0:
integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A==
postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.21:
- version "8.4.27"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057"
- integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==
+ version "8.4.31"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
+ integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
@@ -7013,7 +7081,7 @@ readable-stream@^2.0.1:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-readable-stream@^3.0.6, readable-stream@^3.5.0, readable-stream@^3.6.0:
+readable-stream@^3.0.6, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
@@ -7321,7 +7389,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
diff --git a/environment.docker.sh b/environment.docker.sh
index d2dc616a..90c5e1bb 100755
--- a/environment.docker.sh
+++ b/environment.docker.sh
@@ -6,3 +6,5 @@ export FLORA_HTTP_PORT=8084
export FLORA_DB_CONNSTRING="host=${FLORA_DB_HOST} dbname=${FLORA_DB_DATABASE}\
user=${FLORA_DB_USER} password=${FLORA_DB_PASSWORD}"
export PGPASSWORD=${FLORA_DB_PASSWORD}
+
+export FLORA_PG_URI="postgresql://${FLORA_DB_USER}:${FLORA_DB_PASSWORD}@${FLORA_DB_HOST}:${FLORA_DB_PORT}/${FLORA_DB_DATABASE}"
diff --git a/environment.sh b/environment.sh
index a0be0c3d..141c9925 100755
--- a/environment.sh
+++ b/environment.sh
@@ -7,7 +7,7 @@ export FLORA_DB_PORT="5432"
export FLORA_DB_USER="postgres"
export FLORA_DB_PASSWORD="postgres"
export FLORA_DB_DATABASE="flora_dev"
-export FLORA_DB_POOL_CONNECTIONS="96"
+export FLORA_DB_POOL_CONNECTIONS="50"
export FLORA_DB_TIMEOUT="10"
export FLORA_DB_SSLMODE="allow"
export FLORA_DB_PARAMETERS="?sslmode=verify-ca"
diff --git a/environment.test.sh b/environment.test.sh
index 3fcec8c4..0ec5421d 100755
--- a/environment.test.sh
+++ b/environment.test.sh
@@ -9,3 +9,5 @@ export FLORA_DB_DATABASE="flora_test"
export FLORA_DB_CONNSTRING="host=${FLORA_DB_HOST} dbname=${FLORA_DB_DATABASE} \
user=${FLORA_DB_USER} password=${FLORA_DB_PASSWORD} \
sslmode=allow"
+
+export FLORA_DB_POOL_CONNECTIONS=50
diff --git a/flake.lock b/flake.lock
index 24bbb448..adc96ef1 100644
--- a/flake.lock
+++ b/flake.lock
@@ -201,11 +201,11 @@
"nixpkgs": "nixpkgs_4"
},
"locked": {
- "lastModified": 1689157215,
- "narHash": "sha256-RbmIc0+wcoZzQ6MBXsFcNhOJts3yWmBrbHnZcr5hhsY=",
+ "lastModified": 1696069899,
+ "narHash": "sha256-ZLIgeaIjmzlICtCg88xiaJsM7u03F3WGxoDCAknCfvE=",
"ref": "refs/heads/master",
- "rev": "f586739656a9ac84528f09dcf2aebe48d26699d2",
- "revCount": 1126,
+ "rev": "a863ad211af049462319295cb74f0261c88532ca",
+ "revCount": 1130,
"type": "git",
"url": "https://gitlab.horizon-haskell.net/package-sets/horizon-platform"
},
diff --git a/flora.cabal b/flora.cabal
index 21432020..c39e599e 100644
--- a/flora.cabal
+++ b/flora.cabal
@@ -1,6 +1,6 @@
cabal-version: 3.0
name: flora
-version: 1.0.13
+version: 1.0.14
homepage: https://github.com/flora-pm/flora-server/#readme
bug-reports: https://github.com/flora-pm/flora-server/issues
author: Théophile Choutri
@@ -12,7 +12,7 @@ extra-source-files:
LICENSE
README.md
-tested-with: GHC ==9.4.5
+tested-with: GHC ==9.4.7
source-repository head
type: git
@@ -27,19 +27,18 @@ flag prod
common common-extensions
default-extensions:
- NoStarIsType
DataKinds
DeriveAnyClass
DerivingStrategies
DerivingVia
DuplicateRecordFields
LambdaCase
+ NoStarIsType
OverloadedLabels
OverloadedRecordDot
OverloadedStrings
PackageImports
PolyKinds
- QuasiQuotes
RecordWildCards
StrictData
TypeFamilies
@@ -81,6 +80,7 @@ library
Data.Positive
Data.Text.Display.Orphans
Data.Time.Orphans
+ Database.PostgreSQL.Simple.Orphans
Distribution.Orphans
Distribution.Orphans.CompilerFlavor
Distribution.Orphans.ConfVar
@@ -96,6 +96,12 @@ library
Flora.Import.Types
Flora.Logging
Flora.Model.Admin.Report
+ Flora.Model.BlobIndex.Internal
+ Flora.Model.BlobIndex.Query
+ Flora.Model.BlobIndex.Types
+ Flora.Model.BlobIndex.Update
+ Flora.Model.BlobStore.API
+ Flora.Model.BlobStore.Types
Flora.Model.Category
Flora.Model.Category.Query
Flora.Model.Category.Types
@@ -111,7 +117,9 @@ library
Flora.Model.Package.Query
Flora.Model.Package.Types
Flora.Model.Package.Update
- Flora.Model.PackageIndex
+ Flora.Model.PackageIndex.Query
+ Flora.Model.PackageIndex.Types
+ Flora.Model.PackageIndex.Update
Flora.Model.PersistentSession
Flora.Model.Release
Flora.Model.Release.Query
@@ -123,14 +131,17 @@ library
Flora.Model.User.Query
Flora.Model.User.Update
Flora.Publish
+ Flora.QRCode
Flora.Search
JSON
Log.Backend.File
Lucid.Orphans
+ Servant.API.ContentTypes.GZip
build-depends:
, aeson
, base ^>=4.17
+ , base16-bytestring
, base64
, blaze-builder
, bytestring
@@ -138,6 +149,7 @@ library
, colourista
, containers
, cryptohash-md5
+ , cryptohash-sha256
, cryptonite
, cryptonite-conduit
, deepseq
@@ -145,9 +157,12 @@ library
, directory
, effectful-core
, envparse
+ , extra
, filepath
, http-api-data
+ , http-media
, iso8601-time
+ , JuicyPixels
, log-base
, log-effectful
, lucid
@@ -166,7 +181,11 @@ library
, poolboy
, postgresql-simple
, pretty
+ , qrcode-core
+ , qrcode-juicypixels
, resource-pool
+ , sel
+ , servant
, servant-lucid
, servant-server
, slugify
@@ -178,6 +197,7 @@ library
, text-display
, time
, unliftio
+ , utf8-string
, uuid
, vector
, vector-algorithms
@@ -198,6 +218,7 @@ library flora-web
FloraWeb.API.Server.Packages
FloraWeb.Client
FloraWeb.Common.Auth
+ FloraWeb.Common.Auth.TwoFactor
FloraWeb.Common.Auth.Types
FloraWeb.Common.Guards
FloraWeb.Common.Metrics
@@ -205,13 +226,18 @@ library flora-web
FloraWeb.Common.Pagination
FloraWeb.Common.Tracing
FloraWeb.Common.Utils
+ FloraWeb.Components.Alert
+ FloraWeb.Components.Button
FloraWeb.Components.CategoryCard
FloraWeb.Components.Footer
FloraWeb.Components.Header
+ FloraWeb.Components.Icons
+ FloraWeb.Components.MainSearchBar
FloraWeb.Components.Navbar
FloraWeb.Components.PackageListHeader
FloraWeb.Components.PackageListItem
FloraWeb.Components.PaginationNav
+ FloraWeb.Components.SlimSearchBar
FloraWeb.Components.Utils
FloraWeb.Components.VersionListHeader
FloraWeb.Embedded
@@ -222,12 +248,14 @@ library flora-web
FloraWeb.Pages.Routes.Packages
FloraWeb.Pages.Routes.Search
FloraWeb.Pages.Routes.Sessions
+ FloraWeb.Pages.Routes.Settings
FloraWeb.Pages.Server
FloraWeb.Pages.Server.Admin
FloraWeb.Pages.Server.Categories
FloraWeb.Pages.Server.Packages
FloraWeb.Pages.Server.Search
FloraWeb.Pages.Server.Sessions
+ FloraWeb.Pages.Server.Settings
FloraWeb.Pages.Templates
FloraWeb.Pages.Templates.Admin
FloraWeb.Pages.Templates.Admin.Packages
@@ -235,13 +263,14 @@ library flora-web
FloraWeb.Pages.Templates.Error
FloraWeb.Pages.Templates.Haddock
FloraWeb.Pages.Templates.Packages
- FloraWeb.Pages.Templates.Pages.Categories
- FloraWeb.Pages.Templates.Pages.Categories.Index
- FloraWeb.Pages.Templates.Pages.Categories.Show
- FloraWeb.Pages.Templates.Pages.Home
- FloraWeb.Pages.Templates.Pages.Packages
- FloraWeb.Pages.Templates.Pages.Search
- FloraWeb.Pages.Templates.Pages.Sessions
+ FloraWeb.Pages.Templates.Screens.Categories
+ FloraWeb.Pages.Templates.Screens.Categories.Index
+ FloraWeb.Pages.Templates.Screens.Categories.Show
+ FloraWeb.Pages.Templates.Screens.Home
+ FloraWeb.Pages.Templates.Screens.Packages
+ FloraWeb.Pages.Templates.Screens.Search
+ FloraWeb.Pages.Templates.Screens.Sessions
+ FloraWeb.Pages.Templates.Screens.Settings
FloraWeb.Pages.Templates.Types
FloraWeb.Routes
FloraWeb.Servant.Common
@@ -253,8 +282,10 @@ library flora-web
, aeson
, async
, base ^>=4.17
+ , base32
, bytestring
, Cabal-syntax
+ , chronos
, clock
, cmark-gfm
, colourista
@@ -265,6 +296,7 @@ library flora-web
, deriving-aeson
, effectful
, effectful-core
+ , extra
, flora
, flora-jobs
, haddock-library
@@ -283,6 +315,7 @@ library flora-web
, mtl
, network-uri
, odd-jobs
+ , one-time-password
, openapi3
, optics-core
, password
@@ -297,6 +330,7 @@ library flora-web
, raven-haskell
, resource-pool
, safe-exceptions
+ , sel
, servant
, servant-client
, servant-client-core
@@ -306,6 +340,7 @@ library flora-web
, text
, text-display
, time
+ , torsor
, uuid
, vector
, vector-algorithms
@@ -353,6 +388,7 @@ library flora-jobs
, pg-entity
, pg-transact-effectful
, postgresql-simple
+ , req
, resource-pool
, servant
, servant-client
@@ -360,7 +396,6 @@ library flora-jobs
, text
, text-display
, time
- , typed-process
, vector
executable flora-server
@@ -389,16 +424,20 @@ executable flora-cli
, flora
, flora-web
, log-base
+ , log-effectful
, lucid
+ , monad-time-effectful
, optics-core
, optparse-applicative
, password-types
, pg-transact-effectful
, PyF
, text
+ , text-display
, transformers
, uuid
, vector
+ , zlib
test-suite flora-test
import: common-extensions
@@ -410,10 +449,12 @@ test-suite flora-test
build-depends:
, aeson
, base
+ , bytestring
, Cabal-syntax
, containers
, effectful-core
, exceptions
+ , filepath
, flora
, flora-web
, hedgehog
@@ -423,6 +464,7 @@ test-suite flora-test
, monad-time-effectful
, optics-core
, password
+ , password-types
, pg-entity
, pg-transact
, pg-transact-effectful
@@ -432,20 +474,25 @@ test-suite flora-test
, servant
, servant-client
, servant-server
+ , tar
, tasty
, tasty-hunit
, text
, time
, transformers
+ , typed-process
, uuid
, vector
+ , zlib
other-modules:
+ Flora.BlobSpec
Flora.CabalSpec
Flora.CategorySpec
Flora.ImportSpec
Flora.OddJobSpec
Flora.PackageSpec
+ Flora.SearchSpec
Flora.TemplateSpec
Flora.TestUtils
Flora.UserSpec
diff --git a/migrations/20230809181650_create_binary.sql b/migrations/20230809181650_create_binary.sql
new file mode 100644
index 00000000..8d883aa0
--- /dev/null
+++ b/migrations/20230809181650_create_binary.sql
@@ -0,0 +1,12 @@
+create table if not exists blob_relations (
+ blob_hash text not null,
+ blob_dep_hash text not null,
+ blob_dep_path text not null,
+ blob_dep_directory bool not null,
+
+ constraint pk_relation primary key (blob_hash, blob_dep_path)
+);
+
+-- Just points to the hash of the root directory
+alter table releases add tarball_root_hash text;
+alter table releases add tarball_archive_hash text;
diff --git a/migrations/20230924120348_add_revised_at.sql b/migrations/20230924120348_add_revised_at.sql
new file mode 100644
index 00000000..e8a038b6
--- /dev/null
+++ b/migrations/20230924120348_add_revised_at.sql
@@ -0,0 +1,2 @@
+alter table releases
+ add revised_at timestamptz
diff --git a/migrations/20231002224826_add_url_to_package_indexes.sql b/migrations/20231002224826_add_url_to_package_indexes.sql
new file mode 100644
index 00000000..f0b25069
--- /dev/null
+++ b/migrations/20231002224826_add_url_to_package_indexes.sql
@@ -0,0 +1,2 @@
+alter table package_indexes
+ add url text not null;
diff --git a/migrations/20231020123433_add_requirement_components.sql b/migrations/20231020123433_add_requirement_components.sql
new file mode 100644
index 00000000..27564aad
--- /dev/null
+++ b/migrations/20231020123433_add_requirement_components.sql
@@ -0,0 +1,4 @@
+alter table requirements
+ drop column metadata;
+alter table requirements
+ add components text[];
diff --git a/migrations/20231122154627_add_totp_to_users.sql b/migrations/20231122154627_add_totp_to_users.sql
new file mode 100644
index 00000000..cb6fbb24
--- /dev/null
+++ b/migrations/20231122154627_add_totp_to_users.sql
@@ -0,0 +1,3 @@
+alter table users
+ add column totp_key text,
+ add column totp_enabled boolean not null;
diff --git a/migrations/20231210110311_add_description_to_package_index.sql b/migrations/20231210110311_add_description_to_package_index.sql
new file mode 100644
index 00000000..14ee8e13
--- /dev/null
+++ b/migrations/20231210110311_add_description_to_package_index.sql
@@ -0,0 +1,2 @@
+alter table package_indexes
+ add column description text not null default '';
diff --git a/scripts/.zshrc b/scripts/.zshrc
index 02ecec4d..9942263a 100644
--- a/scripts/.zshrc
+++ b/scripts/.zshrc
@@ -1,7 +1,5 @@
#!/usr/bin/env zsh
-set -euo pipefail
-
export SHELL="zsh"
export ZSH="$HOME/.oh-my-zsh"
export LANG=C.UTF-8
diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh
index 72b11e41..9ac307c1 100755
--- a/scripts/run-tests.sh
+++ b/scripts/run-tests.sh
@@ -7,11 +7,6 @@ source ./environment.test.sh
export DATALOG_DIR="cbits/"
-make db-drop
-make db-setup
-
-cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000"
-
if [ -z "$1" ] ;
then
cabal test
diff --git a/src/core/Flora/Environment.hs b/src/core/Flora/Environment.hs
index 92498dcc..98e23bd4 100644
--- a/src/core/Flora/Environment.hs
+++ b/src/core/Flora/Environment.hs
@@ -4,6 +4,8 @@ module Flora.Environment
( FloraEnv (..)
, DeploymentEnv (..)
, LoggingEnv (..)
+ , FeatureEnv (..)
+ , BlobStoreImpl (..)
, TestEnv (..)
, getFloraEnv
, getFloraTestEnv
@@ -11,6 +13,7 @@ module Flora.Environment
where
import Colourista.IO (blueMessage)
+import Data.Aeson (ToJSON)
import Data.ByteString (ByteString)
import Data.Pool (Pool)
import Data.Pool qualified as Pool
@@ -37,6 +40,7 @@ data FloraEnv = FloraEnv
, domain :: Text
, logging :: LoggingEnv
, environment :: DeploymentEnv
+ , features :: FeatureEnv
, config :: FloraConfig
, assets :: Assets
}
@@ -64,6 +68,25 @@ mkPool connectionInfo timeout' connections =
(realToFrac timeout')
connections
+data BlobStoreImpl = BlobStoreFS FilePath | BlobStorePure
+ deriving stock (Generic, Show)
+
+instance ToJSON BlobStoreImpl
+
+newtype FeatureEnv = FeatureEnv {blobStoreImpl :: Maybe BlobStoreImpl}
+ deriving stock (Generic, Show)
+
+instance ToJSON FeatureEnv
+
+-- In future we'll want to error for conflicting options
+featureConfigToEnv :: FeatureConfig -> Eff es FeatureEnv
+featureConfigToEnv FeatureConfig{..} =
+ case blobStoreFS of
+ Just fp | tarballsEnabled -> pure . FeatureEnv . Just $ BlobStoreFS fp
+ _ ->
+ pure . FeatureEnv $
+ if tarballsEnabled then Just BlobStorePure else Nothing
+
configToEnv :: (Fail :> es, IOE :> es) => FloraConfig -> Eff es FloraEnv
configToEnv floraConfig = do
let PoolConfig{connectionTimeout, connections} = floraConfig.dbConfig
@@ -71,6 +94,7 @@ configToEnv floraConfig = do
jobsPool <- mkPool floraConfig.connectionInfo connectionTimeout connections
assets <- getAssets floraConfig.environment
liftIO $ print assets
+ featureEnv <- featureConfigToEnv floraConfig.features
pure
FloraEnv
{ pool = pool
@@ -80,6 +104,7 @@ configToEnv floraConfig = do
, domain = floraConfig.domain
, logging = floraConfig.logging
, environment = floraConfig.environment
+ , features = featureEnv
, assets = assets
, config = floraConfig
}
diff --git a/src/core/Flora/Environment/Config.hs b/src/core/Flora/Environment/Config.hs
index c0f0580f..5cd36aea 100644
--- a/src/core/Flora/Environment/Config.hs
+++ b/src/core/Flora/Environment/Config.hs
@@ -2,6 +2,7 @@
module Flora.Environment.Config
( FloraConfig (..)
, LoggingEnv (..)
+ , FeatureConfig (..)
, ConnectionInfo (..)
, TestConfig (..)
, PoolConfig (..)
@@ -43,12 +44,14 @@ import Env
, def
, help
, nonempty
+ , optional
, str
, switch
, var
, (<=<)
)
import GHC.Generics (Generic)
+import System.FilePath (isValid)
import Text.Read (readMaybe)
data ConnectionInfo = ConnectionInfo
@@ -100,6 +103,12 @@ data LoggingEnv = LoggingEnv
}
deriving stock (Show, Generic)
+data FeatureConfig = FeatureConfig
+ { tarballsEnabled :: Bool
+ , blobStoreFS :: Maybe FilePath
+ }
+ deriving stock (Show, Generic)
+
-- | The datatype that is used to model the external configuration
data FloraConfig = FloraConfig
{ dbConfig :: PoolConfig
@@ -107,6 +116,7 @@ data FloraConfig = FloraConfig
, domain :: Text
, httpPort :: Word16
, logging :: LoggingEnv
+ , features :: FeatureConfig
, environment :: DeploymentEnv
}
deriving stock (Show, Generic)
@@ -135,7 +145,7 @@ parsePoolConfig =
<*> var
(int >=> nonNegative)
"FLORA_DB_POOL_CONNECTIONS"
- (help "Number of connections per sub-pool")
+ (help "Number of connections across all sub-pools")
parseLoggingEnv :: Parser Error LoggingEnv
parseLoggingEnv =
@@ -144,6 +154,15 @@ parseLoggingEnv =
<*> switch "FLORA_PROMETHEUS_ENABLED" (help "Whether or not Prometheus is enabled")
<*> var loggingDestination "FLORA_LOGGING_DESTINATION" (help "Where do the logs go")
+parseFeatures :: Parser Error FeatureConfig
+parseFeatures =
+ FeatureConfig
+ <$> switch "FLORA_TARBALLS_ENABLED" (help "Whether to store package tarballs, by default off for now")
+ <*> optional
+ ( var filepath "FLORA_TARBALLS_FS_PATH" $
+ help "Store tarball blobs in the supplied filesystem directory"
+ )
+
parsePort :: Parser Error Word16
parsePort = var port "FLORA_HTTP_PORT" (help "HTTP Port for Flora")
@@ -162,6 +181,7 @@ parseConfig =
<*> parseDomain
<*> parsePort
<*> parseLoggingEnv
+ <*> parseFeatures
<*> parseDeploymentEnv
parseTestConfig :: Parser Error TestConfig
@@ -204,6 +224,9 @@ loggingDestination "json" = Right Json
loggingDestination "json-file" = Right JSONFile
loggingDestination e = Left $ unread e
+filepath :: Reader Error FilePath
+filepath fp = if isValid fp then Right fp else Left $ unread fp
+
getAssets :: (Fail :> es, IOE :> es) => DeploymentEnv -> Eff es Assets
getAssets environment =
case environment of
diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs
index a19dfe11..9b031d46 100644
--- a/src/core/Flora/Import/Package.hs
+++ b/src/core/Flora/Import/Package.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE MultiWayIf #-}
+
-- |
-- Module: Flora.Import.Package
--
@@ -34,6 +36,7 @@ import Data.Time (UTCTime)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Database.PostgreSQL.Simple (Connection)
+import Distribution.Compat.NonEmptySet (toList)
import Distribution.Compiler (CompilerFlavor (..))
import Distribution.Fields.ParseResult
import Distribution.PackageDescription (CondBranch (..), CondTree (condTreeData), Condition (CNot), ConfVar, UnqualComponentName, allLibraries, unPackageName, unUnqualComponentName)
@@ -81,9 +84,7 @@ import Flora.Model.Release.Types
import Flora.Model.Release.Update qualified as Update
import Flora.Model.Requirement
( Requirement (..)
- , RequirementMetadata (..)
, deterministicRequirementId
- , flag
)
import Flora.Model.User
@@ -124,7 +125,8 @@ coreLibraries =
versionList :: Set Version
versionList =
Set.fromList
- [ Version.mkVersion [9, 8, 1]
+ [ Version.mkVersion [9, 10, 1]
+ , Version.mkVersion [9, 8, 1]
, Version.mkVersion [9, 6, 3]
, Version.mkVersion [9, 6, 2]
, Version.mkVersion [9, 6, 1]
@@ -180,11 +182,13 @@ importFile
=> UserId
-> FilePath
-- ^ The absolute path to the Cabal file
+ -> (Text, Set PackageName)
+ -- ^ The name of the repository
-> Eff es ()
-importFile userId path =
+importFile userId path repo =
withWorkerDbPool $ \wq ->
loadFile path
- >>= uncurry (extractPackageDataFromCabal userId Nothing)
+ >>= uncurry (extractPackageDataFromCabal userId repo)
>>= persistImportOutput wq
enqueueImportJob :: (DB :> es, IOE :> es) => ImportOutput -> Eff es ()
@@ -201,10 +205,15 @@ enqueueImportJob importOutput = do
(ImportPackage importOutput)
)
-importRelFile :: (Time :> es, Reader PoolConfig :> es, DB :> es, IOE :> es, Log :> es) => UserId -> FilePath -> Eff es ()
-importRelFile user dir = do
+importRelFile
+ :: (Time :> es, Reader PoolConfig :> es, DB :> es, IOE :> es, Log :> es)
+ => UserId
+ -> FilePath
+ -> (Text, Set PackageName)
+ -> Eff es ()
+importRelFile user dir repo = do
workdir <- (> dir) <$> liftIO System.getCurrentDirectory
- importFile user workdir
+ importFile user workdir repo
-- | Loads and parses a Cabal file
loadFile
@@ -242,17 +251,22 @@ parseString parser name bs = do
Log.logAttention_ (display $ show err)
throw $ CabalFileCouldNotBeParsed name
-loadAndExtractCabalFile :: (IOE :> es, Log :> es, Time :> es) => UserId -> FilePath -> Eff es ImportOutput
-loadAndExtractCabalFile userId filePath =
+loadAndExtractCabalFile
+ :: (IOE :> es, Log :> es, Time :> es)
+ => UserId
+ -> FilePath
+ -> (Text, Set PackageName)
+ -> Eff es ImportOutput
+loadAndExtractCabalFile userId filePath repo =
loadFile filePath
- >>= uncurry (extractPackageDataFromCabal userId Nothing)
+ >>= uncurry (extractPackageDataFromCabal userId repo)
-- | Persists an 'ImportOutput' to the database. An 'ImportOutput' can be obtained
-- by extracting relevant information from a Cabal file using 'extractPackageDataFromCabal'
persistImportOutput :: (DB :> es, IOE :> es) => Poolboy.WorkQueue -> ImportOutput -> Eff es ()
persistImportOutput wq (ImportOutput package categories release components) = do
dbPool <- getPool
- liftIO . T.putStrLn $ "📦 Persisting package: " <> packageName <> ", 🗓 Release v" <> display (release.version)
+ liftIO . T.putStrLn $ "📦 Persisting package: " <> packageName <> ", 🗓 Release v" <> display release.version
persistPackage
Update.upsertRelease release
parallelRun dbPool (persistComponent dbPool) components
@@ -260,7 +274,7 @@ persistImportOutput wq (ImportOutput package categories release components) = do
where
parallelRun :: (MonadIO m, Foldable t) => Pool Connection -> (a -> Eff [DB, IOE] b) -> t a -> m ()
parallelRun pool f xs = liftIO $ forM_ xs $ Poolboy.enqueue wq . void . runEff . runDB pool . f
- packageName = display (package.namespace) <> "/" <> display (package.name)
+ packageName = display package.namespace <> "/" <> display package.name
persistPackage = do
let packageId = package.packageId
Update.upsertPackage package
@@ -269,7 +283,7 @@ persistImportOutput wq (ImportOutput package categories release components) = do
persistComponent dbPool (packageComponent, deps) = do
liftIO . T.putStrLn $
"🧩 Persisting component: "
- <> display (packageComponent.canonicalForm)
+ <> display packageComponent.canonicalForm
<> " with "
<> display (length deps)
<> " dependencies."
@@ -277,30 +291,39 @@ persistImportOutput wq (ImportOutput package categories release components) = do
parallelRun dbPool persistImportDependency deps
persistImportDependency dep = do
- Update.upsertPackage (dep.package)
- Update.upsertRequirement (dep.requirement)
+ Update.upsertPackage dep.package
+ Update.upsertRequirement dep.requirement
withWorkerDbPool :: (Reader PoolConfig :> es, IOE :> es) => (Poolboy.WorkQueue -> Eff es a) -> Eff es a
withWorkerDbPool f = do
cfg <- ask @PoolConfig
withEffToIO $ \effIO ->
- Poolboy.withPoolboy (Poolboy.poolboySettingsWith cfg.connections) Poolboy.waitingStopFinishWorkers $ \wq ->
- effIO $ f wq
+ Poolboy.withPoolboy
+ (Poolboy.poolboySettingsWith cfg.connections)
+ Poolboy.waitingStopFinishWorkers
+ $ \wq ->
+ effIO $ f wq
-- | Transforms a 'GenericPackageDescription' from Cabal into an 'ImportOutput'
-- that can later be inserted into the database. This function produces stable, deterministic ids,
-- so it should be possible to extract and insert a single package many times in a row.
-extractPackageDataFromCabal :: (IOE :> es, Time :> es) => UserId -> Maybe Text -> UTCTime -> GenericPackageDescription -> Eff es ImportOutput
-extractPackageDataFromCabal userId repository uploadTime genericDesc = do
+extractPackageDataFromCabal
+ :: (IOE :> es, Time :> es)
+ => UserId
+ -> (Text, Set PackageName)
+ -> UTCTime
+ -> GenericPackageDescription
+ -> Eff es ImportOutput
+extractPackageDataFromCabal userId (repositoryName, repositoryPackages) uploadTime genericDesc = do
let packageDesc = genericDesc.packageDescription
let flags = Vector.fromList genericDesc.genPackageFlags
let packageName = force $ packageDesc ^. #package % #pkgName % to unPackageName % to pack % to PackageName
- let packageVersion = force $ packageDesc.package.pkgVersion
- let namespace = force $ chooseNamespace packageName
- let packageId = force $ deterministicPackageId namespace packageName
- let releaseId = force $ deterministicReleaseId packageId packageVersion
+ let packageVersion = force packageDesc.package.pkgVersion
+ let namespace = chooseNamespace packageName repositoryName repositoryPackages
+ let packageId = deterministicPackageId namespace packageName
+ let releaseId = deterministicReleaseId packageId packageVersion
timestamp <- Time.currentTime
- let sourceRepos = getRepoURL packageName $ packageDesc.sourceRepos
+ let sourceRepos = getRepoURL packageName packageDesc.sourceRepos
let rawCategoryField = packageDesc ^. #category % to Cabal.fromShortText % to T.pack
let categoryList = fmap (Tuning.UserPackageCategory . T.stripStart . T.stripEnd) (T.splitOn "," rawCategoryField)
categories <- liftIO $ Tuning.normalisedCategories <$> Tuning.normalise categoryList
@@ -329,7 +352,9 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do
, readmeStatus = NotImported
, changelog = Nothing
, changelogStatus = NotImported
- , repository
+ , repository = Just repositoryName
+ , tarballRootHash = Nothing
+ , tarballArchiveHash = Nothing
, license = Cabal.license packageDesc
, sourceRepos
, homepage = Just $ display packageDesc.homepage
@@ -341,23 +366,24 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do
, flags = ReleaseFlags flags
, testedWith = getVersions . extractTestedWith . Vector.fromList $ packageDesc.testedWith
, deprecated = Nothing
+ , revisedAt = Nothing
}
- let lib = extractLibrary package release Nothing [] <$> allLibraries packageDesc
- let condLib = maybe [] (extractCondTree extractLibrary package release Nothing) (genericDesc.condLibrary)
- let condSubLibs = extractCondTrees extractLibrary package release genericDesc.condSubLibraries
+ let lib = extractLibrary package (repositoryName, repositoryPackages) release Nothing [] <$> allLibraries packageDesc
+ let condLib = maybe [] (extractCondTree extractLibrary package (repositoryName, repositoryPackages) release Nothing) genericDesc.condLibrary
+ let condSubLibs = extractCondTrees extractLibrary package (repositoryName, repositoryPackages) release genericDesc.condSubLibraries
- let foreignLibs = extractForeignLib package release Nothing [] <$> packageDesc.foreignLibs
- let condForeignLibs = extractCondTrees extractForeignLib package release genericDesc.condForeignLibs
+ let foreignLibs = extractForeignLib package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.foreignLibs
+ let condForeignLibs = extractCondTrees extractForeignLib package (repositoryName, repositoryPackages) release genericDesc.condForeignLibs
- let executables = extractExecutable package release Nothing [] <$> packageDesc.executables
- let condExecutables = extractCondTrees extractExecutable package release genericDesc.condExecutables
+ let executables = extractExecutable package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.executables
+ let condExecutables = extractCondTrees extractExecutable package (repositoryName, repositoryPackages) release genericDesc.condExecutables
- let testSuites = extractTestSuite package release Nothing [] <$> packageDesc.testSuites
- let condTestSuites = extractCondTrees extractTestSuite package release genericDesc.condTestSuites
+ let testSuites = extractTestSuite package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.testSuites
+ let condTestSuites = extractCondTrees extractTestSuite package (repositoryName, repositoryPackages) release genericDesc.condTestSuites
- let benchmarks = extractBenchmark package release Nothing [] <$> packageDesc.benchmarks
- let condBenchmarks = extractCondTrees extractBenchmark package release genericDesc.condBenchmarks
+ let benchmarks = extractBenchmark package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.benchmarks
+ let condBenchmarks = extractCondTrees extractBenchmark package (repositoryName, repositoryPackages) release genericDesc.condBenchmarks
let components =
lib
@@ -373,41 +399,76 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do
<> condBenchmarks
pure ImportOutput{..}
-extractLibrary :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Library -> ImportComponent
-extractLibrary package =
+extractLibrary
+ :: Package
+ -> (Text, Set PackageName)
+ -> Release
+ -> Maybe UnqualComponentName
+ -> [Condition ConfVar]
+ -> Library
+ -> ImportComponent
+extractLibrary package repository =
genericComponentExtractor
Component.Library
- (^. #libName % to getLibName)
+ (^. #libName % to (getLibName package.name))
(^. #libBuildInfo % #targetBuildDepends)
package
- where
- getLibName :: LibraryName -> Text
- getLibName LMainLibName = display (package.name)
- getLibName (LSubLibName lname) = T.pack $ unUnqualComponentName lname
+ repository
+
+getLibName :: PackageName -> LibraryName -> Text
+getLibName pname LMainLibName = display pname
+getLibName _ (LSubLibName lname) = T.pack $ unUnqualComponentName lname
-extractForeignLib :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> ForeignLib -> ImportComponent
-extractForeignLib package =
+extractForeignLib
+ :: Package
+ -> (Text, Set PackageName)
+ -> Release
+ -> Maybe UnqualComponentName
+ -> [Condition ConfVar]
+ -> ForeignLib
+ -> ImportComponent
+extractForeignLib =
genericComponentExtractor
Component.ForeignLib
(^. #foreignLibName % to unUnqualComponentName % to T.pack)
(^. #foreignLibBuildInfo % #targetBuildDepends)
- package
-extractExecutable :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Executable -> ImportComponent
+extractExecutable
+ :: Package
+ -> (Text, Set PackageName)
+ -> Release
+ -> Maybe UnqualComponentName
+ -> [Condition ConfVar]
+ -> Executable
+ -> ImportComponent
extractExecutable =
genericComponentExtractor
Component.Executable
(^. #exeName % to unUnqualComponentName % to T.pack)
(^. #buildInfo % #targetBuildDepends)
-extractTestSuite :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> TestSuite -> ImportComponent
+extractTestSuite
+ :: Package
+ -> (Text, Set PackageName)
+ -> Release
+ -> Maybe UnqualComponentName
+ -> [Condition ConfVar]
+ -> TestSuite
+ -> ImportComponent
extractTestSuite =
genericComponentExtractor
Component.TestSuite
(^. #testName % to unUnqualComponentName % to T.pack)
(^. #testBuildInfo % #targetBuildDepends)
-extractBenchmark :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Benchmark -> ImportComponent
+extractBenchmark
+ :: Package
+ -> (Text, Set PackageName)
+ -> Release
+ -> Maybe UnqualComponentName
+ -> [Condition ConfVar]
+ -> Benchmark
+ -> ImportComponent
extractBenchmark =
genericComponentExtractor
Component.Benchmark
@@ -417,17 +478,18 @@ extractBenchmark =
-- | Traverses the provided 'CondTree' and applies the given 'ComponentExtractor'
-- to every node, returning a list of 'ImportComponent'
extractCondTree
- :: (Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent)
+ :: (Package -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent)
-> Package
+ -> (Text, Set PackageName)
-> Release
-> Maybe UnqualComponentName
-> CondTree ConfVar [Dependency] component
-> [ImportComponent]
-extractCondTree extractor package release defaultComponentName = go []
+extractCondTree extractor package repository release defaultComponentName = go []
where
go cond tree =
- let treeComponent = extractor package release defaultComponentName cond $ tree.condTreeData
- treeSubComponents = (tree.condTreeComponents) >>= extractBranch
+ let treeComponent = extractor package repository release defaultComponentName cond tree.condTreeData
+ treeSubComponents = tree.condTreeComponents >>= extractBranch
in treeComponent : treeSubComponents
extractBranch CondBranch{condBranchCondition, condBranchIfTrue, condBranchIfFalse} =
let condIfTrueComponents = go [condBranchCondition] condBranchIfTrue
@@ -438,13 +500,14 @@ extractCondTree extractor package release defaultComponentName = go []
-- This function builds upon 'extractCondTree' to make it easier to extract fields such as 'condExecutables', 'condTestSuites' etc.
-- from a 'GenericPackageDescription'
extractCondTrees
- :: (Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent)
+ :: (Package -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent)
-> Package
+ -> (Text, Set PackageName)
-> Release
-> [(UnqualComponentName, CondTree ConfVar [Dependency] component)]
-> [ImportComponent]
-extractCondTrees extractor package release trees =
- trees >>= \case (name, tree) -> extractCondTree extractor package release (Just name) tree
+extractCondTrees extractor package repository release trees =
+ trees >>= \case (name, tree) -> extractCondTree extractor package repository release (Just name) tree
genericComponentExtractor
:: forall component
@@ -455,6 +518,7 @@ genericComponentExtractor
-> (component -> [Dependency])
-- ^ Extract dependencies
-> Package
+ -> (Text, Set PackageName)
-> Release
-> Maybe UnqualComponentName
-> [Condition ConfVar]
@@ -465,6 +529,7 @@ genericComponentExtractor
getName
getDeps
package
+ repository
release
defaultComponentName
condition
@@ -475,13 +540,18 @@ genericComponentExtractor
componentId = deterministicComponentId releaseId canonicalForm
metadata = ComponentMetadata (ComponentCondition <$> condition)
component = PackageComponent{..}
- dependencies = force $ buildDependency package componentId <$> getDeps rawComponent
- in force (component, dependencies)
-
-buildDependency :: Package -> ComponentId -> Cabal.Dependency -> ImportDependency
-buildDependency package packageComponentId (Cabal.Dependency depName versionRange _) =
+ dependencies = buildDependency package repository componentId <$> getDeps rawComponent
+ in (component, dependencies)
+
+buildDependency
+ :: Package
+ -> (Text, Set PackageName)
+ -> ComponentId
+ -> Cabal.Dependency
+ -> ImportDependency
+buildDependency package (repository, repositoryPackages) packageComponentId (Cabal.Dependency depName versionRange libs) =
let name = depName & unPackageName & pack & PackageName
- namespace = chooseNamespace name
+ namespace = chooseNamespace name repository repositoryPackages
packageId = deterministicPackageId namespace name
ownerId = package.ownerId
createdAt = package.createdAt
@@ -495,17 +565,20 @@ buildDependency package packageComponentId (Cabal.Dependency depName versionRang
, packageComponentId
, packageId
, requirement = display . prettyShow $ versionRange
- , metadata = RequirementMetadata{flag = Nothing}
+ , components = fmap (getLibName name) . Vector.fromList $ toList libs
}
- in force $ ImportDependency{package = dependencyPackage, requirement}
+ in ImportDependency{package = dependencyPackage, requirement}
getRepoURL :: PackageName -> [Cabal.SourceRepo] -> Vector Text
getRepoURL _ [] = Vector.empty
-getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty (repo.repoLocation)
+getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty repo.repoLocation
-chooseNamespace :: PackageName -> Namespace
-chooseNamespace name | Set.member name coreLibraries = Namespace "haskell"
-chooseNamespace _ = Namespace "hackage"
+chooseNamespace :: PackageName -> Text -> Set PackageName -> Namespace
+chooseNamespace name repo repositoryPackages =
+ if
+ | name `Set.member` coreLibraries -> Namespace "haskell"
+ | name `Set.member` repositoryPackages -> Namespace repo
+ | otherwise -> Namespace "hackage"
extractTestedWith :: Vector (CompilerFlavor, VersionRange) -> Vector VersionRange
extractTestedWith testedWithVector =
@@ -517,7 +590,7 @@ extractTestedWith testedWithVector =
getVersions :: Vector VersionRange -> Vector Version
getVersions supportedCompilers =
foldMap
- (\version -> Vector.foldMap (\versionRange -> checkVersion version versionRange) supportedCompilers)
+ (\version -> Vector.foldMap (checkVersion version) supportedCompilers)
versionList
checkVersion :: Version -> VersionRange -> Vector Version
diff --git a/src/core/Flora/Import/Package/Bulk.hs b/src/core/Flora/Import/Package/Bulk.hs
index 2bb89e6d..7c3483e1 100644
--- a/src/core/Flora/Import/Package/Bulk.hs
+++ b/src/core/Flora/Import/Package/Bulk.hs
@@ -1,18 +1,28 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -fno-full-laziness #-}
-module Flora.Import.Package.Bulk (importAllFilesInDirectory, importAllFilesInRelativeDirectory, importFromIndex) where
+module Flora.Import.Package.Bulk
+ ( importAllFilesInDirectory
+ , importAllFilesInRelativeDirectory
+ , importFromIndex
+ ) where
+import Codec.Archive.Tar (Entries)
import Codec.Archive.Tar qualified as Tar
+import Codec.Archive.Tar.Entry qualified as Tar
+import Codec.Archive.Tar.Index qualified as Tar
import Codec.Compression.GZip qualified as GZip
-import Control.Monad (join, when, (>=>))
+import Control.Monad (when, (>=>))
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Function ((&))
import Data.List (isSuffixOf)
-import Data.Maybe (fromMaybe, isNothing)
+import Data.Maybe (fromMaybe)
+import Data.Set (Set)
+import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as Text
+import Data.Time (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
import Effectful
import Effectful.Log qualified as Log
@@ -22,16 +32,24 @@ import Effectful.Time (runTime)
import Log (Logger, defaultLogLevel)
import Streamly.Data.Fold qualified as SFold
import Streamly.Prelude qualified as S
+import System.Directory
import System.Directory qualified as System
import System.FilePath
import UnliftIO.Exception (finally)
-import Codec.Archive.Tar.Entry qualified as Tar
-import Data.Time (UTCTime)
import Flora.Environment.Config (PoolConfig (..))
-import Flora.Import.Package (enqueueImportJob, extractPackageDataFromCabal, loadContent, persistImportOutput, withWorkerDbPool)
+import Flora.Import.Package
+ ( enqueueImportJob
+ , extractPackageDataFromCabal
+ , loadContent
+ , persistImportOutput
+ , withWorkerDbPool
+ )
+import Flora.Model.Package
import Flora.Model.Package.Update qualified as Update
-import Flora.Model.PackageIndex (getPackageIndexTimestamp, updatePackageIndexTimestamp)
+import Flora.Model.PackageIndex.Query qualified as Query
+import Flora.Model.PackageIndex.Types
+import Flora.Model.PackageIndex.Update qualified as Update
import Flora.Model.Release.Query qualified as Query
import Flora.Model.Release.Update qualified as Update
import Flora.Model.User
@@ -41,27 +59,41 @@ importAllFilesInRelativeDirectory
:: (Reader PoolConfig :> es, DB :> es, IOE :> es)
=> Logger
-> UserId
- -> Maybe Text
+ -> (Text, Text)
-> FilePath
-> Bool
-> Eff es ()
-importAllFilesInRelativeDirectory appLogger user repository dir directImport = do
+importAllFilesInRelativeDirectory appLogger user (repositoryName, repositoryURL) dir directImport = do
workdir <- (> dir) <$> liftIO System.getCurrentDirectory
- importAllFilesInDirectory appLogger user repository workdir directImport
+ importAllFilesInDirectory appLogger user (repositoryName, repositoryURL) workdir directImport
importFromIndex
:: (Reader PoolConfig :> es, DB :> es, IOE :> es)
=> Logger
-> UserId
- -> Maybe Text
+ -> (Text, Text)
-> FilePath
-> Bool
-> Eff es ()
-importFromIndex appLogger user repository index directImport = do
+importFromIndex appLogger user (repositoryName, repositoryURL) index directImport = do
entries <- Tar.read . GZip.decompress <$> liftIO (BL.readFile index)
- time <- fromMaybe (posixSecondsToUTCTime 0) . join <$> traverse getPackageIndexTimestamp repository
+ let Right repositoryPackages = buildPackageListFromArchive entries
+ mPackageIndex <- Query.getPackageIndexByName repositoryName
+ time <- case mPackageIndex of
+ Nothing -> pure $ posixSecondsToUTCTime 0
+ Just packageIndex ->
+ pure $
+ fromMaybe
+ (posixSecondsToUTCTime 0)
+ packageIndex.timestamp
case Tar.foldlEntries (buildContentStream time) S.nil entries of
- Right stream -> importFromStream appLogger user repository directImport stream
+ Right stream ->
+ importFromStream
+ appLogger
+ user
+ (repositoryName, repositoryURL, repositoryPackages)
+ directImport
+ stream
Left (err, _) ->
Log.runLog "flora-cli" appLogger defaultLogLevel $
Log.logAttention_ $
@@ -81,28 +113,27 @@ importAllFilesInDirectory
:: (Reader PoolConfig :> es, DB :> es, IOE :> es)
=> Logger
-> UserId
- -> Maybe Text
+ -> (Text, Text)
-> FilePath
-> Bool
-> Eff es ()
-importAllFilesInDirectory appLogger user repository dir directImport = do
+importAllFilesInDirectory appLogger user (repositoryName, repositoryURL) dir directImport = do
liftIO $ System.createDirectoryIfMissing True dir
+ packages <- buildPackageListFromDirectory dir
liftIO . putStrLn $ "🔎 Searching cabal files in " <> dir
- importFromStream appLogger user repository directImport $ findAllCabalFilesInDirectory dir
+ importFromStream appLogger user (repositoryName, repositoryURL, packages) directImport $ findAllCabalFilesInDirectory dir
importFromStream
:: (Reader PoolConfig :> es, DB :> es, IOE :> es)
=> Logger
-> UserId
- -> Maybe Text
+ -> (Text, Text, Set PackageName)
-> Bool
-> S.AsyncT IO (String, UTCTime, BS.ByteString)
-> Eff es ()
-importFromStream appLogger user repository directImport stream = do
+importFromStream appLogger user (repositoryName, repositoryURL, repositoryPackages) directImport stream = do
pool <- getPool
poolConfig <- ask @PoolConfig
- -- create a packageindex if it doesn't exist
- maybe (pure ()) createPkgIdx repository
processedPackageCount <-
finally
( withWorkerDbPool $ \wq ->
@@ -113,19 +144,14 @@ importFromStream appLogger user repository directImport stream = do
)
-- We want to refresh db and update latest timestamp even if we fell
-- over at some point
- ( Update.refreshLatestVersions
- >> Update.refreshDependents
- >> maybe (pure ()) updatePkgIdxTimestamp repository
+ ( do
+ Update.refreshLatestVersions
+ Update.refreshDependents
+ timestamp <- Query.getLatestReleaseTime (Just repositoryName)
+ Update.updatePackageIndexByName repositoryName timestamp
)
displayStats processedPackageCount
where
- updatePkgIdxTimestamp repository' =
- Query.getLatestReleaseTime (Just repository')
- >>= updatePackageIndexTimestamp repository'
- createPkgIdx repo = do
- pkgIndexTz <- getPackageIndexTimestamp repo
- when (isNothing pkgIndexTz) $
- updatePackageIndexTimestamp repo Nothing
displayCount =
flip SFold.foldlM' (return 0) $
\previousCount _ ->
@@ -139,10 +165,10 @@ importFromStream appLogger user repository directImport stream = do
. runReader poolConfig
. runDB pool
. runTime
- . Log.runLog "flora-jobs" appLogger defaultLogLevel
+ . Log.runLog "flora-cli" appLogger defaultLogLevel
. ( \(path, timestamp, content) ->
loadContent path content
- >>= ( extractPackageDataFromCabal user repository timestamp
+ >>= ( extractPackageDataFromCabal user (repositoryName, repositoryPackages) timestamp
>=> \importedPackage ->
if directImport
then persistImportOutput wq importedPackage
@@ -167,3 +193,25 @@ findAllCabalFilesInDirectory workdir = S.concatMapM traversePath $ S.fromList [w
timestamp <- System.getModificationTime p
return $ S.fromPure (p, timestamp, content)
_ -> return S.nil
+
+buildPackageListFromArchive :: Entries e -> Either e (Set PackageName)
+buildPackageListFromArchive entries =
+ case Tar.build entries of
+ Left e -> Left e
+ Right tarIndex ->
+ Tar.toList tarIndex
+ & fmap (takeDirectory . takeDirectory . fst)
+ & filter (/= ".")
+ & fmap (PackageName . Text.pack)
+ & Set.fromList
+ & Right
+
+buildPackageListFromDirectory :: IOE :> es => FilePath -> Eff es (Set PackageName)
+buildPackageListFromDirectory dir = do
+ paths <- liftIO $ listDirectory dir
+ paths
+ & fmap takeBaseName
+ & filter (/= ".")
+ & fmap (PackageName . Text.pack)
+ & Set.fromList
+ & pure
diff --git a/src/core/Flora/Model/BlobIndex/Internal.hs b/src/core/Flora/Model/BlobIndex/Internal.hs
new file mode 100644
index 00000000..c4a9a1ed
--- /dev/null
+++ b/src/core/Flora/Model/BlobIndex/Internal.hs
@@ -0,0 +1,174 @@
+{-# LANGUAGE OverloadedLists #-}
+
+module Flora.Model.BlobIndex.Internal
+ ( TarError
+ , Sha256Sum
+ , TarRoot (..)
+ , TarTree (..)
+ , tarballToTree
+ , treeToTarball
+ , hashTree
+ )
+where
+
+import Data.Aeson (ToJSON (..), object)
+import Data.ByteString (StrictByteString)
+import Data.ByteString qualified as BS
+import Data.ByteString.Lazy (LazyByteString)
+import Data.ByteString.UTF8 qualified as BSU
+import Data.List (foldl')
+import Data.Map qualified as M
+import Data.Text qualified as T
+import Data.Text.Display (display)
+import Log ((.=))
+import System.FilePath (dropTrailingPathSeparator, joinPath, splitPath, (>))
+
+import Codec.Archive.Tar qualified as Tar
+import Codec.Archive.Tar.Entry qualified as Tar
+import Crypto.Hash.SHA256 qualified as SHA
+import Distribution.Version (Version)
+
+import Flora.Model.BlobIndex.Types (TarError (..))
+import Flora.Model.BlobStore.API (hashByteString)
+import Flora.Model.BlobStore.Types (Sha256Sum (..))
+import Flora.Model.Package (PackageName)
+
+-- | Structure for representing a tarball directory tree
+data TarTree a
+ = TarDirectory a (M.Map FilePath (TarTree a))
+ | TarFile a StrictByteString
+ deriving (Eq, Show)
+
+ann :: TarTree a -> a
+ann (TarDirectory a _) = a
+ann (TarFile a _) = a
+
+instance ToJSON a => ToJSON (TarTree a) where
+ toJSON (TarDirectory a nodes) =
+ object ["ann" .= a, "nodes" .= nodes]
+ toJSON (TarFile a _content) =
+ object ["ann" .= a]
+
+-- | Root directory structure
+--
+-- We expect everything contained within a directory of "{pname}-{version}"
+-- anything outside of that is a malformed tarball we shouldn't accept
+data TarRoot a = TarRoot a PackageName Version (M.Map FilePath (TarTree a))
+ deriving (Eq, Show)
+
+instance ToJSON a => ToJSON (TarRoot a) where
+ toJSON (TarRoot a pname version tree) =
+ object
+ [ "ann" .= a
+ , "packageName" .= pname
+ , "version" .= version
+ , "tree" .= tree
+ ]
+
+-- | Aux function for finding where to put one node in the tree
+--
+-- Provided the entry has the expected root directory all other directories will
+-- be created if not found
+insertTarContents
+ :: [FilePath] -> TarTree () -> TarRoot () -> Either TarError (TarRoot ())
+insertTarContents dirs content (TarRoot () pname version tree) = case dirs of
+ x : xs -> TarRoot () pname version <$> M.alterF (go xs) x tree
+ _ -> Left TarEmpty
+ where
+ go [] Nothing = Right $ Just content
+ -- Sometimes directories are specified in different orders so we should leave
+ -- them as is, they may have had entries added we don't want to remove
+ go [] (Just t@TarDirectory{}) = Right $ Just t
+ -- Files are also occasionally duplicated so we should overwrite it like a tarball
+ -- extraction would do
+ go [] (Just TarFile{}) = Right $ Just content
+ go (x : xs) Nothing = Just . TarDirectory () <$> M.alterF (go xs) x M.empty
+ go (x : xs) (Just (TarDirectory () nodes)) = Just . TarDirectory () <$> M.alterF (go xs) x nodes
+ go _ (Just TarFile{}) = Left $ TarCouldntInsert $ joinPath dirs
+
+-- First we construct a directory tree from a tarball
+-- This makes it easier to create merkle trees later, and gives us a check for
+-- conflicts
+tarballToTree :: PackageName -> Version -> LazyByteString -> Either TarError (TarRoot ())
+tarballToTree pname version =
+ either (Left . TarFormatError . fst) id
+ . checkAndFold
+ . Tar.read
+ where
+ root = TarRoot () pname version M.empty
+ rootdir = T.unpack $ display pname <> "-" <> display version
+ sanitisedTarPaths = fmap dropTrailingPathSeparator . splitPath . Tar.entryPath
+
+ -- If we start with just the root directory we want to skip over it
+ checkAndFold t@(Tar.Next e es) =
+ Tar.foldlEntries go (Right root) $
+ if sanitisedTarPaths e == [rootdir] then es else t
+ checkAndFold Tar.Done = Right $ Left TarEmpty
+ checkAndFold (Tar.Fail err) = Left (err, Right root)
+
+ -- Insert each tar entry into our tree with a check for anything outside the root dir
+ go (Left err) _ = Left err
+ go (Right acc) entry
+ | head dirs /= rootdir = Left $ TarUnexpectedLayout $ joinPath dirs
+ | otherwise = case Tar.entryContent entry of
+ Tar.NormalFile bs _ ->
+ insertTarContents
+ (drop 1 dirs)
+ (TarFile () $ BS.toStrict bs)
+ acc
+ Tar.Directory ->
+ insertTarContents
+ (drop 1 dirs)
+ (TarDirectory () M.empty)
+ acc
+ u -> Left $ TarUnsupportedEntry u
+ where
+ dirs = sanitisedTarPaths entry
+
+-- Traverse over directory tree and create tarball contents
+treeToTarball :: TarRoot a -> LazyByteString
+treeToTarball (TarRoot _ pname version tree) =
+ Tar.write $
+ Tar.directoryEntry
+ (toTarPath True rootdir)
+ : concatMap
+ (\(fp, node) -> nodeToEntries (rootdir > fp) node)
+ (M.toList tree)
+ where
+ toTarPath b dir =
+ either (\fp -> error $ "Directory path too long: " <> fp) id $
+ Tar.toTarPath b dir
+ rootdir = T.unpack $ display pname <> "-" <> display version
+ nodeToEntries dir = \case
+ TarDirectory _ nodes ->
+ Tar.directoryEntry
+ (toTarPath True dir)
+ : concatMap (\(fp, node) -> nodeToEntries (dir > fp) node) (M.toList nodes)
+ TarFile _ contents ->
+ pure $ Tar.fileEntry (toTarPath False dir) $ BS.fromStrict contents
+
+-- | Tag the tree with the sha256sum hashes
+--
+-- Hashed by contents for files, by listed hashes and filepaths for directories
+hashTree :: TarRoot () -> TarRoot Sha256Sum
+hashTree (TarRoot _ pname version tree) =
+ let tree' = go <$> tree
+ in TarRoot (toHash tree') pname version tree'
+ where
+ go (TarFile _ content) =
+ let hash = hashByteString content
+ in TarFile hash content
+ go (TarDirectory _ nodes) =
+ let nodes' = go <$> nodes
+ in TarDirectory (toHash nodes') nodes'
+ toHash =
+ Sha256Sum
+ . SHA.finalize
+ . foldl' SHA.update SHA.init
+ . concatMap
+ ( \(fp, entry) ->
+ [ bytestring (ann entry)
+ , BSU.fromString fp
+ ]
+ )
+ . M.toList
diff --git a/src/core/Flora/Model/BlobIndex/Query.hs b/src/core/Flora/Model/BlobIndex/Query.hs
new file mode 100644
index 00000000..958b2c89
--- /dev/null
+++ b/src/core/Flora/Model/BlobIndex/Query.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Flora.Model.BlobIndex.Query
+ ( queryTar
+ ) where
+
+import Control.Exception (throw)
+import Data.ByteString.Lazy (LazyByteString)
+import Data.Map qualified as M
+import Data.Text.Display (display)
+import Data.Vector qualified as V
+
+import Database.PostgreSQL.Entity (_orderBy, _selectWhere)
+import Database.PostgreSQL.Entity.DBT (QueryNature (..), query)
+import Database.PostgreSQL.Entity.Types (SortKeyword (..), field)
+import Database.PostgreSQL.Simple (Only (..))
+import Effectful (Eff, type (:>))
+import Effectful.Log (Log)
+import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+import Log qualified
+
+import Distribution.Version (Version)
+import Flora.Model.BlobIndex.Types (BlobRelation (..), BlobStoreQueryError (..))
+import Flora.Model.BlobStore.API (BlobStoreAPI, get)
+import Flora.Model.Package (PackageName)
+
+import Flora.Model.BlobIndex.Internal
+
+-- | Query a package name, version and hash and construct a uncompressed tarball
+-- from the database
+queryTar
+ :: forall es
+ . (Log :> es, DB :> es, BlobStoreAPI :> es)
+ => PackageName
+ -> Version
+ -> Sha256Sum
+ -> Eff es LazyByteString
+queryTar pname version rootHash = do
+ Log.logInfo_ $ "Querying for " <> display rootHash
+ children <- traverse go =<< queryChildren rootHash
+ let tree = TarRoot rootHash pname version . M.fromList $ V.toList children
+ pure $ treeToTarball tree
+ where
+ queryChildren :: Sha256Sum -> Eff es (V.Vector BlobRelation)
+ queryChildren hash =
+ dbtToEff $!
+ query
+ Select
+ ( _selectWhere @BlobRelation [[field| blob_hash |]]
+ -- Ensures we consistently get back the directory structure
+ -- This may not be the same as the hackage tarball!
+ <> _orderBy ([field| blob_dep_path |], ASC)
+ )
+ (Only hash)
+ go :: BlobRelation -> Eff es (FilePath, TarTree Sha256Sum)
+ go BlobRelation{..}
+ | blobDepDirectory =
+ (blobDepPath,)
+ . TarDirectory blobDepHash
+ . M.fromList
+ . V.toList
+ <$> (traverse go =<< queryChildren blobDepHash)
+ go BlobRelation{..} = do
+ mcontent <- get blobDepHash
+ case mcontent of
+ Nothing -> throw $ IncompleteDirectoryTree pname version
+ Just bytes ->
+ pure
+ ( blobDepPath
+ , TarFile blobDepHash bytes
+ )
diff --git a/src/core/Flora/Model/BlobIndex/Types.hs b/src/core/Flora/Model/BlobIndex/Types.hs
new file mode 100644
index 00000000..398424f2
--- /dev/null
+++ b/src/core/Flora/Model/BlobIndex/Types.hs
@@ -0,0 +1,78 @@
+module Flora.Model.BlobIndex.Types where
+
+import Control.DeepSeq (NFData)
+import Control.Exception (Exception)
+import Data.Aeson.Orphans ()
+import Data.Text.Display (Display (..))
+import GHC.Generics (Generic)
+
+import Codec.Archive.Tar qualified as Tar
+import Database.PostgreSQL.Entity.Types (Entity, GenericEntity, TableName)
+import Database.PostgreSQL.Simple.FromRow (FromRow (..))
+import Database.PostgreSQL.Simple.Orphans ()
+import Database.PostgreSQL.Simple.ToRow (ToRow (..))
+import Distribution.Version (Version)
+
+import Flora.Model.BlobStore.Types (Sha256Sum (..))
+import Flora.Model.Package.Types (PackageName)
+
+data TarError
+ = TarFormatError Tar.FormatError
+ | TarUnsupportedEntry Tar.EntryContent
+ | TarUnexpectedLayout FilePath
+ | TarEmpty
+ | TarCouldntInsert FilePath
+ deriving stock (Eq, Show)
+ deriving anyclass (Exception)
+
+data BlobStoreQueryError
+ = IncompleteDirectoryTree PackageName Version
+ deriving stock (Eq, Show)
+ deriving anyclass (Exception)
+
+data BlobStoreInsertError
+ = NoPackage PackageName
+ | NoRelease PackageName Version
+ | BlobStoreTarError PackageName Version TarError
+ deriving stock (Eq, Show)
+ deriving anyclass (Exception)
+
+instance Display BlobStoreInsertError where
+ displayBuilder = \case
+ NoPackage pname ->
+ "Couldn't find package " <> displayBuilder pname
+ NoRelease pname version ->
+ "Couldn't find release "
+ <> displayBuilder pname
+ <> "-"
+ <> displayBuilder version
+ BlobStoreTarError pname version err ->
+ "Tarball issue with release "
+ <> displayBuilder pname
+ <> "-"
+ <> displayBuilder version
+ <> ": "
+ <> displayBuilder (show err)
+
+data BlobRelation = BlobRelation
+ { blobHash :: Sha256Sum
+ , blobDepHash :: Sha256Sum
+ , blobDepPath :: FilePath
+ , blobDepDirectory :: Bool
+ }
+ deriving (Generic, NFData)
+ deriving (FromRow, ToRow)
+ deriving
+ (Entity)
+ via (GenericEntity '[TableName "blob_relations"] BlobRelation)
+
+instance Display BlobRelation where
+ displayBuilder (BlobRelation hash depHash depPath depDirectory) =
+ "BlobRelation "
+ <> displayBuilder hash
+ <> " "
+ <> displayBuilder depHash
+ <> " "
+ <> displayBuilder depPath
+ <> " "
+ <> displayBuilder depDirectory
diff --git a/src/core/Flora/Model/BlobIndex/Update.hs b/src/core/Flora/Model/BlobIndex/Update.hs
new file mode 100644
index 00000000..c73956a7
--- /dev/null
+++ b/src/core/Flora/Model/BlobIndex/Update.hs
@@ -0,0 +1,80 @@
+module Flora.Model.BlobIndex.Update where
+
+import Control.Monad (void, when)
+import Control.Monad.IO.Class (MonadIO)
+import Data.ByteString.Lazy (LazyByteString)
+import Data.Int (Int64)
+import Data.Map qualified as M
+import Data.String (fromString)
+import Data.Text.Display (display)
+import Effectful (Eff, type (:>))
+import Effectful.Log (Log)
+import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+import Effectful.Time (Time)
+import Log qualified
+
+import Database.PostgreSQL.Entity (Entity, _insert)
+import Database.PostgreSQL.Entity.DBT (QueryNature (..), execute)
+import Database.PostgreSQL.Simple (ToRow)
+import Database.PostgreSQL.Simple.Types (Query)
+import Database.PostgreSQL.Transact (DBT)
+
+import Distribution.Version (Version)
+
+import Flora.Model.BlobIndex.Internal
+import Flora.Model.BlobIndex.Types
+import Flora.Model.BlobStore.API
+import Flora.Model.Package.Query qualified as Query
+import Flora.Model.Package.Types
+import Flora.Model.Release.Query qualified as Query
+import Flora.Model.Release.Types (Release (..), ReleaseId (..))
+import Flora.Model.Release.Update qualified as Update
+
+insertTar
+ :: (Log :> es, DB :> es, BlobStoreAPI :> es, Time :> es)
+ => PackageName
+ -> Version
+ -> LazyByteString
+ -> Eff es (Either BlobStoreInsertError Sha256Sum)
+insertTar pname version contents = do
+ mpackage <- Query.getPackageByNamespaceAndName (Namespace "hackage") pname
+ case mpackage of
+ Nothing -> pure . Left $ NoPackage pname
+ Just package -> do
+ mrelease <- Query.getReleaseByVersion (package.packageId) version
+ case mrelease of
+ Nothing -> pure . Left $ NoRelease pname version
+ Just release -> do
+ Update.updateTarballArchiveHash release.releaseId contents
+ case hashTree <$> tarballToTree pname version contents of
+ Left err -> pure . Left $ BlobStoreTarError pname version err
+ Right t@(TarRoot rootHash _ _ _) -> Right rootHash <$ insertTree (release.releaseId) t
+
+insertTree
+ :: (Log :> es, DB :> es, BlobStoreAPI :> es)
+ => ReleaseId
+ -> TarRoot Sha256Sum
+ -> Eff es ()
+insertTree releaseId t@(TarRoot rootHash _ _ tree) = do
+ Log.logTrace "Trying to insert directory tree" t
+ mTarballHash <- Query.getReleaseTarballRootHash releaseId
+ case mTarballHash of
+ Just tarballHash -> Log.logInfo_ $ "Hash already inserted with hash: " <> display tarballHash
+ Nothing -> do
+ Update.updateTarballRootHash releaseId rootHash
+ void $! M.traverseWithKey (insertBlobs rootHash) tree
+ Log.logInfo_ $ "Inserted hash tree with root " <> display rootHash
+ where
+ _onConflictDoNothing :: Query
+ _onConflictDoNothing = fromString "on conflict do nothing"
+
+ insertDoNothing :: forall e m. (ToRow e, Entity e, MonadIO m) => e -> DBT m Int64
+ insertDoNothing = execute Update (_insert @e <> _onConflictDoNothing)
+
+ insertBlobs parentHash dir (TarDirectory childHash nodes) = do
+ res <- dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir True
+ when (res > 0) $! void $ M.traverseWithKey (insertBlobs childHash) nodes
+ void . dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir True
+ insertBlobs parentHash dir (TarFile childHash content) = do
+ put childHash content
+ void . dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir False
diff --git a/src/core/Flora/Model/BlobStore/API.hs b/src/core/Flora/Model/BlobStore/API.hs
new file mode 100644
index 00000000..2e6b7929
--- /dev/null
+++ b/src/core/Flora/Model/BlobStore/API.hs
@@ -0,0 +1,85 @@
+module Flora.Model.BlobStore.API
+ ( -- | Effect
+ BlobStoreAPI
+ , get
+ , put
+ , hashByteString
+ -- | Handlers
+ , runBlobStoreFS
+ , runBlobStorePure
+ )
+where
+
+import Crypto.Hash.SHA256 qualified as SHA
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 qualified as BS
+import Data.Map.Strict qualified as M
+import Data.Text qualified as T
+import Data.Text.Display (display)
+import System.Directory (createDirectoryIfMissing, doesFileExist)
+import System.FilePath ((>))
+
+import Effectful (Dispatch (..), DispatchOf, Eff, Effect, IOE, liftIO, type (:>))
+import Effectful.Dispatch.Dynamic (interpret, reinterpret, send)
+import Effectful.State.Static.Local (evalState, gets, modify)
+
+import Flora.Model.BlobStore.Types
+
+data BlobStoreAPI :: Effect where
+ Get :: Sha256Sum -> BlobStoreAPI m (Maybe ByteString)
+ Put :: Sha256Sum -> ByteString -> BlobStoreAPI m ()
+
+type instance DispatchOf BlobStoreAPI = Dynamic
+
+get :: BlobStoreAPI :> es => Sha256Sum -> Eff es (Maybe ByteString)
+get = send . Get
+
+put :: BlobStoreAPI :> es => Sha256Sum -> ByteString -> Eff es ()
+put hash content = send (Put hash content)
+
+hashByteString :: ByteString -> Sha256Sum
+hashByteString = Sha256Sum . SHA.hash
+
+-- | Run a blob store in a local filepath
+runBlobStoreFS
+ :: forall es a
+ . IOE :> es
+ => FilePath
+ -> Eff (BlobStoreAPI : es) a
+ -> Eff es a
+runBlobStoreFS fp e = do
+ liftIO $ createDirectoryIfMissing True fp
+ interpret (const handler) e
+ where
+ -- To avoid excessive entries in one directory we create directories of first
+ -- two characters
+ doesHashExist :: Sha256Sum -> Eff es (FilePath, Bool)
+ doesHashExist hash = liftIO $ do
+ let hashStr = T.unpack (display hash)
+ dir = fp > take 2 hashStr
+ file = dir > drop 2 hashStr
+ createDirectoryIfMissing True dir
+ exists <- doesFileExist file
+ pure (file, exists)
+
+ -- Need to tie the handler type to the top level type
+ handler :: BlobStoreAPI (Eff localEs) a' -> Eff es a'
+ handler = \case
+ Get hash -> do
+ (file, exists) <- doesHashExist hash
+ if exists
+ then Just <$> liftIO (BS.readFile file)
+ else pure Nothing
+ Put hash content -> do
+ (file, exists) <- doesHashExist hash
+ if exists then pure () else liftIO $ BS.writeFile file content
+
+-- | Nun a pure in memory implementation of the blob store
+--
+-- This should only really be used for testing
+runBlobStorePure :: Eff (BlobStoreAPI : es) a -> Eff es a
+runBlobStorePure = reinterpret (evalState $ M.empty @Sha256Sum @ByteString) $
+ const $
+ \case
+ Get hash -> gets $ M.lookup hash
+ Put hash content -> modify $ M.insert hash content
diff --git a/src/core/Flora/Model/BlobStore/Types.hs b/src/core/Flora/Model/BlobStore/Types.hs
new file mode 100644
index 00000000..45a9cc10
--- /dev/null
+++ b/src/core/Flora/Model/BlobStore/Types.hs
@@ -0,0 +1,38 @@
+module Flora.Model.BlobStore.Types where
+
+import Control.DeepSeq (NFData)
+import Data.Aeson.Types
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Data.Text.Display (Display (..), display)
+import Data.Text.Encoding (decodeUtf8Lenient, encodeUtf8)
+import Data.Text.Lazy.Builder (fromText)
+import GHC.Generics (Generic)
+
+import Data.ByteString.Base16 qualified as B16
+import Database.PostgreSQL.Simple.FromField (FromField (..))
+import Database.PostgreSQL.Simple.ToField (ToField (..))
+
+newtype Sha256Sum = Sha256Sum {bytestring :: ByteString}
+ deriving (Eq, Show, Generic)
+ deriving newtype (Ord, NFData)
+
+instance ToField Sha256Sum where
+ toField = toField . decodeUtf8Lenient . B16.encode . bytestring
+
+instance FromField Sha256Sum where
+ fromField f mbs =
+ Sha256Sum . B16.decodeLenient . encodeUtf8
+ <$> fromField @Text f mbs
+
+instance Display Sha256Sum where
+ displayBuilder = fromText . decodeUtf8Lenient . B16.encode . bytestring
+
+instance ToJSON Sha256Sum where
+ toJSON = String . display
+
+instance FromJSON Sha256Sum where
+ parseJSON (String txt) = pure . Sha256Sum . B16.decodeLenient $ encodeUtf8 txt
+ parseJSON invalid =
+ prependFailure "Parsing Sha256Sum failed" $!
+ typeMismatch "Invalid" invalid
diff --git a/src/core/Flora/Model/Category/Query.hs b/src/core/Flora/Model/Category/Query.hs
index 054fc179..ddd1e1cd 100644
--- a/src/core/Flora/Model/Category/Query.hs
+++ b/src/core/Flora/Model/Category/Query.hs
@@ -35,7 +35,6 @@ getPackagesFromCategorySlug slug =
liftIO $ T.putStrLn $ "Could not find category from slug: \"" <> slug <> "\""
pure Vector.empty
Just Category{categoryId} -> do
- liftIO $ T.putStrLn "Category found!"
dbtToEff $
joinSelectOneByField @Package @PackageCategory
[field| package_id |]
diff --git a/src/core/Flora/Model/Job.hs b/src/core/Flora/Model/Job.hs
index 1d8ce000..a4c586f3 100644
--- a/src/core/Flora/Model/Job.hs
+++ b/src/core/Flora/Model/Job.hs
@@ -42,6 +42,14 @@ data ReadmeJobPayload = ReadmeJobPayload
(ToJSON, FromJSON)
via (CustomJSON '[FieldLabelModifier '[CamelToSnake]] ReadmeJobPayload)
+data TarballJobPayload = TarballJobPayload
+ { package :: PackageName
+ , releaseId :: ReleaseId
+ , version :: IntAesonVersion
+ }
+ deriving stock (Generic)
+ deriving anyclass (ToJSON, FromJSON)
+
data UploadTimeJobPayload = UploadTimeJobPayload
{ packageName :: PackageName
, releaseId :: ReleaseId
@@ -71,9 +79,9 @@ data ImportHackageIndexPayload = ImportHackageIndexPayload
-- these represent the possible odd jobs we can run.
data FloraOddJobs
= FetchReadme ReadmeJobPayload
+ | FetchTarball TarballJobPayload
| FetchUploadTime UploadTimeJobPayload
| FetchChangelog ChangelogJobPayload
- | ImportHackageIndex ImportHackageIndexPayload
| ImportPackage ImportOutput
| FetchPackageDeprecationList
| FetchReleaseDeprecationList PackageName (Vector ReleaseId)
@@ -95,4 +103,6 @@ instance ToJSON LogEvent where
LogJobTimeout job -> toJSON ("timed-out" :: Text, job)
LogPoll -> toJSON ("poll" :: Text)
LogWebUIRequest -> toJSON ("web-ui-request" :: Text)
+ LogKillJobSuccess job -> toJSON ("kill-success" :: Text, job)
+ LogKillJobFailed job -> toJSON ("kill-failed" :: Text, job)
LogText other -> toJSON ("other" :: Text, other)
diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs
index 77c501be..46f9134c 100644
--- a/src/core/Flora/Model/Package/Query.hs
+++ b/src/core/Flora/Model/Package/Query.hs
@@ -1,7 +1,34 @@
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
-module Flora.Model.Package.Query where
+module Flora.Model.Package.Query
+ ( countPackages
+ , countPackagesByName
+ , countPackagesInNamespace
+ , getAllPackageDependents
+ , getAllPackageDependentsWithLatestVersion
+ , getAllPackages
+ , getAllRequirements
+ , getComponent
+ , getNonDeprecatedPackages
+ , getNumberOfPackageDependents
+ , getPackageByNamespaceAndName
+ , getPackageCategories
+ , getPackageDependents
+ , getPackageDependentsByName
+ , getPackageDependentsWithLatestVersion
+ , getPackagesByNamespace
+ , getPackagesFromCategoryWithLatestVersion
+ , getRequirements
+ , listAllPackages
+ , listAllPackagesInNamespace
+ , numberOfPackageRequirementsQuery
+ , searchPackage
+ , unsafeGetComponent
+ , getComponentById
+ , searchPackageByNamespace
+ , getNumberOfPackageRequirements
+ ) where
import Data.Text (Text)
import Data.Text.Display (display)
@@ -81,7 +108,21 @@ getAllPackageDependents
=> Namespace
-> PackageName
-> Eff es (Vector Package)
-getAllPackageDependents namespace packageName = dbtToEff $ query Select packageDependentsQuery (namespace, packageName)
+getAllPackageDependents namespace packageName =
+ dbtToEff $ query Select packageDependentsQuery (namespace, packageName)
+
+getPackageDependentsByName
+ :: DB :> es
+ => Namespace
+ -> PackageName
+ -> Text
+ -> Eff es (Vector Package)
+getPackageDependentsByName namespace packageName searchString =
+ dbtToEff $
+ query
+ Select
+ searchPackageDependentsQuery
+ (namespace, packageName, searchString)
-- | This function gets the first 6 dependents of a package
getPackageDependents :: DB :> es => Namespace -> PackageName -> Eff es (Vector Package)
@@ -89,13 +130,26 @@ getPackageDependents namespace packageName = dbtToEff $ query Select q (namespac
where
q = packageDependentsQuery <> " LIMIT 6"
-getNumberOfPackageDependents :: DB :> es => Namespace -> PackageName -> Eff es Word
-getNumberOfPackageDependents namespace packageName =
- dbtToEff $ do
- (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageDependentsQuery (namespace, packageName)
- case result of
- Just (Only n) -> pure $ fromIntegral n
- Nothing -> pure 0
+getNumberOfPackageDependents
+ :: DB :> es
+ => Namespace
+ -> PackageName
+ -> Maybe Text
+ -> Eff es Word
+getNumberOfPackageDependents namespace packageName mbSearchString = do
+ case mbSearchString of
+ Nothing ->
+ dbtToEff $ do
+ (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageDependentsQuery (namespace, packageName)
+ case result of
+ Just (Only n) -> pure $ fromIntegral n
+ Nothing -> pure 0
+ Just searchString ->
+ dbtToEff $ do
+ (result :: Maybe (Only Int)) <- queryOne Select searchNumberOfPackageDependentsQuery (namespace, packageName, searchString)
+ case result of
+ Just (Only n) -> pure $ fromIntegral n
+ Nothing -> pure 0
numberOfPackageDependentsQuery :: Query
numberOfPackageDependentsQuery =
@@ -108,6 +162,19 @@ numberOfPackageDependentsQuery =
AND dep."name" = ?
|]
+searchNumberOfPackageDependentsQuery :: Query
+searchNumberOfPackageDependentsQuery =
+ [sql|
+ SELECT DISTINCT count(p."package_id")
+ FROM "packages" AS p
+ INNER JOIN "dependents" AS dep
+ ON p."package_id" = dep."dependent_id"
+ WHERE dep."namespace" = ?
+ AND dep."name" = ?
+ AND ? <% p."name"
+ |]
+
+-- | Fetch the dependents of a package.
packageDependentsQuery :: Query
packageDependentsQuery =
[sql|
@@ -126,16 +193,27 @@ packageDependentsQuery =
AND dep."name" = ?
|]
+searchPackageDependentsQuery :: Query
+searchPackageDependentsQuery =
+ packageDependentsQuery <> " AND ? <% p.name"
+
getAllPackageDependentsWithLatestVersion
:: DB :> es
=> Namespace
-> PackageName
-> (Word, Word)
+ -> Maybe Text
-> Eff es (Vector DependencyInfo)
-getAllPackageDependentsWithLatestVersion namespace packageName (offset, limit) =
- dbtToEff $ query Select q (namespace, packageName, offset, limit)
- where
- q = packageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?"
+getAllPackageDependentsWithLatestVersion namespace packageName (offset, limit) mSearchString =
+ case mSearchString of
+ Nothing ->
+ dbtToEff $ query Select q (namespace, packageName, offset, limit)
+ where
+ q = packageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?"
+ Just searchString ->
+ dbtToEff $ query Select q (namespace, packageName, searchString, offset, limit)
+ where
+ q = searchPackageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?"
getPackageDependentsWithLatestVersion
:: (DB :> es, Log :> es, Time :> es)
@@ -157,21 +235,60 @@ getPackageDependentsWithLatestVersion namespace packageName = do
packageDependentsWithLatestVersionQuery :: Query
packageDependentsWithLatestVersionQuery =
[sql|
- SELECT DISTINCT p."namespace"
- , p."name"
- , ''
- , max(r."version")
- , r.synopsis as "synopsis"
- , r.license as "license"
- FROM "packages" AS p
- INNER JOIN "dependents" AS dep
- ON p."package_id" = dep."dependent_id"
- INNER JOIN "releases" AS r
- ON r."package_id" = p."package_id"
- WHERE dep."namespace" = ?
- AND dep."name" = ?
- GROUP BY (p.namespace, p.name, synopsis, license)
- ORDER BY p.namespace DESC
+WITH dependents AS (
+ SELECT row_number() OVER (
+ PARTITION BY p.name
+ ORDER BY r.version DESC) AS rank
+ , p.namespace
+ , p.name
+ , r.version
+ , r.synopsis
+ , r.license
+ FROM packages AS p
+ INNER JOIN dependents AS dep ON p.package_id = dep.dependent_id
+ INNER JOIN releases AS r ON r.package_id = p.package_id
+ WHERE dep.namespace = ?
+ AND dep.name = ?
+)
+
+SELECT d.namespace
+ , d.name
+ , ''
+ , (ARRAY[]::text[])
+ , d.version
+ , d.synopsis
+ , d.license
+FROM dependents AS d
+WHERE rank = 1
+ |]
+
+searchPackageDependentsWithLatestVersionQuery :: Query
+searchPackageDependentsWithLatestVersionQuery =
+ [sql|
+WITH dependents AS (
+ SELECT row_number() OVER (
+ PARTITION BY p.name
+ ORDER BY r.version DESC) AS rank
+ , p.namespace
+ , p.name
+ , r.version
+ , r.synopsis
+ , r.license
+ FROM packages AS p
+ INNER JOIN dependents AS dep ON p.package_id = dep.dependent_id
+ INNER JOIN releases AS r ON r.package_id = p.package_id
+ WHERE dep.namespace = ? AND dep.name = ? AND ? <% p."name"
+)
+
+SELECT d.namespace
+ , d.name
+ , ''
+ , (ARRAY[]::text[])
+ , d.version
+ , d.synopsis
+ , d.license
+FROM dependents AS d
+WHERE rank = 1
|]
getComponentById :: DB :> es => ComponentId -> Eff es (Maybe PackageComponent)
@@ -223,7 +340,7 @@ getAllRequirementsQuery :: Query
getAllRequirementsQuery =
[sql|
with requirements as (
- select distinct p1.component_type, p1.component_name, p0.namespace, p0.name, r0.requirement
+ select distinct p1.component_type, p1.component_name, p0.namespace, p0.name, r0.requirement, r0.components
from requirements as r0
inner join packages as p0 on p0.package_id = r0.package_id
inner join package_components as p1 on p1.package_component_id = r0.package_component_id
@@ -235,6 +352,7 @@ getAllRequirementsQuery =
, req.namespace
, req.name
, req.requirement
+ , req.components
, r3.version as "dependency_latest_version"
, r3.synopsis as "dependency_latest_synopsis"
, r3.license as "dependency_latest_license"
@@ -242,7 +360,7 @@ getAllRequirementsQuery =
inner join packages as p2 on p2.namespace = req.namespace and p2.name = req.name
inner join releases as r3 on r3.package_id = p2.package_id
where r3.version = (select max(version) from releases where package_id = p2.package_id)
- group by req.component_type, req.component_name, req.namespace, req.name, req.requirement, r3.version, r3.synopsis, r3.license
+ group by req.component_type, req.component_name, req.namespace, req.name, req.requirement, req.components, r3.version, r3.synopsis, r3.license
order by req.component_type, req.component_name desc
|]
@@ -250,7 +368,8 @@ getAllRequirementsQuery =
getRequirementsQuery :: Query
getRequirementsQuery =
[sql|
- select distinct dependency.namespace, dependency.name, req.requirement from requirements as req
+ select distinct dependency.namespace, dependency.name, req.requirement
+ from requirements as req
inner join packages as dependency on dependency.package_id = req.package_id
inner join package_components as pc ON pc.package_component_id = req.package_component_id
and (pc.component_type = 'library')
@@ -345,6 +464,40 @@ searchPackage (offset, limit) searchString =
|]
(searchString, searchString, offset, limit)
+searchPackageByNamespace
+ :: DB :> es
+ => (Word, Word)
+ -> Namespace
+ -> Text
+ -> Eff es (Vector PackageInfo)
+searchPackageByNamespace (offset, limit) namespace searchString =
+ dbtToEff $
+ query
+ Select
+ [sql|
+ SELECT lv."namespace"
+ , lv."name"
+ , lv."synopsis"
+ , lv."version"
+ , lv."license"
+ , word_similarity(lv.name, ?) as rating
+ FROM latest_versions as lv
+ WHERE
+ ? <% lv."name"
+ AND lv."namespace" = ?
+ GROUP BY
+ lv."namespace"
+ , lv."name"
+ , lv."synopsis"
+ , lv."version"
+ , lv."license"
+ ORDER BY rating desc, count(lv."namespace") desc, lv.name asc
+ OFFSET ?
+ LIMIT ?
+ ;
+ |]
+ (searchString, searchString, namespace, offset, limit)
+
-- | Returns a summary of packages
listAllPackages
:: DB :> es
diff --git a/src/core/Flora/Model/Package/Types.hs b/src/core/Flora/Model/Package/Types.hs
index 8edacbbf..86f7b95e 100644
--- a/src/core/Flora/Model/Package/Types.hs
+++ b/src/core/Flora/Model/Package/Types.hs
@@ -11,6 +11,7 @@ import Data.ByteString (ByteString)
import Data.ByteString.Lazy (fromStrict)
import Data.Maybe (fromJust, fromMaybe)
import Data.OpenApi (Schema (..), ToParamSchema (..), ToSchema (..), genericDeclareNamedSchema)
+import Data.String (IsString (..))
import Data.Text (Text, isPrefixOf, unpack)
import Data.Text qualified as Text
import Data.Text.Display
@@ -79,12 +80,21 @@ instance FromHttpApiData PackageName where
Nothing -> Left "Could not parse package name"
Just a -> Right a
+extractPackageNameText :: PackageName -> Text
+extractPackageNameText (PackageName text) = text
+
parsePackageName :: Text -> Maybe PackageName
parsePackageName txt =
- if matches "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt
+ if matches "^[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$" txt
then Just $ PackageName txt
else Nothing
+instance IsString PackageName where
+ fromString =
+ fromMaybe (error "Bad package name")
+ . parsePackageName
+ . Text.pack
+
instance ToSchema PackageName where
declareNamedSchema proxy =
genericDeclareNamedSchema openApiSchemaOptions proxy
@@ -99,7 +109,7 @@ packageNameSchema :: Schema
packageNameSchema =
mempty
& #description
- ?~ "Name of a package\n It corresponds to the regular expression: `[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*`"
+ ?~ "Name of a package\n It corresponds to the regular expression: `^[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$`"
newtype Namespace = Namespace Text
deriving stock (Show, Generic)
@@ -138,10 +148,14 @@ instance FromHttpApiData Namespace where
parseNamespace :: Text -> Maybe Namespace
parseNamespace txt =
- if matches "@[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt
+ if matches "^@[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$" txt
then Just $ Namespace txt
else Nothing
+extractNamespaceText :: Namespace -> Text
+extractNamespaceText (Namespace text) =
+ fromMaybe text (Text.stripPrefix "@" text)
+
instance ToSchema Namespace where
declareNamedSchema proxy =
genericDeclareNamedSchema openApiSchemaOptions proxy
diff --git a/src/core/Flora/Model/Package/Update.hs b/src/core/Flora/Model/Package/Update.hs
index 7e012c84..9b1cb591 100644
--- a/src/core/Flora/Model/Package/Update.hs
+++ b/src/core/Flora/Model/Package/Update.hs
@@ -69,7 +69,7 @@ insertRequirement :: DB :> es => Requirement -> Eff es ()
insertRequirement = dbtToEff . insert @Requirement
upsertRequirement :: DB :> es => Requirement -> Eff es ()
-upsertRequirement req = dbtToEff $ upsert @Requirement req [[field| metadata |], [field| requirement |]]
+upsertRequirement req = dbtToEff $ upsert @Requirement req [[field| components |], [field| requirement |]]
bulkInsertRequirements :: DB :> es => [Requirement] -> Eff es ()
bulkInsertRequirements requirements =
diff --git a/src/core/Flora/Model/PackageIndex/Query.hs b/src/core/Flora/Model/PackageIndex/Query.hs
new file mode 100644
index 00000000..e8d34fd4
--- /dev/null
+++ b/src/core/Flora/Model/PackageIndex/Query.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Flora.Model.PackageIndex.Query where
+
+import Data.Text (Text)
+import Database.PostgreSQL.Entity (selectOneByField)
+import Database.PostgreSQL.Entity.Types
+import Database.PostgreSQL.Simple (Only (..))
+import Effectful
+import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+
+import Flora.Model.PackageIndex.Types
+
+getPackageIndexByName :: DB :> es => Text -> Eff es (Maybe PackageIndex)
+getPackageIndexByName repository =
+ let index = case repository of
+ "haskell" -> "hackage"
+ r -> r
+ in dbtToEff $
+ selectOneByField [field| repository |] (Only index)
diff --git a/src/core/Flora/Model/PackageIndex.hs b/src/core/Flora/Model/PackageIndex/Types.hs
similarity index 50%
rename from src/core/Flora/Model/PackageIndex.hs
rename to src/core/Flora/Model/PackageIndex/Types.hs
index ae3f4d84..c7608717 100644
--- a/src/core/Flora/Model/PackageIndex.hs
+++ b/src/core/Flora/Model/PackageIndex/Types.hs
@@ -1,26 +1,20 @@
-{-# LANGUAGE OverloadedLists #-}
-{-# LANGUAGE QuasiQuotes #-}
-
-module Flora.Model.PackageIndex where
+module Flora.Model.PackageIndex.Types where
import GHC.Generics
import Control.DeepSeq (NFData)
-import Control.Monad (void)
import Data.Text (Text)
import Data.Text.Display
import Data.Time (UTCTime)
import Data.UUID
import Data.UUID.V4 qualified as UUID
-import Database.PostgreSQL.Entity (insert, selectOneByField, update)
import Database.PostgreSQL.Entity.Types
-import Database.PostgreSQL.Simple (Only (..))
import Database.PostgreSQL.Simple.FromField (FromField (..))
import Database.PostgreSQL.Simple.FromRow (FromRow (..))
import Database.PostgreSQL.Simple.ToField (ToField (..))
import Database.PostgreSQL.Simple.ToRow (ToRow (..))
import Effectful
-import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+import Text.Regex.Pcre2
newtype PackageIndexId = PackageIndexId {getPackageIndexId :: UUID}
deriving stock (Generic)
@@ -32,6 +26,8 @@ data PackageIndex = PackageIndex
{ packageIndexId :: PackageIndexId
, repository :: Text
, timestamp :: Maybe UTCTime
+ , url :: Text
+ , description :: Text
}
deriving stock (Eq, Show, Generic)
deriving anyclass (FromRow, ToRow, NFData)
@@ -39,22 +35,11 @@ data PackageIndex = PackageIndex
(Entity)
via (GenericEntity '[TableName "package_indexes"] PackageIndex)
-mkPackageIndex :: IOE :> es => Text -> Maybe UTCTime -> Eff es PackageIndex
-mkPackageIndex repository timestamp = do
+mkPackageIndex :: IOE :> es => Text -> Text -> Text -> Maybe UTCTime -> Eff es PackageIndex
+mkPackageIndex repository url description timestamp = do
packageIndexId <- PackageIndexId <$> liftIO UUID.nextRandom
pure $ PackageIndex{..}
-getPackageIndexTimestamp :: DB :> es => Text -> Eff es (Maybe UTCTime)
-getPackageIndexTimestamp repository = do
- res :: Maybe PackageIndex <- dbtToEff $ selectOneByField [field| repository |] (Only repository)
- pure $ res >>= timestamp
-
-updatePackageIndexTimestamp :: (IOE :> es, DB :> es) => Text -> Maybe UTCTime -> Eff es ()
-updatePackageIndexTimestamp repository timestamp = do
- packageIndex <- mkPackageIndex repository timestamp
- void $
- dbtToEff $
- selectOneByField @PackageIndex [field| repository |] (Only repository)
- >>= maybe
- (insert @PackageIndex packageIndex)
- (\pkgIx -> update @PackageIndex pkgIx{timestamp})
+parseRepository :: Text -> Bool
+parseRepository txt =
+ matches "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt
diff --git a/src/core/Flora/Model/PackageIndex/Update.hs b/src/core/Flora/Model/PackageIndex/Update.hs
new file mode 100644
index 00000000..5b8ef702
--- /dev/null
+++ b/src/core/Flora/Model/PackageIndex/Update.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module Flora.Model.PackageIndex.Update
+ ( updatePackageIndexByName
+ , createPackageIndex
+ ) where
+
+import Control.Monad (void)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import Database.PostgreSQL.Entity (insert, updateFieldsBy)
+import Database.PostgreSQL.Entity.Types
+import Database.PostgreSQL.Simple (Only (..))
+import Effectful
+import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+
+import Flora.Model.PackageIndex.Types
+ ( PackageIndex (..)
+ , mkPackageIndex
+ )
+
+updatePackageIndexByName :: DB :> es => Text -> Maybe UTCTime -> Eff es ()
+updatePackageIndexByName repositoryName newTimestamp = do
+ void $
+ dbtToEff $
+ updateFieldsBy @PackageIndex
+ [[field| timestamp |]]
+ ([field| repository |], repositoryName)
+ (Only newTimestamp)
+
+createPackageIndex :: (IOE :> es, DB :> es) => Text -> Text -> Text -> Maybe UTCTime -> Eff es ()
+createPackageIndex repositoryName url description timestamp = do
+ packageIndex <- mkPackageIndex repositoryName url description timestamp
+ void $ dbtToEff $ insert @PackageIndex packageIndex
diff --git a/src/core/Flora/Model/Release/Query.hs b/src/core/Flora/Model/Release/Query.hs
index 0f4dd4b9..1f78778a 100644
--- a/src/core/Flora/Model/Release/Query.hs
+++ b/src/core/Flora/Model/Release/Query.hs
@@ -3,20 +3,23 @@
module Flora.Model.Release.Query
( getReleases
+ , getReleaseTarballRootHash
+ , getReleaseTarballArchive
, getReleaseByVersion
- , getPackageReleases
- , getPackageReleasesWithoutReadme
- , getPackageReleasesWithoutChangelog
- , getPackageReleasesWithoutUploadTimestamp
+ , getHackagePackageReleasesWithoutReadme
+ , getHackagePackageReleasesWithoutChangelog
+ , getHackagePackageReleasesWithoutUploadTimestamp
+ , getHackagePackageReleasesWithoutTarball
, getAllReleases
, getLatestReleaseTime
, getNumberOfReleases
, getReleaseComponents
- , getPackagesWithoutReleaseDeprecationInformation
+ , getHackagePackagesWithoutReleaseDeprecationInformation
, getVersionFromManyReleaseIds
)
where
+import Control.Monad (join)
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Vector (Vector)
@@ -31,7 +34,11 @@ import Distribution.Version (Version)
import Effectful
import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+import Data.ByteString (fromStrict)
+import Data.ByteString.Lazy (LazyByteString)
import Distribution.Orphans.Version ()
+import Flora.Model.BlobStore.API (BlobStoreAPI, get)
+import Flora.Model.BlobStore.Types
import Flora.Model.Component.Types
import Flora.Model.Package.Types
import Flora.Model.Release.Types
@@ -54,6 +61,21 @@ getLatestReleaseTime repo =
q = [sql| select max(r0.uploaded_at) from releases as r0 where r0.repository = ? |]
q' = [sql| select max(uploaded_at) from releases |]
+getReleaseTarballRootHash :: DB :> es => ReleaseId -> Eff es (Maybe Sha256Sum)
+getReleaseTarballRootHash releaseId = dbtToEff $ do
+ mRelease <- selectOneByField @Release [field| release_id |] (Only releaseId)
+ case mRelease of
+ Just release -> pure $ tarballRootHash release
+ Nothing -> error $ "Internal error: searched for releaseId that doesn't exist: " <> show releaseId
+
+getReleaseTarballArchive :: (BlobStoreAPI :> es, DB :> es) => ReleaseId -> Eff es (Maybe LazyByteString)
+getReleaseTarballArchive releaseId = do
+ mRelease <- dbtToEff $ selectOneByField @Release [field| release_id |] (Only releaseId)
+ case mRelease of
+ Nothing -> error $ "Internal error: searched for releaseId that doesn't exist: " <> show releaseId
+ Just release -> do
+ fmap fromStrict . join <$> traverse get release.tarballArchiveHash
+
getAllReleases :: DB :> es => PackageId -> Eff es (Vector Release)
getAllReleases pid =
dbtToEff $ do
@@ -76,8 +98,10 @@ getVersionFromManyReleaseIds releaseIds = do
where r0.release_id in ?
|]
-getPackageReleases :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName))
-getPackageReleases =
+getHackagePackageReleasesWithoutReadme
+ :: DB :> es
+ => Eff es (Vector (ReleaseId, Version, PackageName))
+getHackagePackageReleasesWithoutReadme =
dbtToEff $
query Select querySpec ()
where
@@ -88,29 +112,34 @@ getPackageReleases =
from releases as r
join packages as p
on p.package_id = r.package_id
+ where r.readme_status = 'not-imported'
+ and p.namespace = 'hackage'
+ or p.namespace = 'haskell'
|]
-getPackageReleasesWithoutReadme
+getHackagePackageReleasesWithoutUploadTimestamp
:: DB :> es
=> Eff es (Vector (ReleaseId, Version, PackageName))
-getPackageReleasesWithoutReadme =
+getHackagePackageReleasesWithoutUploadTimestamp =
dbtToEff $
query Select querySpec ()
where
querySpec :: Query
querySpec =
[sql|
- select r.release_id, r.version, p."name"
+ select r."release_id", r."version", p."name"
from releases as r
join packages as p
- on p.package_id = r.package_id
- where r.readme_status = 'not-imported'
+ on p."package_id" = r."package_id"
+ where r."uploaded_at" is null
+ and p."namespace" = 'hackage'
+ or p."namespace" = 'haskell'
|]
-getPackageReleasesWithoutUploadTimestamp
+getHackagePackageReleasesWithoutChangelog
:: DB :> es
=> Eff es (Vector (ReleaseId, Version, PackageName))
-getPackageReleasesWithoutUploadTimestamp =
+getHackagePackageReleasesWithoutChangelog =
dbtToEff $
query Select querySpec ()
where
@@ -121,30 +150,30 @@ getPackageReleasesWithoutUploadTimestamp =
from releases as r
join packages as p
on p.package_id = r.package_id
- where r.uploaded_at is null
+ where r.changelog_status = 'not-imported'
+ and p.namespace = 'hackage'
+ or p.namespace = 'haskell'
|]
-getPackageReleasesWithoutChangelog
+getHackagePackageReleasesWithoutTarball
:: DB :> es
=> Eff es (Vector (ReleaseId, Version, PackageName))
-getPackageReleasesWithoutChangelog =
- dbtToEff $
- query Select querySpec ()
+getHackagePackageReleasesWithoutTarball =
+ dbtToEff $! query Select querySpec ()
where
- querySpec :: Query
querySpec =
[sql|
- select r.release_id, r.version, p."name"
+ select r.release_id, r.version, p.name
from releases as r
join packages as p
on p.package_id = r.package_id
- where r.changelog_status = 'not-imported'
+ where r.tarball_root_hash is null
|]
-getPackagesWithoutReleaseDeprecationInformation
+getHackagePackagesWithoutReleaseDeprecationInformation
:: DB :> es
=> Eff es (Vector (PackageName, Vector ReleaseId))
-getPackagesWithoutReleaseDeprecationInformation =
+getHackagePackagesWithoutReleaseDeprecationInformation =
dbtToEff $ query_ Select q
where
q =
@@ -153,6 +182,8 @@ getPackagesWithoutReleaseDeprecationInformation =
from releases as r0
join packages as p1 on r0.package_id = p1.package_id
where r0.deprecated is null
+ and p1.namespace = 'hackage'
+ or p1.namespace = 'haskell'
group by p1.name;
|]
diff --git a/src/core/Flora/Model/Release/Types.hs b/src/core/Flora/Model/Release/Types.hs
index 06f0a48d..09f27096 100644
--- a/src/core/Flora/Model/Release/Types.hs
+++ b/src/core/Flora/Model/Release/Types.hs
@@ -42,6 +42,7 @@ import Deriving.Aeson
import Distribution.Orphans ()
import Distribution.Orphans.CompilerFlavor ()
import Distribution.Orphans.PackageFlag ()
+import Flora.Model.BlobStore.Types
import Flora.Model.Package
newtype ReleaseId = ReleaseId {getReleaseId :: UUID}
@@ -61,7 +62,7 @@ instance ToJSON TextHtml where
toJSON (MkTextHtml a) = String $ Text.toStrict $ Lucid.renderText a
instance FromJSON TextHtml where
- parseJSON = withText "TextHtml" (\text -> pure $ MkTextHtml $ Lucid.toHtmlRaw @Text text)
+ parseJSON = withText "TextHtml" (pure . MkTextHtml . Lucid.toHtmlRaw @Text)
instance NFData TextHtml where
rnf a = seq a ()
@@ -88,6 +89,8 @@ data Release = Release
, readmeStatus :: ImportStatus
, changelog :: Maybe TextHtml
, changelogStatus :: ImportStatus
+ , tarballRootHash :: Maybe Sha256Sum
+ , tarballArchiveHash :: Maybe Sha256Sum
, license :: SPDX.License
, sourceRepos :: Vector Text
, homepage :: Maybe Text
@@ -100,6 +103,7 @@ data Release = Release
, testedWith :: Vector Version
, deprecated :: Maybe Bool
, repository :: Maybe Text
+ , revisedAt :: Maybe UTCTime
}
deriving stock (Eq, Show, Generic)
deriving anyclass (FromRow, ToRow, NFData)
@@ -108,7 +112,7 @@ data Release = Release
via (GenericEntity '[TableName "releases"] Release)
instance Ord Release where
- compare x y = compare (x.version) (y.version)
+ compare x y = compare x.version y.version
newtype ReleaseFlags = ReleaseFlags (Vector PackageFlag)
deriving stock (Eq, Ord, Show, Generic)
diff --git a/src/core/Flora/Model/Release/Update.hs b/src/core/Flora/Model/Release/Update.hs
index fbb17b89..343dc033 100644
--- a/src/core/Flora/Model/Release/Update.hs
+++ b/src/core/Flora/Model/Release/Update.hs
@@ -4,6 +4,7 @@
module Flora.Model.Release.Update where
import Control.Monad (void)
+import Data.Text.Display (display)
import Database.PostgreSQL.Entity
import Database.PostgreSQL.Entity.DBT (QueryNature (Update), execute, executeMany)
import Database.PostgreSQL.Entity.Types (field)
@@ -12,10 +13,15 @@ import Database.PostgreSQL.Simple.SqlQQ (sql)
import Effectful
import Effectful.PostgreSQL.Transact.Effect
+import Crypto.Hash.SHA256 qualified as SHA
+import Data.ByteString (toStrict)
+import Data.ByteString.Lazy (LazyByteString)
import Data.Function ((&))
import Data.Time (UTCTime)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
+import Flora.Model.BlobStore.API (BlobStoreAPI, put)
+import Flora.Model.BlobStore.Types
import Flora.Model.Release.Types
insertRelease :: DB :> es => Release -> Eff es ()
@@ -47,6 +53,15 @@ updateUploadTime releaseId timestamp =
([field| release_id |], releaseId)
(Only (Just timestamp))
+updateRevisionTime :: DB :> es => ReleaseId -> UTCTime -> Eff es ()
+updateRevisionTime releaseId timestamp =
+ dbtToEff $
+ void $
+ updateFieldsBy @Release
+ [[field| revised_at |]]
+ ([field| release_id |], releaseId)
+ (Only (Just timestamp))
+
updateChangelog :: DB :> es => ReleaseId -> Maybe TextHtml -> ImportStatus -> Eff es ()
updateChangelog releaseId changelogBody status =
dbtToEff $
@@ -58,6 +73,30 @@ updateChangelog releaseId changelogBody status =
([field| release_id |], releaseId)
(changelogBody, status)
+updateTarballRootHash :: DB :> es => ReleaseId -> Sha256Sum -> Eff es ()
+updateTarballRootHash releaseId hash =
+ dbtToEff $
+ void $
+ updateFieldsBy @Release
+ [[field| tarball_root_hash |]]
+ ([field| release_id |], releaseId)
+ (Only $ Just $ display hash)
+
+updateTarballArchiveHash
+ :: (BlobStoreAPI :> es, DB :> es)
+ => ReleaseId
+ -> LazyByteString
+ -> Eff es ()
+updateTarballArchiveHash releaseId (toStrict -> content) = do
+ let hash = Sha256Sum . SHA.hash $ content
+ put hash content
+ dbtToEff $
+ void $
+ updateFieldsBy @Release
+ [[field| tarball_archive_hash |]]
+ ([field| release_id |], releaseId)
+ (Only . Just $ display hash)
+
setReleasesDeprecationMarker :: DB :> es => Vector (Bool, ReleaseId) -> Eff es ()
setReleasesDeprecationMarker releaseVersions =
dbtToEff $ void $ executeMany Update q (releaseVersions & Vector.toList)
@@ -67,5 +106,5 @@ setReleasesDeprecationMarker releaseVersions =
UPDATE releases as r0
SET deprecated = upd.x
FROM (VALUES (?,?)) as upd(x,y)
- WHERE r0.release_id = (upd.y :: uuid)
+ WHERE r0.release_id = (upd.y :: uuid)
|]
diff --git a/src/core/Flora/Model/Requirement.hs b/src/core/Flora/Model/Requirement.hs
index 5bbada50..8eea8ddd 100644
--- a/src/core/Flora/Model/Requirement.hs
+++ b/src/core/Flora/Model/Requirement.hs
@@ -1,7 +1,6 @@
module Flora.Model.Requirement where
import Crypto.Hash.MD5 qualified as MD5
-import Data.Data
import Data.Foldable (foldl')
import Data.Map.Strict qualified as Map
import Data.Text (Text)
@@ -13,11 +12,9 @@ import Database.PostgreSQL.Entity.Types (GenericEntity, TableName)
import Database.PostgreSQL.Simple (ToRow)
import Database.PostgreSQL.Simple.FromField
( FromField
- , fromField
- , fromJSONField
)
import Database.PostgreSQL.Simple.FromRow (FromRow (..))
-import Database.PostgreSQL.Simple.ToField (ToField, toField, toJSONField)
+import Database.PostgreSQL.Simple.ToField (ToField)
import Control.DeepSeq
import Data.ByteString.Lazy (fromStrict)
@@ -50,8 +47,8 @@ data Requirement = Requirement
-- ^ Package that is being depended on
, requirement :: Text
-- ^ The human-readable version range expression of this requirement
- , metadata :: RequirementMetadata
- -- ^ Additional metadata, like flags
+ , components :: Vector Text
+ -- ^ Components that are depended on
}
deriving stock (Eq, Show, Generic)
deriving anyclass (FromRow, ToRow, NFData, FromJSON, ToJSON)
@@ -62,26 +59,12 @@ data Requirement = Requirement
(Display)
via ShowInstance Requirement
-data RequirementMetadata = RequirementMetadata
- { flag :: Maybe Text
- }
- deriving stock (Eq, Show, Generic, Typeable)
- deriving anyclass (NFData)
- deriving
- (ToJSON, FromJSON)
- via (CustomJSON '[FieldLabelModifier '[CamelToSnake]] RequirementMetadata)
-
-instance FromField RequirementMetadata where
- fromField = fromJSONField
-
-instance ToField RequirementMetadata where
- toField = toJSONField
-
-- | This datatype holds information about the latest version of a dependency
data DependencyInfo = DependencyInfo
{ namespace :: Namespace
, name :: PackageName
, requirement :: Text
+ , components :: Vector Text
, latestVersion :: Version
, latestSynopsis :: Text
, latestLicense :: SPDX.License
@@ -96,6 +79,7 @@ data ComponentDependency' = ComponentDependency'
, namespace :: Namespace
, name :: PackageName
, requirement :: Text
+ , components :: Vector Text
, latestVersion :: Version
, latestSynopsis :: Text
, latestLicense :: SPDX.License
diff --git a/src/core/Flora/Model/User.hs b/src/core/Flora/Model/User.hs
index 122ba828..3c54d7d4 100644
--- a/src/core/Flora/Model/User.hs
+++ b/src/core/Flora/Model/User.hs
@@ -1,5 +1,5 @@
{-# LANGUAGE UndecidableInstances #-}
-{-# OPTIONS_GHC -fno-warn-orphans -Wno-redundant-constraints #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
module Flora.Model.User
( UserId (..)
@@ -34,12 +34,14 @@ import Database.PostgreSQL.Entity
import Database.PostgreSQL.Entity.Types
import Database.PostgreSQL.Simple.FromField (FromField (..), fromJSONField)
import Database.PostgreSQL.Simple.FromRow (FromRow (..))
+import Database.PostgreSQL.Simple.Orphans ()
import Database.PostgreSQL.Simple.ToField (ToField (..), toJSONField)
import Database.PostgreSQL.Simple.ToRow (ToRow (..))
import Effectful
import Effectful.Time qualified as Time
import GHC.Generics
import GHC.TypeLits (ErrorMessage (..), TypeError)
+import Sel.HMAC.SHA256 qualified as HMAC
import Web.HttpApiData (FromHttpApiData, ToHttpApiData)
newtype UserId = UserId {getUserId :: UUID}
@@ -60,6 +62,8 @@ data User = User
, userFlags :: UserFlags
, createdAt :: UTCTime
, updatedAt :: UTCTime
+ , totpKey :: Maybe HMAC.AuthenticationKey
+ , totpEnabled :: Bool
}
deriving stock (Eq, Generic, Show)
deriving anyclass (FromRow, ToRow, NFData)
@@ -125,6 +129,8 @@ mkUser UserCreationForm{username, email, password} = do
let updatedAt = timestamp
let displayName = ""
let userFlags = UserFlags{isAdmin = False, canLogin = True}
+ let totpKey = Nothing
+ let totpEnabled = False
pure User{..}
mkAdmin :: IOE :> es => AdminCreationForm -> Eff es User
@@ -134,7 +140,9 @@ mkAdmin AdminCreationForm{username, email, password} = do
let createdAt = timestamp
let updatedAt = timestamp
let displayName = ""
- let userFlags = UserFlags{isAdmin = True, canLogin = False}
+ let userFlags = UserFlags{isAdmin = True, canLogin = True}
+ let totpKey = Nothing
+ let totpEnabled = False
pure User{..}
hashPassword :: IOE :> es => Password -> Eff es (PasswordHash Argon2)
diff --git a/src/core/Flora/Model/User/Update.hs b/src/core/Flora/Model/User/Update.hs
index a09ccb80..45d45c0e 100644
--- a/src/core/Flora/Model/User/Update.hs
+++ b/src/core/Flora/Model/User/Update.hs
@@ -1,40 +1,60 @@
{-# LANGUAGE QuasiQuotes #-}
-module Flora.Model.User.Update where
+module Flora.Model.User.Update
+ ( addAdmin
+ , lockAccount
+ , unlockAccount
+ , insertUser
+ , deleteUser
+ , setupTOTP
+ , confirmTOTP
+ , unSetTOTP
+ ) where
import Control.Monad
import Database.PostgreSQL.Entity (delete, insert)
import Database.PostgreSQL.Entity.DBT (QueryNature (Update), execute)
import Database.PostgreSQL.Simple (Only (Only))
import Database.PostgreSQL.Simple.SqlQQ (sql)
-
import Effectful (Eff, IOE, type (:>))
import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff)
+import Effectful.Time (Time)
+import Effectful.Time qualified as Time
+import Sel.HMAC.SHA256 qualified as HMAC
+
import Flora.Model.User
-addAdmin :: (DB :> es, IOE :> es) => AdminCreationForm -> Eff es User
+addAdmin :: (DB :> es, Time :> es, IOE :> es) => AdminCreationForm -> Eff es User
addAdmin form = do
adminUser <- mkAdmin form
insertUser adminUser
- unlockAccount (adminUser.userId)
+ unlockAccount adminUser.userId
pure adminUser
-lockAccount :: DB :> es => UserId -> Eff es ()
-lockAccount userId = dbtToEff $ void $ execute Update q (Only userId)
+lockAccount :: (DB :> es, Time :> es) => UserId -> Eff es ()
+lockAccount userId = do
+ ts <- Time.currentTime
+ dbtToEff $ void $ execute Update q (ts, userId)
where
q =
[sql|
- update users as u set user_flags = jsonb_set(user_flags, '{can_login}', 'false', false)
+ update users as u
+ set user_flags = jsonb_set(user_flags, '{can_login}', 'false', false),
+ updated_at = ?
where u.user_id = ?;
|]
-unlockAccount :: DB :> es => UserId -> Eff es ()
-unlockAccount userId = dbtToEff $ void $ execute Update q (Only userId)
+unlockAccount :: (DB :> es, Time :> es) => UserId -> Eff es ()
+unlockAccount userId = do
+ ts <- Time.currentTime
+ dbtToEff $ void $ execute Update q (ts, userId)
where
q =
[sql|
- update users as u set user_flags = jsonb_set(user_flags, '{can_login}', 'true', false)
- where u.user_id = ?;
+ update users as u
+ set user_flags = jsonb_set(user_flags, '{can_login}', 'true', false),
+ updated_at = ?
+ where u.user_id = ?
|]
insertUser :: DB :> es => User -> Eff es ()
@@ -42,3 +62,53 @@ insertUser user = dbtToEff $ insert @User user
deleteUser :: DB :> es => UserId -> Eff es ()
deleteUser userId = dbtToEff $ delete @User (Only userId)
+
+setupTOTP
+ :: (DB :> es, Time :> es)
+ => UserId
+ -> HMAC.AuthenticationKey
+ -> Eff es ()
+setupTOTP userId key = do
+ ts <- Time.currentTime
+ dbtToEff $ void $ execute Update q (key, ts, userId)
+ where
+ q =
+ [sql|
+ update users as u
+ set totp_key = ?,
+ updated_at = ?
+ where u.user_id = ?;
+ |]
+
+confirmTOTP
+ :: (DB :> es, Time :> es)
+ => UserId
+ -> Eff es ()
+confirmTOTP userId = do
+ ts <- Time.currentTime
+ dbtToEff $ void $ execute Update q (ts, userId)
+ where
+ q =
+ [sql|
+ update users as u
+ set totp_enabled = true,
+ updated_at = ?
+ where u.user_id = ?;
+ |]
+
+unSetTOTP
+ :: (DB :> es, Time :> es)
+ => UserId
+ -> Eff es ()
+unSetTOTP userId = do
+ ts <- Time.currentTime
+ dbtToEff $ void $ execute Update q (ts, userId)
+ where
+ q =
+ [sql|
+ update users as u
+ set totp_enabled = false,
+ totp_key = Null,
+ updated_at = ?
+ where u.user_id = ?;
+ |]
diff --git a/src/core/Flora/QRCode.hs b/src/core/Flora/QRCode.hs
new file mode 100644
index 00000000..9849fc20
--- /dev/null
+++ b/src/core/Flora/QRCode.hs
@@ -0,0 +1,20 @@
+module Flora.QRCode where
+
+import Codec.Picture
+import Codec.QRCode
+import Codec.QRCode.JuicyPixels
+import Data.ByteString (StrictByteString)
+import Data.ByteString.Base64
+import Data.ByteString.Lazy qualified as BSL
+import Data.Function ((&))
+import Data.Text (Text)
+
+generateQRCode :: Text -> StrictByteString
+generateQRCode uri =
+ case encodeText (defaultQRCodeOptions L) Iso8859_1OrUtf8WithoutECI uri of
+ Nothing -> error $ "QR code can't be encoded for text " <> show uri
+ Just qrImage ->
+ toImage 4 20 qrImage
+ & encodePng
+ & BSL.toStrict
+ & encodeBase64'
diff --git a/src/core/Flora/Search.hs b/src/core/Flora/Search.hs
index c59ec650..ccfc637a 100644
--- a/src/core/Flora/Search.hs
+++ b/src/core/Flora/Search.hs
@@ -1,8 +1,14 @@
+{-# LANGUAGE ViewPatterns #-}
+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
+
+{-# HLINT ignore "Use <$>" #-}
+
module Flora.Search where
import Data.Aeson
import Data.List qualified as List
import Data.Text (Text)
+import Data.Text qualified as Text
import Data.Text.Display (Display (..))
import Data.Text.Lazy.Builder qualified as Builder
import Data.Vector (Vector)
@@ -16,19 +22,49 @@ import Log qualified
import Flora.Logging
import Flora.Model.Package (Namespace (..), PackageInfo (..), PackageName (..), formatPackage)
import Flora.Model.Package.Query qualified as Query
+import Flora.Model.Package.Types qualified as Package
+import Flora.Model.Requirement
data SearchAction
= ListAllPackages
| ListAllPackagesInNamespace Namespace
| SearchPackages Text
- | DependentsOf Namespace PackageName
+ | DependentsOf
+ Namespace
+ -- ^ Namespace
+ PackageName
+ -- ^ Package
+ (Maybe Text)
+ -- ^ Search within the package
+ | SearchInNamespace Namespace PackageName
deriving (Eq, Ord, Show)
instance Display SearchAction where
displayBuilder ListAllPackages = "Packages"
displayBuilder (ListAllPackagesInNamespace namespace) = "Packages in " <> displayBuilder namespace
displayBuilder (SearchPackages title) = "\"" <> Builder.fromText title <> "\""
- displayBuilder (DependentsOf namespace packageName) = "Dependents of " <> displayBuilder namespace <> "/" <> displayBuilder packageName
+ displayBuilder (DependentsOf namespace packageName mbSearchString) =
+ "Dependents of "
+ <> displayBuilder namespace
+ <> "/"
+ <> displayBuilder packageName
+ <> foldMap (\searchString -> " \"" <> Builder.fromText searchString <> "\"") mbSearchString
+ displayBuilder (SearchInNamespace namespace packageName) =
+ "Package " <> displayBuilder namespace <> "/" <> displayBuilder packageName
+
+search
+ :: (DB :> es, Log :> es, Time :> es)
+ => (Word, Word)
+ -> Text
+ -> Eff es (Word, Vector PackageInfo)
+search pagination queryString =
+ case parseSearchQuery queryString of
+ Just (ListAllPackagesInNamespace namespace) -> listAllPackagesInNamespace pagination namespace
+ Just ListAllPackages -> listAllPackages pagination
+ Just (SearchInNamespace namespace (PackageName packageName)) -> searchPackageByNamespaceAndName pagination namespace packageName
+ Just (DependentsOf namespace packageName mSearchString) -> searchDependents pagination namespace packageName mSearchString
+ Just (SearchPackages _) -> searchPackageByName pagination queryString
+ Nothing -> searchPackageByName pagination queryString
searchPackageByName
:: (DB :> es, Log :> es, Time :> es)
@@ -57,13 +93,68 @@ searchPackageByName (offset, limit) queryString = do
count <- Query.countPackagesByName queryString
pure (count, results)
+searchPackageByNamespaceAndName
+ :: (DB :> es, Log :> es, Time :> es)
+ => (Word, Word)
+ -> Namespace
+ -> Text
+ -> Eff es (Word, Vector PackageInfo)
+searchPackageByNamespaceAndName (offset, limit) namespace queryString = do
+ (results, duration) <- timeAction $ Query.searchPackageByNamespace (offset, limit) namespace queryString
+
+ Log.logInfo "search-results" $
+ object
+ [ "search_string" .= queryString
+ , "duration" .= duration
+ , "results_count" .= Vector.length results
+ , "results"
+ .= List.map
+ ( \PackageInfo{name, rating} ->
+ object
+ [ "package" .= formatPackage namespace name
+ , "score" .= rating
+ ]
+ )
+ (Vector.toList results)
+ ]
+
+ count <- Query.countPackagesByName queryString
+ pure (count, results)
+
+searchDependents
+ :: DB :> es
+ => (Word, Word)
+ -> Namespace
+ -> PackageName
+ -> Maybe Text
+ -> Eff es (Word, Vector PackageInfo)
+searchDependents pagination namespace packageName mSearchString = do
+ results <-
+ Query.getAllPackageDependentsWithLatestVersion
+ namespace
+ packageName
+ pagination
+ mSearchString
+ totalDependents <- Query.getNumberOfPackageDependents namespace packageName mSearchString
+ pure (totalDependents, fmap dependencyInfoToPackageInfo results)
+
+dependencyInfoToPackageInfo :: DependencyInfo -> PackageInfo
+dependencyInfoToPackageInfo dep =
+ PackageInfo
+ dep.namespace
+ dep.name
+ dep.latestSynopsis
+ dep.latestVersion
+ dep.latestLicense
+ Nothing
+
listAllPackagesInNamespace
:: (DB :> es, Time :> es, Log :> es)
- => Namespace
- -> (Word, Word)
+ => (Word, Word)
+ -> Namespace
-> Eff es (Word, Vector PackageInfo)
-listAllPackagesInNamespace namespace (offset, limit) = do
- (results, duration) <- timeAction $ Query.listAllPackagesInNamespace (offset, limit) namespace
+listAllPackagesInNamespace pagination namespace = do
+ (results, duration) <- timeAction $ Query.listAllPackagesInNamespace pagination namespace
Log.logInfo "packages-in-namespace" $
object
@@ -93,3 +184,48 @@ listAllPackages (offset, limit) = do
results <- Query.listAllPackages (offset, limit)
count <- Query.countPackages
pure (count, results)
+
+-- | Search modifiers:
+--
+-- * depends:<@namespace>/
+-- * in:<@namespace>/
+-- * in:<@namespace>
+parseSearchQuery :: Text -> Maybe SearchAction
+parseSearchQuery = \case
+ (Text.stripPrefix "depends:" -> Just rest) ->
+ case parseNamespacedPackageSearch rest of
+ Just (namespace, packageName) ->
+ Just $ DependentsOf namespace packageName Nothing
+ Nothing -> Just $ SearchPackages rest
+ (Text.stripPrefix "in:" -> Just rest) ->
+ case parseNamespaceAndPackageSearch rest of
+ (Just namespace, Just packageName) ->
+ Just $ SearchInNamespace namespace packageName
+ (Just namespace, Nothing) ->
+ Just $ ListAllPackagesInNamespace namespace
+ _ -> Just $ SearchPackages rest
+ e -> Just $ SearchPackages e
+
+-- Determine if the string is
+-- <@namespace>/
+parseNamespacedPackageSearch :: Text -> Maybe (Namespace, PackageName)
+parseNamespacedPackageSearch text =
+ case Text.breakOn "/" text of
+ (_, "") -> Nothing
+ (Package.parseNamespace -> Just namespace, Text.stripPrefix "/" -> Just potentialPackageName) ->
+ case Package.parsePackageName potentialPackageName of
+ Just packageName -> Just (namespace, packageName)
+ Nothing -> Nothing
+ (_, _) -> Nothing
+
+parseNamespaceAndPackageSearch :: Text -> (Maybe Namespace, Maybe PackageName)
+parseNamespaceAndPackageSearch text =
+ case Text.breakOn " " text of
+ (Package.parseNamespace -> Just namespace, "") ->
+ (Just namespace, Nothing)
+ (_, "") -> (Nothing, Nothing)
+ (Package.parseNamespace -> Just namespace, Text.stripPrefix " " -> Just potentialPackageName) ->
+ case Package.parsePackageName potentialPackageName of
+ Just packageName -> (Just namespace, Just packageName)
+ Nothing -> (Just namespace, Nothing)
+ (_, _) -> (Nothing, Nothing)
diff --git a/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs
new file mode 100644
index 00000000..c09a7d39
--- /dev/null
+++ b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs
@@ -0,0 +1,27 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Database.PostgreSQL.Simple.Orphans where
+
+import Control.DeepSeq
+import Data.ByteString (ByteString)
+import Data.Text qualified as Text
+import Database.PostgreSQL.Simple.FromField (FromField (..), ResultError (..), returnError)
+import Database.PostgreSQL.Simple.ToField
+import Database.PostgreSQL.Simple.Types (Binary (..))
+import Sel.HMAC.SHA256 qualified as HMAC
+
+deriving newtype instance NFData (Binary ByteString)
+
+instance FromField HMAC.AuthenticationKey where
+ fromField f Nothing = returnError UnexpectedNull f ""
+ fromField f (Just bs) =
+ case HMAC.authenticationKeyFromHexByteString bs of
+ Left err -> returnError ConversionFailed f (Text.unpack err)
+ Right value -> pure value
+
+instance ToField HMAC.AuthenticationKey where
+ toField = Escape . HMAC.unsafeAuthenticationKeyToHexByteString
+
+instance NFData HMAC.AuthenticationKey where
+ rnf :: HMAC.AuthenticationKey -> ()
+ rnf a = seq a ()
diff --git a/src/datatypes/Distribution/Orphans/Version.hs b/src/datatypes/Distribution/Orphans/Version.hs
index f5577b57..7d470ae6 100644
--- a/src/datatypes/Distribution/Orphans/Version.hs
+++ b/src/datatypes/Distribution/Orphans/Version.hs
@@ -5,6 +5,8 @@ module Distribution.Orphans.Version where
import Data.Aeson
import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
+import Data.Either (fromRight)
+import Data.String (IsString (..))
import Data.Text qualified as Text
import Data.Text.Display
import Data.Text.Lazy.Builder qualified as Builder
@@ -18,6 +20,9 @@ import Distribution.Types.Version qualified as Cabal
import Distribution.Version (VersionRange)
import Servant
+instance IsString Version where
+ fromString = fromRight (error "Bad version") . eitherParsec
+
instance ToJSON Version where
toJSON = Aeson.String . display . Pretty.prettyShow
diff --git a/src/datatypes/Servant/API/ContentTypes/GZip.hs b/src/datatypes/Servant/API/ContentTypes/GZip.hs
new file mode 100644
index 00000000..3ece22a4
--- /dev/null
+++ b/src/datatypes/Servant/API/ContentTypes/GZip.hs
@@ -0,0 +1,20 @@
+module Servant.API.ContentTypes.GZip where
+
+import Codec.Compression.GZip qualified as GZip
+import Data.ByteString.Lazy (ByteString)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Typeable (Typeable)
+import Network.HTTP.Media.MediaType ((//))
+import Servant.API.ContentTypes (Accept (..), MimeRender (..), MimeUnrender (..))
+
+data GZipped
+ deriving (Typeable)
+
+instance Accept GZipped where
+ contentTypes _ = "application" // "x-gzip" :| []
+
+instance MimeUnrender GZipped ByteString where
+ mimeUnrender _ = Right . GZip.decompress
+
+instance MimeRender GZipped ByteString where
+ mimeRender _ = GZip.compress
diff --git a/src/jobs-worker/FloraJobs/Runner.hs b/src/jobs-worker/FloraJobs/Runner.hs
index ac7481e6..ae74206f 100644
--- a/src/jobs-worker/FloraJobs/Runner.hs
+++ b/src/jobs-worker/FloraJobs/Runner.hs
@@ -1,6 +1,5 @@
module FloraJobs.Runner where
-import Control.Concurrent (forkIO)
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
@@ -8,18 +7,22 @@ import Data.Aeson (Result (..), fromJSON, toJSON)
import Data.Function
import Data.Set qualified as Set
import Data.Text.Display
-import Data.Text.Lazy.Encoding qualified as TL
import Data.Vector (Vector)
import Data.Vector qualified as Vector
-import Effectful.PostgreSQL.Transact.Effect
+import Effectful (Eff, IOE, type (:>))
+import Effectful.Log
+import Effectful.PostgreSQL.Transact.Effect (DB)
+import Effectful.Reader.Static (Reader)
+import Effectful.Time (Time)
import Log
import Network.HTTP.Types (gone410, notFound404, statusCode)
import OddJobs.Job (Job (..))
import Servant.Client (ClientError (..))
import Servant.Client.Core (ResponseF (..))
-import System.Process.Typed qualified as System
import Flora.Import.Package (coreLibraries, persistImportOutput, withWorkerDbPool)
+import Flora.Model.BlobIndex.Update qualified as Update
+import Flora.Model.BlobStore.API
import Flora.Model.Job
import Flora.Model.Package.Types
import Flora.Model.Package.Update qualified as Update
@@ -27,39 +30,19 @@ import Flora.Model.Release.Query qualified as Query
import Flora.Model.Release.Types
import Flora.Model.Release.Update qualified as Update
import FloraJobs.Render (renderMarkdown)
-import FloraJobs.Scheduler
-import FloraJobs.ThirdParties.Hackage.API (HackagePreferredVersions (..), VersionedPackage (..))
+import FloraJobs.ThirdParties.Hackage.API (HackagePackageInfo (..), HackagePreferredVersions (..), VersionedPackage (..))
import FloraJobs.ThirdParties.Hackage.Client qualified as Hackage
import FloraJobs.Types
-fetchNewIndex :: JobsRunner ()
-fetchNewIndex =
- localDomain "index-import" $ do
- logInfo_ "Fetching new index"
- System.runProcess_ "cabal update"
- System.runProcess_ "cp ~/.cabal/packages/hackage.haskell.org/01-index.tar 01-index/"
- System.runProcess_ "cd 01-index && tar -xf 01-index.tar"
- System.runProcess_ "make import-from-hackage"
- logInfo_ "New index processed"
- releases <- Query.getPackageReleasesWithoutReadme
- pool <- getPool
- liftIO $
- forkIO $
- forM_
- releases
- ( \(releaseId, version, packagename) -> scheduleReadmeJob pool releaseId packagename version
- )
- liftIO $ void $ scheduleIndexImportJob pool
-
runner :: Job -> JobsRunner ()
runner job = localDomain "job-runner" $
case fromJSON (jobPayload job) of
Error str -> logMessage LogAttention "decode error" (toJSON str)
Success val -> case val of
FetchReadme x -> makeReadme x
+ FetchTarball x -> fetchTarball x
FetchUploadTime x -> fetchUploadTime x
FetchChangelog x -> fetchChangeLog x
- ImportHackageIndex _ -> fetchNewIndex
ImportPackage x ->
withWorkerDbPool $ \wq ->
persistImportOutput wq x
@@ -105,28 +88,60 @@ makeReadme pay@ReadmeJobPayload{..} =
let readmeBody = renderMarkdown ("README" <> show mpPackage) bodyText
Update.updateReadme mpReleaseId (Just $ MkTextHtml readmeBody) Imported
+fetchTarball
+ :: ( IOE :> es
+ , Time :> es
+ , DB :> es
+ , Reader JobsRunnerEnv :> es
+ , Log :> es
+ , BlobStoreAPI :> es
+ )
+ => TarballJobPayload
+ -> Eff es ()
+fetchTarball pay@TarballJobPayload{..} = do
+ localDomain "fetch-tarball" $ do
+ mArchive <- Query.getReleaseTarballArchive releaseId
+ content <- case mArchive of
+ Just bs -> pure bs
+ Nothing -> do
+ logInfo "Fetching tarball" pay
+ let payload = VersionedPackage{..}
+ result <- Hackage.request $ Hackage.getPackageTarball payload
+ case result of
+ Right bs -> pure bs
+ Left e@(FailureResponse _ response) -> do
+ logAttention "Could not fetch tarball from hackage" $
+ object
+ [ "package" .= display payload.package
+ , "status_code" .= statusCode response.responseStatusCode
+ ]
+ throw e
+ Left e -> throw e
+ mhash <- Update.insertTar package (unIntAesonVersion version) content
+ case mhash of
+ Right hash ->
+ logInfo
+ ("Inserted tarball for " <> display package)
+ (object ["release_id" .= releaseId, "root_hash" .= hash])
+ Left err -> do
+ logAttention_ $ "Failed to insert tarball for " <> display package
+ throw err
+
fetchUploadTime :: UploadTimeJobPayload -> JobsRunner ()
fetchUploadTime payload@UploadTimeJobPayload{packageName, packageVersion, releaseId} =
localDomain "fetch-upload-time" $ do
logInfo "Fetching upload time" payload
let requestPayload = VersionedPackage packageName packageVersion
- result <- Hackage.request $ Hackage.getPackageUploadTime requestPayload
- case result of
- Right timestamp -> do
- logInfo_ $ "Got a timestamp for " <> display packageName
- Update.updateUploadTime releaseId timestamp
- Left e@(FailureResponse _ response)
- -- If the upload time simply doesn't exist, we skip it by marking the job as successful.
- | response.responseStatusCode == notFound404 -> pure ()
- | response.responseStatusCode == gone410 -> pure ()
- | otherwise -> do
- logAttention "Timestamp retrieval failed" $
- object
- [ "status" .= statusCode (response.responseStatusCode)
- , "body" .= TL.decodeUtf8 (response.responseBody)
- ]
- throw e
- Left e -> throw e
+ packageInfo <- liftIO $ Hackage.getPackageInfo requestPayload
+ if packageInfo.metadataRevision == 0
+ then do
+ Log.logInfo_ "No revision, using the upload time"
+ Update.updateUploadTime releaseId packageInfo.uploadedAt
+ else do
+ Log.logInfo_ "Found a revision, querying the original package info"
+ originalPackageInfo <- liftIO $ Hackage.getPackageWithRevision requestPayload 0
+ Update.updateRevisionTime releaseId packageInfo.uploadedAt
+ Update.updateUploadTime releaseId originalPackageInfo.uploadedAt
-- | This job fetches the deprecation list and inserts the appropriate metadata in the packages
fetchPackageDeprecationList :: JobsRunner ()
@@ -144,7 +159,7 @@ fetchPackageDeprecationList = do
Left e@(FailureResponse _ response) -> do
logAttention "Could not fetch package deprecation list from Hackage" $
object
- [ "status_code" .= statusCode (response.responseStatusCode)
+ [ "status_code" .= statusCode response.responseStatusCode
]
throw e
Left e -> throw e
@@ -175,7 +190,7 @@ fetchReleaseDeprecationList packageName releases = do
logAttention "Could not fetch release deprecation list from Hackage" $
object
[ "package" .= display packageName
- , "status_code" .= statusCode (response.responseStatusCode)
+ , "status_code" .= statusCode response.responseStatusCode
]
throw e
Left e -> throw e
diff --git a/src/jobs-worker/FloraJobs/Scheduler.hs b/src/jobs-worker/FloraJobs/Scheduler.hs
index d5d8fa5a..f14fb9a0 100644
--- a/src/jobs-worker/FloraJobs/Scheduler.hs
+++ b/src/jobs-worker/FloraJobs/Scheduler.hs
@@ -3,9 +3,9 @@
-- | Represents the various jobs that can be run
module FloraJobs.Scheduler
( scheduleReadmeJob
+ , scheduleTarballJob
, scheduleChangelogJob
, scheduleUploadTimeJob
- , scheduleIndexImportJob
, schedulePackageDeprecationListJob
, scheduleReleaseDeprecationListJob
, scheduleRefreshLatestVersions
@@ -18,8 +18,8 @@ module FloraJobs.Scheduler
)
where
+import Data.Aeson (ToJSON)
import Data.Pool
-import Data.Time qualified as Time
import Data.Vector (Vector)
import Database.PostgreSQL.Entity.DBT
import Database.PostgreSQL.Simple (Only (..))
@@ -27,9 +27,8 @@ import Database.PostgreSQL.Simple qualified as PG
import Database.PostgreSQL.Simple.SqlQQ (sql)
import Distribution.Types.Version
import Effectful.PostgreSQL.Transact.Effect
-import Effectful.Time qualified as Time
import Log
-import OddJobs.Job (Job (..), createJob, scheduleJob)
+import OddJobs.Job (Job (..), createJob)
import Flora.Model.Job
import Flora.Model.Package
@@ -47,74 +46,35 @@ scheduleReadmeJob pool rid package version =
(FetchReadme $ ReadmeJobPayload package rid $ MkIntAesonVersion version)
)
+scheduleTarballJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job
+scheduleTarballJob pool rid package version =
+ createJobWithResource pool $ FetchTarball $ TarballJobPayload package rid $ MkIntAesonVersion version
+
scheduleChangelogJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job
scheduleChangelogJob pool rid package version =
- withResource
- pool
- ( \res ->
- createJob
- res
- jobTableName
- (FetchChangelog $ ChangelogJobPayload package rid $ MkIntAesonVersion version)
- )
+ createJobWithResource pool $ FetchChangelog $ ChangelogJobPayload package rid $ MkIntAesonVersion version
scheduleUploadTimeJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job
scheduleUploadTimeJob pool releaseId packageName version =
- withResource
- pool
- ( \res ->
- createJob
- res
- jobTableName
- (FetchUploadTime $ UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version))
- )
-
-scheduleIndexImportJob :: Pool PG.Connection -> IO Job
-scheduleIndexImportJob pool =
- withResource
- pool
- ( \conn -> do
- t <- Time.currentTime
- let runAt = Time.addUTCTime Time.nominalDay t
- scheduleJob
- conn
- jobTableName
- (ImportHackageIndex ImportHackageIndexPayload)
- runAt
- )
+ createJobWithResource pool $
+ FetchUploadTime $
+ UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version)
schedulePackageDeprecationListJob :: Pool PG.Connection -> IO Job
schedulePackageDeprecationListJob pool =
- withResource
- pool
- ( \conn ->
- createJob
- conn
- jobTableName
- FetchPackageDeprecationList
- )
+ createJobWithResource pool FetchPackageDeprecationList
-scheduleReleaseDeprecationListJob :: Pool PG.Connection -> (PackageName, Vector ReleaseId) -> IO Job
+scheduleReleaseDeprecationListJob
+ :: Pool PG.Connection -> (PackageName, Vector ReleaseId) -> IO Job
scheduleReleaseDeprecationListJob pool (package, releaseIds) =
- withResource
- pool
- ( \conn ->
- createJob
- conn
- jobTableName
- (FetchReleaseDeprecationList package releaseIds)
- )
+ createJobWithResource pool (FetchReleaseDeprecationList package releaseIds)
scheduleRefreshLatestVersions :: Pool PG.Connection -> IO Job
-scheduleRefreshLatestVersions pool =
- withResource
- pool
- ( \conn ->
- createJob
- conn
- jobTableName
- RefreshLatestVersions
- )
+scheduleRefreshLatestVersions pool = createJobWithResource pool RefreshLatestVersions
+
+createJobWithResource :: ToJSON p => Pool PG.Connection -> p -> IO Job
+createJobWithResource pool job =
+ withResource pool $ \conn -> createJob conn jobTableName job
checkIfIndexImportJobIsNotRunning :: JobsRunner Bool
checkIfIndexImportJobIsNotRunning = do
diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs
index 378e9eee..ebfc6caf 100644
--- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs
+++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs
@@ -1,6 +1,9 @@
+{-# LANGUAGE TemplateHaskell #-}
+
module FloraJobs.ThirdParties.Hackage.API where
import Data.Aeson
+import Data.Aeson.TH
import Data.Bifunctor qualified as Bifunctor
import Data.ByteString.Lazy as ByteString
import Data.List.NonEmpty
@@ -13,6 +16,7 @@ import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Network.HTTP.Media ((//), (/:))
import Servant.API
+import Servant.API.ContentTypes.GZip
import Servant.API.Generic
import Distribution.Orphans ()
@@ -41,12 +45,17 @@ instance ToHttpApiData VersionedPackage where
toUrlPiece VersionedPackage{package, version} =
display package <> "-" <> display version
+newtype VersionedTarball = VersionedTarball VersionedPackage
+
+instance ToHttpApiData VersionedTarball where
+ toUrlPiece (VersionedTarball vt) = toUrlPiece vt <> ".tar.gz"
+
data HackageAPI' mode = HackageAPI'
{ listUsers :: mode :- "users" :> Get '[JSON] [HackageUserObject]
, withUser :: mode :- "user" :> Capture "username" Text :> NamedRoutes HackageUserAPI
, packages :: mode :- "packages" :> NamedRoutes HackagePackagesAPI
, withPackage :: mode :- "package" :> Capture "versioned_package" VersionedPackage :> NamedRoutes HackagePackageAPI
- , withPackageName :: mode :- "package" :> Capture "pacakgeName" PackageName :> NamedRoutes HackagePackageAPI
+ , withPackageNameOnly :: mode :- "package" :> Capture "packageName" PackageName :> NamedRoutes HackagePackageAPI
}
deriving stock (Generic)
@@ -60,6 +69,9 @@ data HackagePackageAPI mode = HackagePackageAPI
, getUploadTime :: mode :- "upload-time" :> Get '[PlainText] UTCTime
, getChangelog :: mode :- "changelog.txt" :> Get '[PlainerText] Text
, getDeprecatedReleases :: mode :- "preferred.json" :> Get '[JSON] HackagePreferredVersions
+ , getPackageInfo :: mode :- Get '[JSON] HackagePackageInfo
+ , getPackageWithRevision :: mode :- "revision" :> Capture "revision_number" Text :> Get '[JSON] HackagePackageInfo
+ , getTarball :: mode :- Capture "tarball" VersionedTarball :> Get '[GZipped] ByteString
}
deriving stock (Generic)
@@ -90,7 +102,15 @@ data HackagePreferredVersions = HackagePreferredVersions
deriving stock (Eq, Show, Generic)
instance FromJSON HackagePreferredVersions where
- parseJSON = withObject "Hacakge preferred versions" $ \o -> do
+ parseJSON = withObject "Hackage preferred versions" $ \o -> do
deprecatedVersions <- o .:? "deprecated-version" .!= Vector.empty
normalVersions <- o .: "normal-version"
pure HackagePreferredVersions{..}
+
+data HackagePackageInfo = HackagePackageInfo
+ { metadataRevision :: Word
+ , uploadedAt :: UTCTime
+ }
+ deriving stock (Eq, Show)
+
+$(deriveJSON defaultOptions{fieldLabelModifier = camelTo2 '_'} ''HackagePackageInfo)
diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs
index e3f9b12d..fcc24cca 100644
--- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs
+++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs
@@ -4,20 +4,24 @@
module FloraJobs.ThirdParties.Hackage.Client where
import Control.Monad.IO.Class
+import Data.ByteString.Lazy (ByteString)
import Data.Proxy
import Data.Text
import Data.Time (UTCTime)
import Data.Time.Orphans ()
import Data.Vector (Vector)
+import Effectful (Eff, IOE, type (:>))
import Effectful.Reader.Static
+import Network.HTTP.Req (GET (GET), NoReqBody (..))
+import Network.HTTP.Req qualified as Req
import Servant.API ()
-import Servant.Client
+import Servant.Client (BaseUrl (..), Client, ClientError (..), ClientM, Scheme (..), client, mkClientEnv, runClientM, (//), (/:))
import Flora.Model.Package.Types
import FloraJobs.ThirdParties.Hackage.API as API
-import FloraJobs.Types (JobsRunner, JobsRunnerEnv (..))
+import FloraJobs.Types (JobsRunnerEnv (..))
-request :: ClientM a -> JobsRunner (Either ClientError a)
+request :: (IOE :> es, Reader JobsRunnerEnv :> es) => ClientM a -> Eff es (Either ClientError a)
request req = do
JobsRunnerEnv{httpManager} <- ask
let clientEnv =
@@ -35,6 +39,14 @@ listHackageUsers = hackageClient // API.listUsers
getHackageUser :: Text -> ClientM HackageUserDetailsObject
getHackageUser username = hackageClient // API.withUser /: username // API.getUser
+getPackageTarball :: VersionedPackage -> ClientM ByteString
+getPackageTarball versionedPackage =
+ hackageClient
+ // API.withPackage
+ /: versionedPackage
+ // API.getTarball
+ /: VersionedTarball versionedPackage
+
getPackageReadme :: VersionedPackage -> ClientM Text
getPackageReadme versionedPackage =
hackageClient
@@ -65,6 +77,30 @@ getDeprecatedPackages =
getDeprecatedReleasesList :: PackageName -> ClientM HackagePreferredVersions
getDeprecatedReleasesList packageName =
hackageClient
- // API.withPackageName
+ // API.withPackageNameOnly
/: packageName
// getDeprecatedReleases
+
+getPackageInfo :: VersionedPackage -> IO HackagePackageInfo
+getPackageInfo versionedPackage = do
+ Req.runReq Req.defaultHttpConfig $ do
+ response <-
+ Req.req
+ GET
+ (Req.https "hackage.haskell.org" Req./: "package" Req./~ versionedPackage)
+ NoReqBody
+ Req.jsonResponse
+ mempty
+ pure $ Req.responseBody response
+
+getPackageWithRevision :: VersionedPackage -> Word -> IO HackagePackageInfo
+getPackageWithRevision versionedPackage revision = do
+ Req.runReq Req.defaultHttpConfig $ do
+ response <-
+ Req.req
+ GET
+ (Req.https "hackage.haskell.org" Req./: "package" Req./~ versionedPackage Req./: "revision" Req./~ revision)
+ NoReqBody
+ Req.jsonResponse
+ mempty
+ pure $ Req.responseBody response
diff --git a/src/jobs-worker/FloraJobs/Types.hs b/src/jobs-worker/FloraJobs/Types.hs
index b1466179..92af877c 100644
--- a/src/jobs-worker/FloraJobs/Types.hs
+++ b/src/jobs-worker/FloraJobs/Types.hs
@@ -25,8 +25,10 @@ import OddJobs.ConfigBuilder
import OddJobs.Job (Config (..), Job, LogEvent (..), LogLevel (..))
import OddJobs.Types (ConcurrencyControl (..), UIConfig (..))
+import Flora.Environment
import Flora.Environment.Config
import Flora.Logging qualified as Logging
+import Flora.Model.BlobStore.API
import Flora.Model.Job ()
type JobsRunner =
@@ -34,18 +36,23 @@ type JobsRunner =
'[ DB
, Reader PoolConfig
, Reader JobsRunnerEnv
+ , BlobStoreAPI
, Log
, Time
, IOE
]
-runJobRunner :: Pool Connection -> JobsRunnerEnv -> FloraConfig -> Logger -> JobsRunner a -> IO a
-runJobRunner pool runnerEnv cfg logger jobRunner =
+runJobRunner :: Pool Connection -> JobsRunnerEnv -> FloraEnv -> Logger -> JobsRunner a -> IO a
+runJobRunner pool runnerEnv floraEnv logger jobRunner =
runEff
. runTime
. LogEff.runLog "flora-jobs" logger defaultLogLevel
+ . ( case floraEnv.features.blobStoreImpl of
+ Just (BlobStoreFS fp) -> runBlobStoreFS fp
+ _ -> runBlobStorePure
+ )
. runReader runnerEnv
- . runReader cfg.dbConfig
+ . runReader floraEnv.config.dbConfig
. runDB pool
$ jobRunner
@@ -78,18 +85,18 @@ data JobsRunnerEnv = JobsRunnerEnv
makeConfig
:: JobsRunnerEnv
- -> FloraConfig
+ -> FloraEnv
-> Logger
-> Pool PG.Connection
-> (Job -> JobsRunner ())
-> Config
-makeConfig runnerEnv cfg logger pool runnerContinuation =
+makeConfig runnerEnv floraEnv logger pool runnerContinuation =
mkConfig
- (\level event -> structuredLogging cfg logger level event)
+ (\level event -> structuredLogging (floraEnv.config) logger level event)
jobTableName
pool
(MaxConcurrentJobs 100)
- (runJobRunner pool runnerEnv cfg logger . runnerContinuation)
+ (runJobRunner pool runnerEnv floraEnv logger . runnerContinuation)
(\x -> x{cfgDeleteSuccessfulJobs = False, cfgDefaultMaxAttempts = 3})
makeUIConfig :: FloraConfig -> Logger -> Pool PG.Connection -> UIConfig
diff --git a/src/web/FloraWeb/Common/Auth.hs b/src/web/FloraWeb/Common/Auth.hs
index 4637003a..ebb896dd 100644
--- a/src/web/FloraWeb/Common/Auth.hs
+++ b/src/web/FloraWeb/Common/Auth.hs
@@ -1,7 +1,9 @@
module FloraWeb.Common.Auth
( module FloraWeb.Common.Auth.Types
- , FloraAuthContext
- , authHandler
+ , OptionalAuthContext
+ , StrictAuthContext
+ , optionalAuthHandler
+ , strictAuthHandler
)
where
@@ -37,36 +39,56 @@ import FloraWeb.Session
import FloraWeb.Types
import Servant qualified
-type FloraAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session)
+type OptionalAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session)
+type StrictAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session)
-authHandler :: Logger -> FloraEnv -> FloraAuthContext
-authHandler logger floraEnv =
+optionalAuthHandler :: Logger -> FloraEnv -> OptionalAuthContext
+optionalAuthHandler logger floraEnv =
mkAuthHandler
( \request ->
- handler request
- & Logging.runLog (floraEnv.environment) logger
- & DB.runDB (floraEnv.pool)
+ handler False floraEnv request
+ & Logging.runLog floraEnv.environment logger
+ & DB.runDB floraEnv.pool
& runVisitorSession
& effToHandler
)
- where
- handler :: Request -> Eff '[Log, DB, IsVisitor, Error ServerError, IOE] (Headers '[Header "Set-Cookie" SetCookie] Session)
- handler req = do
- let cookies = getCookies req
- mbPersistentSessionId <- handlerToEff $ getSessionId cookies
- mbPersistentSession <- getInTheFuckingSessionShinji mbPersistentSessionId
- mUserInfo <- fetchUser mbPersistentSession
- requestID <- liftIO $ getRequestID req
- (mUser, sessionId) <- do
- case mUserInfo of
- Nothing -> do
+
+strictAuthHandler :: Logger -> FloraEnv -> StrictAuthContext
+strictAuthHandler logger floraEnv =
+ mkAuthHandler
+ ( \request ->
+ handler True floraEnv request
+ & Logging.runLog floraEnv.environment logger
+ & DB.runDB floraEnv.pool
+ & runVisitorSession
+ & effToHandler
+ )
+
+handler
+ :: Bool
+ -> FloraEnv
+ -> Request
+ -> Eff
+ '[Log, DB, IsVisitor, Error ServerError, IOE]
+ (Headers '[Header "Set-Cookie" SetCookie] Session)
+handler mustBeConnected floraEnv req = do
+ let cookies = getCookies req
+ mbPersistentSessionId <- handlerToEff $ getSessionId cookies
+ mbPersistentSession <- getInTheFuckingSessionShinji mbPersistentSessionId
+ mUserInfo <- fetchUser mbPersistentSession
+ requestID <- liftIO $ getRequestID req
+ (mUser, sessionId) <- do
+ case mUserInfo of
+ Nothing ->
+ if mustBeConnected
+ then throwError $ err401{errBody = "Log-in first"}
+ else do
nSessionId <- liftIO newPersistentSessionId
pure (Nothing, nSessionId)
- Just (user, userSession) -> do
- pure (Just user, userSession.persistentSessionId)
- webEnvStore <- liftIO $ newWebEnvStore (WebEnv floraEnv)
- let sessionCookie = craftSessionCookie sessionId False
- pure $ addCookie sessionCookie (Session{..})
+ Just (user, userSession) -> pure (Just user, userSession.persistentSessionId)
+ webEnvStore <- liftIO $ newWebEnvStore (WebEnv floraEnv)
+ let sessionCookie = craftSessionCookie sessionId False
+ pure $ addCookie sessionCookie (Session{..})
getCookies :: Request -> Cookies
getCookies req =
@@ -101,10 +123,13 @@ getInTheFuckingSessionShinji (Just persistentSessionId) = do
Nothing -> pure Nothing
(Just userSession) -> pure (Just userSession)
-fetchUser :: (Error ServerError :> es, DB :> es) => Maybe PersistentSession -> Eff es (Maybe (User, PersistentSession))
+fetchUser
+ :: (Error ServerError :> es, DB :> es)
+ => Maybe PersistentSession
+ -> Eff es (Maybe (User, PersistentSession))
fetchUser Nothing = pure Nothing
fetchUser (Just userSession) = do
- user <- lookupUser (userSession.userId)
+ user <- lookupUser userSession.userId
pure (Just (user, userSession))
lookupUser :: (Error ServerError :> es, DB :> es) => UserId -> Eff es User
@@ -119,8 +144,8 @@ handlerToEff
. Error ServerError :> es
=> Handler a
-> Eff es a
-handlerToEff handler = do
- v <- unsafeEff_ $ Servant.runHandler handler
+handlerToEff handler' = do
+ v <- unsafeEff_ $ Servant.runHandler handler'
either throwError pure v
effToHandler
diff --git a/src/web/FloraWeb/Common/Auth/TwoFactor.hs b/src/web/FloraWeb/Common/Auth/TwoFactor.hs
new file mode 100644
index 00000000..30bcab89
--- /dev/null
+++ b/src/web/FloraWeb/Common/Auth/TwoFactor.hs
@@ -0,0 +1,44 @@
+module FloraWeb.Common.Auth.TwoFactor
+ ( uriFromKey
+ , validateTOTP
+ ) where
+
+import Chronos (Timespan, now, second)
+import Data.ByteString.Base32 qualified as Base32
+import Data.Maybe (fromJust)
+import Data.Text (Text)
+import OTP.Commons
+import OTP.TOTP
+import Sel.HMAC.SHA256 qualified as HMAC
+import Torsor (scale)
+
+period :: Timespan
+period = scale 30 second
+
+sixDigits :: Digits
+sixDigits = fromJust $ mkDigits 6
+
+uriFromKey :: Text -> Text -> HMAC.AuthenticationKey -> Text
+uriFromKey domain email key =
+ let
+ issuer = "Flora (" <> domain <> ")"
+ in
+ totpToURI
+ (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary key)
+ email
+ issuer
+ sixDigits
+ period
+ HMAC_SHA1
+
+validateTOTP :: HMAC.AuthenticationKey -> Text -> IO Bool
+validateTOTP key code = do
+ timestamp <- now
+ pure $
+ totpSHA1Check
+ key
+ (1, 1)
+ timestamp
+ period
+ sixDigits
+ code
diff --git a/src/web/FloraWeb/Common/Auth/Types.hs b/src/web/FloraWeb/Common/Auth/Types.hs
index 79b839be..72c811a6 100644
--- a/src/web/FloraWeb/Common/Auth/Types.hs
+++ b/src/web/FloraWeb/Common/Auth/Types.hs
@@ -16,6 +16,8 @@ import Servant.Server.Experimental.Auth (AuthServerData)
import Web.Cookie (SetCookie)
import Data.Text (Text)
+import Flora.Environment
+import Flora.Model.BlobStore.API
import Flora.Model.PersistentSession
import Flora.Model.User
import FloraWeb.Types
@@ -66,29 +68,24 @@ demoteSession
-> Eff (IsVisitor : es) a
demoteSession = putVisitorTag . runAdminSession
+type BaseEffects =
+ '[ DB
+ , Time
+ , Reader (Headers '[Header "Set-Cookie" SetCookie] Session)
+ , Reader FeatureEnv
+ , BlobStoreAPI
+ , Log
+ , Error ServerError
+ , IOE
+ ]
+
-- | Datatypes used for every route that doesn't *need* an authenticated user
type FloraPage =
- Eff
- '[ IsVisitor
- , DB
- , Time
- , Reader (Headers '[Header "Set-Cookie" SetCookie] Session)
- , Log
- , Error ServerError
- , IOE
- ]
+ Eff (IsVisitor ': BaseEffects)
-- | Datatypes used for routes that *need* an admin
type FloraAdmin =
- Eff
- '[ IsAdmin
- , DB
- , Time
- , Reader (Headers '[Header "Set-Cookie" SetCookie] Session)
- , Log
- , Error ServerError
- , IOE
- ]
+ Eff (IsAdmin ': BaseEffects)
-- | The effect stack for the development websockets
type FloraDevSocket = Eff [Reader (), Log, Error ServerError, IOE]
@@ -97,6 +94,6 @@ type instance
AuthServerData (AuthProtect "optional-cookie-auth") =
(Headers '[Header "Set-Cookie" SetCookie] Session)
--- type instance
--- AuthServerData (AuthProtect "cookie-auth") =
--- (Headers '[Header "Set-Cookie" SetCookie] (Session 'Authenticated))
+type instance
+ AuthServerData (AuthProtect "cookie-auth") =
+ (Headers '[Header "Set-Cookie" SetCookie] Session)
diff --git a/src/web/FloraWeb/Common/Guards.hs b/src/web/FloraWeb/Common/Guards.hs
index 7198135a..2eb3902b 100644
--- a/src/web/FloraWeb/Common/Guards.hs
+++ b/src/web/FloraWeb/Common/Guards.hs
@@ -2,15 +2,28 @@
module FloraWeb.Common.Guards where
+import Data.Text (Text)
import Distribution.Types.Version (Version)
import Effectful
import Effectful.Log (Log)
import Effectful.PostgreSQL.Transact.Effect
import Effectful.Time (Time)
+import FloraWeb.Pages.Templates
+import Log qualified
+import Optics.Core
+import Servant (respond)
+import Servant.API.UVerb
+
import Flora.Model.Package
import Flora.Model.Package.Query qualified as Query
+import Flora.Model.PackageIndex.Query as Query
+import Flora.Model.PackageIndex.Types (PackageIndex)
import Flora.Model.Release.Query qualified as Query
import Flora.Model.Release.Types (Release)
+import FloraWeb.Common.Auth
+import FloraWeb.Pages.Routes.Sessions (CreateSessionResponses)
+import FloraWeb.Pages.Templates.Screens.Sessions qualified as Sessions
+import FloraWeb.Session (getSession)
guardThatPackageExists
:: (DB :> es, Log :> es, Time :> es)
@@ -40,3 +53,31 @@ guardThatReleaseExists packageId version action = do
case result of
Just release -> pure release
Nothing -> action version
+
+guardThatPackageIndexExists
+ :: DB :> es
+ => Namespace
+ -> (Namespace -> Eff es PackageIndex)
+ -- ^ Action to run if the package index does not exist
+ -> Eff es PackageIndex
+guardThatPackageIndexExists namespace action = do
+ result <- Query.getPackageIndexByName (extractNamespaceText namespace)
+ case result of
+ Just packageIndex -> pure packageIndex
+ Nothing -> action namespace
+
+guardThatUserHasProvidedTOTP
+ :: Maybe Text
+ -> (Text -> FloraPage (Union CreateSessionResponses))
+ -> FloraPage (Union CreateSessionResponses)
+guardThatUserHasProvidedTOTP mTOTP action = do
+ case mTOTP of
+ Just totp -> action totp
+ Nothing -> do
+ session <- getSession
+ Log.logInfo_ "User did not provide a TOTP code"
+ templateDefaults <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateDefaults
+ & (#flashError ?~ mkError "Must provide an OTP code")
+ respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession
diff --git a/src/web/FloraWeb/Components/Alert.hs b/src/web/FloraWeb/Components/Alert.hs
new file mode 100644
index 00000000..b240a1ca
--- /dev/null
+++ b/src/web/FloraWeb/Components/Alert.hs
@@ -0,0 +1,20 @@
+module FloraWeb.Components.Alert where
+
+import Data.Text (Text)
+import FloraWeb.Components.Icons qualified as Icons
+import FloraWeb.Pages.Templates.Types
+import Lucid
+
+info :: Text -> FloraHTML
+info message =
+ output_ [role_ "status", class_ "alert alert-info"] $ do
+ Icons.information
+ div_ [class_ "alert-message"] $
+ toHtml message
+
+exception :: Text -> FloraHTML
+exception message =
+ output_ [role_ "status", class_ "alert alert-error"] $ do
+ Icons.exception
+ div_ [class_ "alert-message"] $
+ toHtml message
diff --git a/src/web/FloraWeb/Components/Button.hs b/src/web/FloraWeb/Components/Button.hs
new file mode 100644
index 00000000..8f78457c
--- /dev/null
+++ b/src/web/FloraWeb/Components/Button.hs
@@ -0,0 +1,8 @@
+module FloraWeb.Components.Button where
+
+import FloraWeb.Pages.Templates
+import Lucid
+
+button :: FloraHTML -> FloraHTML
+button text =
+ button_ [type_ "submit", class_ "button"] text
diff --git a/src/web/FloraWeb/Components/Icons.hs b/src/web/FloraWeb/Components/Icons.hs
new file mode 100644
index 00000000..35e34cd6
--- /dev/null
+++ b/src/web/FloraWeb/Components/Icons.hs
@@ -0,0 +1,77 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module FloraWeb.Components.Icons
+ ( usageInstructionTooltip
+ , chevronRightOutline
+ , pen
+ , lookingGlass
+ , information
+ , exception
+ ) where
+
+import Data.Text (Text)
+import Lucid
+import Lucid.Svg
+ ( d_
+ , fill_
+ , path_
+ , stroke_
+ , stroke_linecap_
+ , stroke_linejoin_
+ , stroke_width_
+ , viewBox_
+ )
+import PyF
+
+import FloraWeb.Pages.Templates.Types (FloraHTML)
+
+usageInstructionTooltip :: FloraHTML
+usageInstructionTooltip =
+ toHtmlRaw @Text
+ [str|
+
+|]
+
+chevronRightOutline :: FloraHTML
+chevronRightOutline =
+ toHtmlRaw @Text
+ [str|
+
+|]
+
+pen :: FloraHTML
+pen =
+ toHtmlRaw @Text
+ [str|
+
+|]
+
+lookingGlass :: FloraHTML
+lookingGlass =
+ button_ [type_ "submit"] $
+ svg_ [xmlns_ "http://www.w3.org/2000/svg", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $
+ path_ [stroke_linecap_ "round", stroke_linejoin_ "round", stroke_width_ "2", d_ "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"]
+
+information :: FloraHTML
+information =
+ toHtmlRaw @Text
+ [str|
+
+ |]
+
+exception :: FloraHTML
+exception =
+ toHtmlRaw @Text
+ [str|
+
+ |]
diff --git a/src/web/FloraWeb/Components/MainSearchBar.hs b/src/web/FloraWeb/Components/MainSearchBar.hs
new file mode 100644
index 00000000..f31a63ac
--- /dev/null
+++ b/src/web/FloraWeb/Components/MainSearchBar.hs
@@ -0,0 +1,23 @@
+module FloraWeb.Components.MainSearchBar where
+
+import FloraWeb.Components.Icons
+import FloraWeb.Pages.Templates.Types (FloraHTML)
+import Lucid
+
+mainSearchBar :: FloraHTML
+mainSearchBar =
+ form_ [action_ "/search", method_ "GET"] $ do
+ div_ [class_ "main-search"] $ do
+ label_ [for_ "search"] ""
+ input_
+ [ class_
+ "search-bar"
+ , type_ "search"
+ , id_ "search"
+ , name_ "q"
+ , placeholder_ "Find a package"
+ , value_ ""
+ , tabindex_ "1"
+ , autofocus_
+ ]
+ lookingGlass
diff --git a/src/web/FloraWeb/Components/Navbar.hs b/src/web/FloraWeb/Components/Navbar.hs
index b32b7bcd..8c0c1f41 100644
--- a/src/web/FloraWeb/Components/Navbar.hs
+++ b/src/web/FloraWeb/Components/Navbar.hs
@@ -28,7 +28,7 @@ navbar = do
navBarLink' "/about" "About" aboutNav
navBarLink' "/categories" "Categories" packagesNav
navBarLink' "/packages" "Packages" packagesNav
- -- userMenu
+ userMenu
themeToggle
brand :: FloraHTML
@@ -110,18 +110,24 @@ userMenu = do
navbarSearch :: FloraHTML
navbarSearch = do
flag <- asks displayNavbarSearch
+ mContent <- asks navbarSearchContent
if flag
then do
+ let contentValue =
+ case mContent of
+ Nothing -> []
+ Just content -> [value_ content]
form_ [action_ "/search", method_ "GET"] $ do
div_ [class_ "flex items-center py-2"] $ do
label_ [for_ "search"] ""
- input_
+ input_ $
[ class_ "navbar-search"
, id_ "search"
, type_ "search"
, name_ "q"
, placeholder_ "Search a package"
]
+ ++ contentValue
else pure mempty
logOff :: Maybe User -> PersistentSessionId -> FloraHTML
diff --git a/src/web/FloraWeb/Components/PackageListHeader.hs b/src/web/FloraWeb/Components/PackageListHeader.hs
index e5b097f3..1aa59ec7 100644
--- a/src/web/FloraWeb/Components/PackageListHeader.hs
+++ b/src/web/FloraWeb/Components/PackageListHeader.hs
@@ -18,6 +18,6 @@ presentationHeader title subtitle numberOfPackages = do
div_ [class_ "page-title"] $ do
h1_ [class_ ""] $ do
span_ [class_ "headline"] $ toHtml title
- p_ [class_ "package-count"] $ toHtml $ display numberOfPackages <> " results"
div_ [class_ "synopsis lg:text-xl text-center"] $
p_ [class_ ""] (toHtml subtitle)
+ p_ [class_ "package-count"] $ toHtml $ display numberOfPackages <> " results"
diff --git a/src/web/FloraWeb/Components/PackageListItem.hs b/src/web/FloraWeb/Components/PackageListItem.hs
index 4a961163..6059d1aa 100644
--- a/src/web/FloraWeb/Components/PackageListItem.hs
+++ b/src/web/FloraWeb/Components/PackageListItem.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE OverloadedLists #-}
+
module FloraWeb.Components.PackageListItem
( packageListItem
, requirementListItem
@@ -6,7 +8,7 @@ module FloraWeb.Components.PackageListItem
where
import Data.Foldable (traverse_)
-import Data.List (sortOn)
+import Data.List (intersperse, sortOn)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text.Display (display)
@@ -53,13 +55,25 @@ requirementListItem allComponentDeps =
traverse_ componentListItems componentDeps
componentListItems :: DependencyInfo -> FloraHTML
-componentListItems DependencyInfo{namespace, name = packageName, latestSynopsis, requirement, latestLicense} = do
+componentListItems DependencyInfo{namespace, name = packageName, latestSynopsis, requirement, latestLicense, components} = do
let href = href_ ("/packages/" <> display namespace <> "/" <> display packageName)
+ component_ = p_ [class_ "package-list-item__component"] . toHtml
li_ [class_ "package-list-item"] $
a_ [href, class_ ""] $ do
- h4_ [class_ "package-list-item__name"] $
+ h4_ [class_ "package-list-item__name"] $ do
strong_ [class_ ""] . toHtml $
display namespace <> "/" <> display packageName
+ case components of
+ [name]
+ | name == display packageName -> pure ()
+ | otherwise -> ":" >> component_ name
+ -- The empty case should never happen but displaying pkg:{} will indicate
+ -- something has gone wrong.
+ _ -> do
+ ":{"
+ sequence_ . intersperse (toHtml @Text ", ") $
+ component_ <$> Vector.toList components
+ "}"
p_ [class_ "package-list-item__synopsis"] $ toHtml latestSynopsis
div_ [class_ "package-list-item__metadata"] $ do
span_ [class_ "package-list-item__license"] $ do
diff --git a/src/web/FloraWeb/Components/PaginationNav.hs b/src/web/FloraWeb/Components/PaginationNav.hs
index dcf1f4ad..4e432746 100644
--- a/src/web/FloraWeb/Components/PaginationNav.hs
+++ b/src/web/FloraWeb/Components/PaginationNav.hs
@@ -49,8 +49,8 @@ mkURL (ListAllPackagesInNamespace namespace) pageNumber =
"/" <> toUrlPiece (Links.namespaceLink namespace pageNumber)
mkURL (SearchPackages searchTerm) pageNumber =
"/" <> toUrlPiece (Links.packageSearchLink searchTerm pageNumber)
-mkURL (DependentsOf namespace packageName) pageNumber =
- "/" <> toUrlPiece (Links.packageDependents namespace packageName pageNumber)
+mkURL (DependentsOf namespace packageName mbSearchString) pageNumber =
+ "/" <> toUrlPiece (Links.packageDependents namespace packageName pageNumber mbSearchString)
paginate
:: Word
diff --git a/src/web/FloraWeb/Components/SlimSearchBar.hs b/src/web/FloraWeb/Components/SlimSearchBar.hs
new file mode 100644
index 00000000..0d4685de
--- /dev/null
+++ b/src/web/FloraWeb/Components/SlimSearchBar.hs
@@ -0,0 +1,30 @@
+module FloraWeb.Components.SlimSearchBar (slimSearchBar, SearchBarOptions (..)) where
+
+import Data.Text (Text)
+import FloraWeb.Components.Icons
+import FloraWeb.Pages.Templates.Types (FloraHTML)
+import Lucid
+
+data SearchBarOptions = SearchBarOptions
+ { actionUrl :: Text
+ , placeholder :: Text
+ , value :: Text
+ }
+
+slimSearchBar :: SearchBarOptions -> FloraHTML
+slimSearchBar SearchBarOptions{actionUrl, placeholder, value} =
+ form_ [action_ actionUrl, method_ "GET"] $! do
+ div_ [class_ "secondary-search"] $ do
+ label_ [for_ "search"] ""
+ input_
+ [ class_
+ "search-bar"
+ , type_ "search"
+ , id_ "search"
+ , name_ "q"
+ , placeholder_ placeholder
+ , value_ value
+ , tabindex_ "1"
+ , autofocus_
+ ]
+ lookingGlass
diff --git a/src/web/FloraWeb/Components/Utils.hs b/src/web/FloraWeb/Components/Utils.hs
index 21e31556..777694d7 100644
--- a/src/web/FloraWeb/Components/Utils.hs
+++ b/src/web/FloraWeb/Components/Utils.hs
@@ -2,7 +2,7 @@ module FloraWeb.Components.Utils where
import Data.Text (Text)
import FloraWeb.Pages.Templates.Types (FloraHTML)
-import Lucid (Attribute, a_, class_, href_, role_, toHtml)
+import Lucid
import Lucid.Base (makeAttribute)
text :: Text -> FloraHTML
diff --git a/src/web/FloraWeb/Links.hs b/src/web/FloraWeb/Links.hs
index 7529b679..b2b7f767 100644
--- a/src/web/FloraWeb/Links.hs
+++ b/src/web/FloraWeb/Links.hs
@@ -82,14 +82,20 @@ packageDependencies namespace packageName version =
/: packageName
/: version
-packageDependents :: Namespace -> PackageName -> Positive Word -> Link
-packageDependents namespace packageName pageNumber =
+packageDependents
+ :: Namespace
+ -> PackageName
+ -> Positive Word
+ -> Maybe Text
+ -> Link
+packageDependents namespace packageName pageNumber search =
links
// Web.packages
// Web.showDependents
/: namespace
/: packageName
/: Just pageNumber
+ /: search
packageVersions :: Namespace -> PackageName -> Link
packageVersions namespace packageName =
diff --git a/src/web/FloraWeb/Pages/Routes.hs b/src/web/FloraWeb/Pages/Routes.hs
index e4c16263..703a5ade 100644
--- a/src/web/FloraWeb/Pages/Routes.hs
+++ b/src/web/FloraWeb/Pages/Routes.hs
@@ -5,6 +5,7 @@ import FloraWeb.Pages.Routes.Categories qualified as Categories
import FloraWeb.Pages.Routes.Packages qualified as Packages
import FloraWeb.Pages.Routes.Search qualified as Search
import FloraWeb.Pages.Routes.Sessions qualified as Sessions
+import FloraWeb.Pages.Routes.Settings qualified as Settings
import Lucid
import Servant
import Servant.API.Generic
@@ -20,6 +21,7 @@ data Routes' mode = Routes'
, packages :: mode :- "packages" :> Packages.Routes
, categories :: mode :- "categories" :> Categories.Routes
, search :: mode :- "search" :> Search.Routes
+ , settings :: mode :- AuthProtect "cookie-auth" :> "settings" :> Settings.Routes
, notFound :: mode :- Get '[HTML] (Html ())
}
deriving stock (Generic)
diff --git a/src/web/FloraWeb/Pages/Routes/Admin.hs b/src/web/FloraWeb/Pages/Routes/Admin.hs
index 13110494..63cb3179 100644
--- a/src/web/FloraWeb/Pages/Routes/Admin.hs
+++ b/src/web/FloraWeb/Pages/Routes/Admin.hs
@@ -17,17 +17,9 @@ type FetchMetadata =
type FetchMetadataResponse =
Headers '[Header "Location" Text] NoContent
-type ImportIndex =
- "index-import"
- :> Verb 'POST 301 '[HTML] ImportIndexResponse
-
-type ImportIndexResponse =
- Headers '[Header "Location" Text] NoContent
-
data Routes' mode = Routes'
{ index :: mode :- Get '[HTML] (Html ())
, fetchMetadata :: mode :- FetchMetadata
- , importIndex :: mode :- ImportIndex
, oddJobs :: mode :- "odd-jobs" :> OddJobs.FinalAPI -- they compose :o
, users :: mode :- "users" :> AdminUsersRoutes
, packages :: mode :- "packages" :> PackagesAdminRoutes
diff --git a/src/web/FloraWeb/Pages/Routes/Packages.hs b/src/web/FloraWeb/Pages/Routes/Packages.hs
index 23b48d3a..73e54008 100644
--- a/src/web/FloraWeb/Pages/Routes/Packages.hs
+++ b/src/web/FloraWeb/Pages/Routes/Packages.hs
@@ -4,11 +4,14 @@ module FloraWeb.Pages.Routes.Packages
)
where
+import Data.ByteString.Lazy (ByteString)
import Data.Positive
+import Data.Text (Text)
import Distribution.Types.Version (Version)
import Flora.Model.Package (Namespace, PackageName)
import Lucid
import Servant
+import Servant.API.ContentTypes.GZip
import Servant.API.Generic
import Servant.HTML.Lucid
@@ -35,6 +38,7 @@ data Routes' mode = Routes'
:> Capture "package" PackageName
:> "dependents"
:> QueryParam "page" (Positive Word)
+ :> QueryParam "q" Text
:> Get '[HTML] (Html ())
, showVersionDependents
:: mode
@@ -43,6 +47,7 @@ data Routes' mode = Routes'
:> Capture "version" Version
:> "dependents"
:> QueryParam "page" (Positive Word)
+ :> QueryParam "q" Text
:> Get '[HTML] (Html ())
, showDependencies
:: mode
@@ -82,5 +87,12 @@ data Routes' mode = Routes'
:> Capture "package" PackageName
:> "versions"
:> Get '[HTML] (Html ())
+ , getTarball
+ :: mode
+ :- Capture "namespace" Namespace
+ :> Capture "package" PackageName
+ :> Capture "version" Version
+ :> Capture "tarball" Text
+ :> Get '[GZipped] ByteString
}
deriving stock (Generic)
diff --git a/src/web/FloraWeb/Pages/Routes/Sessions.hs b/src/web/FloraWeb/Pages/Routes/Sessions.hs
index f517336e..4d67b46f 100644
--- a/src/web/FloraWeb/Pages/Routes/Sessions.hs
+++ b/src/web/FloraWeb/Pages/Routes/Sessions.hs
@@ -54,7 +54,7 @@ data Routes' mode = Routes'
data LoginForm = LoginForm
{ email :: Text
, password :: Text
- , remember :: Maybe ()
+ , totp :: Maybe Text
}
deriving stock (Generic)
diff --git a/src/web/FloraWeb/Pages/Routes/Settings.hs b/src/web/FloraWeb/Pages/Routes/Settings.hs
new file mode 100644
index 00000000..ad4033ea
--- /dev/null
+++ b/src/web/FloraWeb/Pages/Routes/Settings.hs
@@ -0,0 +1,68 @@
+module FloraWeb.Pages.Routes.Settings
+ ( Routes
+ , Routes' (..)
+ , TwoFactorSetupResponses
+ , TwoFactorConfirmationForm (..)
+ , DeleteTwoFactorSetupResponse
+ )
+where
+
+import Lucid
+import Servant
+import Servant.API.Generic
+import Servant.HTML.Lucid
+
+import Data.Text (Text)
+import FloraWeb.Common.Auth ()
+import Web.FormUrlEncoded
+
+type Routes =
+ NamedRoutes Routes'
+
+type GetUserSettings =
+ Get '[HTML] (Html ())
+
+type GetUserSecuritySettings =
+ "security"
+ :> Get '[HTML] (Html ())
+
+type GetTwoFactorSettingsPage =
+ "security"
+ :> "two-factor"
+ :> Get '[HTML] (Html ())
+
+type TwoFactorSetupResponses =
+ '[ WithStatus 200 (Html ())
+ , WithStatus 301 (Headers '[Header "Location" Text] NoContent)
+ ]
+
+data TwoFactorConfirmationForm = TwoFactorConfirmationForm
+ { code :: Text
+ }
+ deriving stock (Generic)
+ deriving anyclass (FromForm, ToForm)
+
+type PostTwoFactorSetup =
+ "security"
+ :> "two-factor"
+ :> "setup"
+ :> ReqBody '[FormUrlEncoded] TwoFactorConfirmationForm
+ :> UVerb 'POST '[HTML] TwoFactorSetupResponses
+
+type DeleteTwoFactorSetup =
+ "security"
+ :> "two-factor"
+ :> "delete"
+ :> Verb 'POST 301 '[HTML] DeleteTwoFactorSetupResponse
+
+type DeleteTwoFactorSetupResponse =
+ Headers '[Header "Location" Text] NoContent
+
+data Routes' mode = Routes'
+ { index :: mode :- GetUserSettings
+ , getSecuritySettings :: mode :- GetUserSecuritySettings
+ , getTwoFactorSettings :: mode :- GetTwoFactorSettingsPage
+ , postTwoFactorSetup :: mode :- PostTwoFactorSetup
+ , deleteTwoFactorSetup :: mode :- DeleteTwoFactorSetup
+ }
+ deriving stock (Generic)
diff --git a/src/web/FloraWeb/Pages/Server.hs b/src/web/FloraWeb/Pages/Server.hs
index 7f33ad22..f003b739 100644
--- a/src/web/FloraWeb/Pages/Server.hs
+++ b/src/web/FloraWeb/Pages/Server.hs
@@ -13,9 +13,10 @@ import FloraWeb.Pages.Server.Categories qualified as Categories
import FloraWeb.Pages.Server.Packages qualified as Packages
import FloraWeb.Pages.Server.Search qualified as Search
import FloraWeb.Pages.Server.Sessions qualified as Sessions
+import FloraWeb.Pages.Server.Settings qualified as Settings
import FloraWeb.Pages.Templates
import FloraWeb.Pages.Templates.Error (web404)
-import FloraWeb.Pages.Templates.Pages.Home qualified as Home
+import FloraWeb.Pages.Templates.Screens.Home qualified as Home
import FloraWeb.Session
import OddJobs.Endpoints qualified as OddJobs
import OddJobs.Types qualified as OddJobs
@@ -30,6 +31,7 @@ server cfg env =
, packages = Packages.server
, categories = Categories.server
, search = Search.server
+ , settings = \_ -> hoistServerWithContext (Proxy @Settings.Routes) (Proxy @'[OptionalAuthContext]) id Settings.server
, notFound = serveNotFound
}
diff --git a/src/web/FloraWeb/Pages/Server/Admin.hs b/src/web/FloraWeb/Pages/Server/Admin.hs
index 23cfbe89..c9f0c258 100644
--- a/src/web/FloraWeb/Pages/Server/Admin.hs
+++ b/src/web/FloraWeb/Pages/Server/Admin.hs
@@ -2,9 +2,13 @@ module FloraWeb.Pages.Server.Admin where
import Control.Concurrent (forkIO)
import Control.Concurrent.Async qualified as Async
+import Control.Monad (void, when)
import Control.Monad.IO.Class
+import Data.Maybe (isJust)
import Data.Proxy (Proxy (..))
import Database.PostgreSQL.Entity.DBT
+import Effectful.Reader.Static (ask)
+import Log qualified
import Lucid
import Network.HTTP.Types.Status (notFound404)
import OddJobs.Endpoints qualified as OddJobs
@@ -12,8 +16,7 @@ import OddJobs.Types qualified as OddJobs
import Optics.Core
import Servant (HasServer (..), hoistServer)
-import Control.Monad (void)
-import Flora.Environment (FloraEnv (..))
+import Flora.Environment (FeatureEnv (..), FloraEnv (..))
import Flora.Model.Admin.Report
import Flora.Model.Package.Query qualified as Query
import Flora.Model.Release.Query qualified as Query
@@ -40,15 +43,13 @@ server cfg env =
, packages = adminPackagesHandler
, oddJobs = OddJobs.server cfg env handlerToEff
, fetchMetadata = fetchMetadataHandler
- , importIndex = indexImportJobHandler
}
-- | This function converts a sub-tree of routes that require 'Admin' role
-- to a sub-tree of Flora pages.
-- It acts as the safeguard that rejects non-admins from protected routes.
ensureAdmin :: ServerT Routes FloraAdmin -> ServerT Routes FloraPage
-ensureAdmin adminServer = do
- hoistServer (Proxy :: Proxy Routes) checkAdmin adminServer
+ensureAdmin adminServer = hoistServer (Proxy :: Proxy Routes) checkAdmin adminServer
where
checkAdmin :: FloraAdmin a -> FloraPage a
checkAdmin adminRoutes = do
@@ -65,67 +66,69 @@ indexHandler = do
templateEnv <-
fromSession session defaultTemplateEnv
>>= \te -> pure $ set (#activeElements % #adminDashboard) True te
- FloraEnv{pool} <- liftIO $ fetchFloraEnv (session.webEnvStore)
+ FloraEnv{pool} <- liftIO $ fetchFloraEnv session.webEnvStore
report <- liftIO $ withPool pool getReport
render templateEnv (Templates.index report)
fetchMetadataHandler :: FloraAdmin FetchMetadataResponse
fetchMetadataHandler = do
session <- getSession
- FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore)
+ FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv session.webEnvStore
liftIO $ void $ schedulePackageDeprecationListJob jobsPool
- releasesWithoutReadme <- Query.getPackageReleasesWithoutReadme
+ releasesWithoutReadme <- Query.getHackagePackageReleasesWithoutReadme
liftIO $
void $
forkIO $
Async.forConcurrently_
releasesWithoutReadme
- ( \(releaseId, version, packagename) -> do
- scheduleReadmeJob jobsPool releaseId packagename version
+ ( \(releaseId, version, packagename) -> scheduleReadmeJob jobsPool releaseId packagename version
)
- releasesWithoutUploadTime <- Query.getPackageReleasesWithoutUploadTimestamp
+ releasesWithoutUploadTime <- Query.getHackagePackageReleasesWithoutUploadTimestamp
liftIO $
void $
forkIO $
Async.forConcurrently_
releasesWithoutUploadTime
- ( \(releaseId, version, packagename) -> do
- scheduleUploadTimeJob jobsPool releaseId packagename version
+ ( \(releaseId, version, packagename) -> scheduleUploadTimeJob jobsPool releaseId packagename version
)
- releasesWithoutChangelog <- Query.getPackageReleasesWithoutChangelog
+ releasesWithoutChangelog <- Query.getHackagePackageReleasesWithoutChangelog
liftIO $
void $
forkIO $
Async.forConcurrently_
releasesWithoutChangelog
- ( \(releaseId, version, packagename) -> do
- scheduleChangelogJob jobsPool releaseId packagename version
+ ( \(releaseId, version, packagename) -> scheduleChangelogJob jobsPool releaseId packagename version
)
- packagesWithoutDeprecationInformation <- Query.getPackagesWithoutReleaseDeprecationInformation
+ features <- ask @FeatureEnv
+ Log.logAttention "features" features
+ when (isJust $ features.blobStoreImpl) $ do
+ releasesWithoutTarball <- Query.getHackagePackageReleasesWithoutTarball
+ liftIO $!
+ void $!
+ forkIO $!
+ Async.forConcurrently_
+ releasesWithoutTarball
+ ( \(releaseId, version, packagename) ->
+ scheduleTarballJob jobsPool releaseId packagename version
+ )
+
+ packagesWithoutDeprecationInformation <- Query.getHackagePackagesWithoutReleaseDeprecationInformation
liftIO $
void $
forkIO $ do
Async.forConcurrently_
packagesWithoutDeprecationInformation
- ( \a -> do
- scheduleReleaseDeprecationListJob jobsPool a
+ ( \a -> scheduleReleaseDeprecationListJob jobsPool a
)
void $ scheduleRefreshLatestVersions jobsPool
pure $ redirect "/admin"
-indexImportJobHandler :: FloraAdmin ImportIndexResponse
-indexImportJobHandler = do
- session <- getSession
- FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore)
- liftIO $ void $ scheduleIndexImportJob jobsPool
- pure $ redirect "/admin"
-
adminUsersHandler :: ServerT AdminUsersRoutes FloraAdmin
adminUsersHandler =
AdminUsersRoutes'
@@ -153,8 +156,7 @@ showUserHandler userId = do
templateEnv <- fromSession session defaultTemplateEnv
case result of
Nothing -> renderError templateEnv notFound404
- Just user -> do
- render templateEnv (Templates.showUser user)
+ Just user -> render templateEnv (Templates.showUser user)
adminPackagesHandler :: ServerT PackagesAdminRoutes FloraAdmin
adminPackagesHandler =
diff --git a/src/web/FloraWeb/Pages/Server/Categories.hs b/src/web/FloraWeb/Pages/Server/Categories.hs
index ecbc4e28..a50f6f18 100644
--- a/src/web/FloraWeb/Pages/Server/Categories.hs
+++ b/src/web/FloraWeb/Pages/Server/Categories.hs
@@ -12,7 +12,7 @@ import FloraWeb.Common.Auth
import FloraWeb.Pages.Routes.Categories
import FloraWeb.Pages.Templates (defaultTemplateEnv, fromSession, render)
import FloraWeb.Pages.Templates.Error
-import FloraWeb.Pages.Templates.Pages.Categories qualified as Template
+import FloraWeb.Pages.Templates.Screens.Categories qualified as Template
import FloraWeb.Session (getSession)
server :: ServerT Routes FloraPage
diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs
index b7eacf3f..a8666590 100644
--- a/src/web/FloraWeb/Pages/Server/Packages.hs
+++ b/src/web/FloraWeb/Pages/Server/Packages.hs
@@ -4,25 +4,34 @@ module FloraWeb.Pages.Server.Packages
)
where
+import Control.Monad (unless)
+import Data.ByteString.Lazy (ByteString)
import Data.Foldable
import Data.Function
import Data.Map.Strict as Map
-import Data.Maybe (fromMaybe, isNothing)
+import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Positive
+import Data.Text (Text)
import Data.Text.Display (display)
import Data.Vector qualified as Vector
import Distribution.Orphans ()
import Distribution.Types.Version (Version)
+import Effectful.Error.Static (throwError)
+import Effectful.Reader.Static (ask)
import Log (object, (.=))
import Log qualified
import Lucid
import Lucid.Orphans ()
import Servant (ServerT)
+import Servant.Server (err404)
-import Control.Monad.IO.Class
+import Flora.Environment (FeatureEnv (..))
import Flora.Logging
+import Flora.Model.BlobIndex.Query qualified as Query
import Flora.Model.Package
import Flora.Model.Package.Query qualified as Query
+import Flora.Model.PackageIndex.Query qualified as Query
+import Flora.Model.PackageIndex.Types (PackageIndex (..))
import Flora.Model.Release.Query qualified as Query
import Flora.Model.Release.Types
import Flora.Search qualified as Search
@@ -33,9 +42,10 @@ import FloraWeb.Pages.Routes.Packages
import FloraWeb.Pages.Templates
import FloraWeb.Pages.Templates.Error
import FloraWeb.Pages.Templates.Packages qualified as Package
-import FloraWeb.Pages.Templates.Pages.Packages qualified as Packages
-import FloraWeb.Pages.Templates.Pages.Search qualified as Search
+import FloraWeb.Pages.Templates.Screens.Packages qualified as Packages
+import FloraWeb.Pages.Templates.Screens.Search qualified as Search
import FloraWeb.Session
+import Network.HTTP.Types (notFound404)
server :: ServerT Routes FloraPage
server =
@@ -51,6 +61,7 @@ server =
, showChangelog = showChangelogHandler
, showVersionChangelog = showVersionChangelogHandler
, listVersions = listVersionsHandler
+ , getTarball = getTarballHandler
}
listPackagesHandler :: Maybe (Positive Word) -> FloraPage (Html ())
@@ -66,13 +77,37 @@ showNamespaceHandler namespace pageParam = do
let pageNumber = pageParam ?: PositiveUnsafe 1
session <- getSession
templateDefaults <- fromSession session defaultTemplateEnv
- (count', results) <- Search.listAllPackagesInNamespace namespace (fromPage pageNumber)
- render templateDefaults $
- Search.showAllPackagesInNamespace namespace count' pageNumber results
+ (count', results) <- Search.listAllPackagesInNamespace (fromPage pageNumber) namespace
+ if extractNamespaceText namespace == "haskell"
+ then do
+ let description = "Core Haskell packages"
+ let templateEnv =
+ templateDefaults
+ { navbarSearchContent = Just $ "in:" <> display namespace <> " "
+ , description = description
+ }
+ render templateEnv $
+ Search.showAllPackagesInNamespace
+ namespace
+ description
+ count'
+ pageNumber
+ results
+ else do
+ mPackageIndex <- Query.getPackageIndexByName (extractNamespaceText namespace)
+ case mPackageIndex of
+ Nothing -> renderError templateDefaults notFound404
+ Just packageIndex -> do
+ let templateEnv =
+ templateDefaults
+ { navbarSearchContent = Just $ "in:" <> display namespace <> " "
+ , description = packageIndex.description
+ }
+ render templateEnv $
+ Search.showAllPackagesInNamespace namespace packageIndex.description count' pageNumber results
showPackageHandler :: Namespace -> PackageName -> FloraPage (Html ())
-showPackageHandler namespace packageName = do
- showPackageVersion namespace packageName Nothing
+showPackageHandler namespace packageName = showPackageVersion namespace packageName Nothing
showVersionHandler :: Namespace -> PackageName -> Version -> FloraPage (Html ())
showVersionHandler namespace packageName version =
@@ -83,19 +118,19 @@ showPackageVersion namespace packageName mversion = do
session <- getSession
templateEnv' <- fromSession session defaultTemplateEnv
package <- guardThatPackageExists namespace packageName (\_ _ -> web404)
- releases <- Query.getReleases (package.packageId)
- liftIO $ putStrLn $ "Number of releases: " <> show (length releases)
+ packageIndex <- guardThatPackageIndexExists namespace $ const web404
+ releases <- Query.getReleases package.packageId
let latestRelease =
releases
- & Vector.filter (\r -> not (fromMaybe False r.deprecated))
+ & Vector.filter (\r -> Just True /= r.deprecated)
& maximumBy (compare `on` (.version))
version = fromMaybe latestRelease.version mversion
release <- guardThatReleaseExists package.packageId version $ const web404
numberOfReleases <- Query.getNumberOfReleases package.packageId
dependents <- Query.getPackageDependents namespace packageName
releaseDependencies <- Query.getRequirements release.releaseId
- categories <- Query.getPackageCategories (package.packageId)
- numberOfDependents <- Query.getNumberOfPackageDependents namespace packageName
+ categories <- Query.getPackageCategories package.packageId
+ numberOfDependents <- Query.getNumberOfPackageDependents namespace packageName Nothing
numberOfDependencies <- Query.getNumberOfPackageRequirements release.releaseId
let templateEnv =
@@ -123,43 +158,68 @@ showPackageVersion namespace packageName mversion = do
, "package" .= (display namespace <> "/" <> display packageName)
]
+ let packageIndexURL = packageIndex.url
+
render templateEnv $
Packages.showPackage
release
releases
numberOfReleases
package
+ packageIndexURL
dependents
numberOfDependents
releaseDependencies
numberOfDependencies
categories
-showDependentsHandler :: Namespace -> PackageName -> Maybe (Positive Word) -> FloraPage (Html ())
-showDependentsHandler namespace packageName mPage = do
+showDependentsHandler
+ :: Namespace
+ -> PackageName
+ -> Maybe (Positive Word)
+ -> Maybe Text
+ -> FloraPage (Html ())
+showDependentsHandler namespace packageName mPage mSearch = do
package <- guardThatPackageExists namespace packageName (\_ _ -> web404)
- releases <- Query.getAllReleases (package.packageId)
+ releases <- Query.getAllReleases package.packageId
let latestRelease = maximumBy (compare `on` (.version)) releases
- showVersionDependentsHandler namespace packageName latestRelease.version mPage
+ showVersionDependentsHandler namespace packageName latestRelease.version mPage mSearch
-showVersionDependentsHandler :: Namespace -> PackageName -> Version -> Maybe (Positive Word) -> FloraPage (Html ())
-showVersionDependentsHandler namespace packageName version Nothing = showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1)
-showVersionDependentsHandler namespace packageName version (Just pageNumber) = do
+showVersionDependentsHandler
+ :: Namespace
+ -> PackageName
+ -> Version
+ -> Maybe (Positive Word)
+ -> Maybe Text
+ -> FloraPage (Html ())
+showVersionDependentsHandler namespace packageName version Nothing mSearch =
+ showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1) mSearch
+showVersionDependentsHandler namespace packageName version pageNumber (Just "") =
+ showVersionDependentsHandler namespace packageName version pageNumber Nothing
+showVersionDependentsHandler namespace packageName version (Just pageNumber) mSearch = do
session <- getSession
templateEnv' <- fromSession session defaultTemplateEnv
- _ <- guardThatPackageExists namespace packageName (\_ _ -> web404)
+ package <- guardThatPackageExists namespace packageName (\_ _ -> web404)
+ release <- guardThatReleaseExists package.packageId version (const web404)
let templateEnv =
templateEnv'
{ title = display namespace <> "/" <> display packageName
, description = "Dependents of " <> display namespace <> display packageName
+ , navbarSearchContent = Just $ "depends:" <> display namespace <> "/" <> display packageName <> " "
}
- results <- Query.getAllPackageDependentsWithLatestVersion namespace packageName (fromPage pageNumber)
- totalDependents <- Query.getNumberOfPackageDependents namespace packageName
+ results <-
+ Query.getAllPackageDependentsWithLatestVersion
+ namespace
+ packageName
+ (fromPage pageNumber)
+ mSearch
+
+ totalDependents <- Query.getNumberOfPackageDependents namespace packageName mSearch
render templateEnv $
Package.showDependents
namespace
packageName
- version
+ release
totalDependents
results
pageNumber
@@ -167,7 +227,7 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) = d
showDependenciesHandler :: Namespace -> PackageName -> FloraPage (Html ())
showDependenciesHandler namespace packageName = do
package <- guardThatPackageExists namespace packageName (\_ _ -> web404)
- releases <- Query.getAllReleases (package.packageId)
+ releases <- Query.getAllReleases package.packageId
let latestRelease = maximumBy (compare `on` (.version)) releases
showVersionDependenciesHandler namespace packageName latestRelease.version
@@ -184,7 +244,7 @@ showVersionDependenciesHandler namespace packageName version = do
}
(releaseDependencies, duration) <-
timeAction $
- Query.getAllRequirements (release.releaseId)
+ Query.getAllRequirements release.releaseId
Log.logInfo "Retrieving all dependencies of the latest release of a package" $
object
@@ -196,14 +256,14 @@ showVersionDependenciesHandler namespace packageName version = do
]
render templateEnv $
- Package.showDependencies namespace packageName version releaseDependencies
+ Package.showDependencies namespace packageName release releaseDependencies
showChangelogHandler :: Namespace -> PackageName -> FloraPage (Html ())
showChangelogHandler namespace packageName = do
package <- guardThatPackageExists namespace packageName (\_ _ -> web404)
- releases <- Query.getAllReleases (package.packageId)
+ releases <- Query.getAllReleases package.packageId
let latestRelease = maximumBy (compare `on` (.version)) releases
- showVersionChangelogHandler namespace packageName (latestRelease.version)
+ showVersionChangelogHandler namespace packageName latestRelease.version
showVersionChangelogHandler :: Namespace -> PackageName -> Version -> FloraPage (Html ())
showVersionChangelogHandler namespace packageName version = do
@@ -218,7 +278,7 @@ showVersionChangelogHandler namespace packageName version = do
, description = "Changelog of @" <> display namespace <> display packageName
}
- render templateEnv $ Package.showChangelog namespace packageName version (release.changelog)
+ render templateEnv $ Package.showChangelog namespace packageName version release.changelog
listVersionsHandler :: Namespace -> PackageName -> FloraPage (Html ())
listVersionsHandler namespace packageName = do
@@ -230,5 +290,20 @@ listVersionsHandler namespace packageName = do
{ title = display namespace <> "/" <> display packageName
, description = "Releases of " <> display namespace <> display packageName
}
- releases <- Query.getAllReleases (package.packageId)
+ releases <- Query.getAllReleases package.packageId
render templateEnv $ Package.listVersions namespace packageName releases
+
+constructTarballPath :: PackageName -> Version -> Text
+constructTarballPath pname v = display pname <> "-" <> display v <> ".tar.gz"
+
+getTarballHandler :: Namespace -> PackageName -> Version -> Text -> FloraPage ByteString
+getTarballHandler namespace packageName version tarballName = do
+ features <- ask @FeatureEnv
+ unless (isJust features.blobStoreImpl) $ throwError err404
+ package <- guardThatPackageExists namespace packageName $ \_ _ -> web404
+ release <- guardThatReleaseExists package.packageId version $ const web404
+ case release.tarballRootHash of
+ Just rootHash
+ | constructTarballPath packageName version == tarballName ->
+ Query.queryTar packageName version rootHash
+ _ -> throwError err404
diff --git a/src/web/FloraWeb/Pages/Server/Search.hs b/src/web/FloraWeb/Pages/Server/Search.hs
index 76e2b003..91cc1a76 100644
--- a/src/web/FloraWeb/Pages/Server/Search.hs
+++ b/src/web/FloraWeb/Pages/Server/Search.hs
@@ -4,15 +4,14 @@ import Data.Positive
import Data.Text (Text)
import Data.Vector qualified as Vector
import Lucid (Html)
-import Optics.Core
import Servant (ServerT)
import Flora.Model.Package.Types
import Flora.Search qualified as Search
import FloraWeb.Common.Pagination
import FloraWeb.Pages.Routes.Search (Routes, Routes' (..))
-import FloraWeb.Pages.Templates (TemplateEnv (..), defaultTemplateEnv, fromSession, render)
-import FloraWeb.Pages.Templates.Pages.Search qualified as Search
+import FloraWeb.Pages.Templates
+import FloraWeb.Pages.Templates.Screens.Search qualified as Search
import FloraWeb.Session
server :: ServerT Routes FloraPage
@@ -23,22 +22,14 @@ server =
searchHandler :: Maybe Text -> Maybe (Positive Word) -> FloraPage (Html ())
searchHandler Nothing pageParam = searchHandler (Just "") pageParam
-searchHandler (Just "") pageParam = do
- let pageNumber = pageParam ?: PositiveUnsafe 1
- session <- getSession
- templateDefaults <- fromSession session defaultTemplateEnv
- (count, results) <- Search.listAllPackages (fromPage pageNumber)
- let (templateEnv :: TemplateEnv) =
- templateDefaults & #displayNavbarSearch .~ False
- render templateEnv $ Search.showAllPackages count pageNumber results
searchHandler (Just searchString) pageParam = do
let pageNumber = pageParam ?: PositiveUnsafe 1
session <- getSession
- templateEnv <- fromSession session defaultTemplateEnv
- (count, results) <- Search.searchPackageByName (fromPage pageNumber) searchString
- let (matchVector, rest) = Vector.partition (\p -> p.name == PackageName searchString) results
- let (mExactMatch, packagesInfo) =
- case Vector.uncons matchVector of
- Just (exactResult, _) -> (Just exactResult, rest)
- Nothing -> (Nothing, rest)
- render templateEnv $ Search.showResults searchString count pageNumber mExactMatch packagesInfo
+ templateDefaults <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateDefaults
+ { navbarSearchContent = Just searchString
+ }
+ (count, results) <- Search.search (fromPage pageNumber) searchString
+ let (matchVector, packagesInfo) = Vector.partition (\p -> p.name == PackageName searchString) results
+ render templateEnv $ Search.showResults searchString count pageNumber matchVector packagesInfo
diff --git a/src/web/FloraWeb/Pages/Server/Sessions.hs b/src/web/FloraWeb/Pages/Server/Sessions.hs
index e1714946..44f83cdc 100644
--- a/src/web/FloraWeb/Pages/Server/Sessions.hs
+++ b/src/web/FloraWeb/Pages/Server/Sessions.hs
@@ -1,21 +1,28 @@
+{-# LANGUAGE OverloadedRecordDot #-}
+
module FloraWeb.Pages.Server.Sessions where
+import Data.Maybe
import Data.Password.Argon2
import Data.Text.Display
import Log qualified
import Optics.Core
+import Servant
+import Control.Monad.IO.Class
+import Data.Text (Text)
import Flora.Model.PersistentSession
import Flora.Model.User
import Flora.Model.User.Orphans ()
import Flora.Model.User.Query qualified as Query
import FloraWeb.Common.Auth
+import FloraWeb.Common.Auth.TwoFactor qualified as TwoFactor
+import FloraWeb.Common.Guards (guardThatUserHasProvidedTOTP)
import FloraWeb.Common.Utils
import FloraWeb.Pages.Routes.Sessions
import FloraWeb.Pages.Templates
-import FloraWeb.Pages.Templates.Pages.Sessions as Sessions
+import FloraWeb.Pages.Templates.Screens.Sessions as Sessions
import FloraWeb.Session
-import Servant
server :: ServerT Routes FloraPage
server =
@@ -39,7 +46,7 @@ newSessionHandler = do
respond $ WithStatus @301 (redirect "/")
createSessionHandler :: LoginForm -> FloraPage (Union CreateSessionResponses)
-createSessionHandler LoginForm{email, password} = do
+createSessionHandler LoginForm{email, password, totp} = do
session <- getSession
mUser <- Query.getUserByEmail email
case mUser of
@@ -51,20 +58,53 @@ createSessionHandler LoginForm{email, password} = do
& (#flashError ?~ mkError "Could not authenticate")
respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession
Just user ->
- if validatePassword (mkPassword password) (user.password)
- then do
- Log.logInfo_ "[+] User connected!"
- sessionId <- persistSession (session.sessionId) (user.userId)
- let sessionCookie = craftSessionCookie sessionId True
- respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie
+ if user.userFlags.canLogin
+ then
+ if validatePassword (mkPassword password) user.password
+ then do
+ if user.totpEnabled
+ then guardThatUserHasProvidedTOTP totp $ \userCode -> do
+ checkTOTPIsValid userCode user
+ else do
+ sessionId <- persistSession session.sessionId user.userId
+ let sessionCookie = craftSessionCookie sessionId True
+ respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie
+ else do
+ Log.logInfo_ "Invalid password"
+ templateDefaults <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateDefaults
+ & (#flashError ?~ mkError "Could not authenticate")
+ respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession
else do
- Log.logInfo_ "[+] Couldn't authenticate user"
+ Log.logInfo_ "User not allowed to log-in"
templateDefaults <- fromSession session defaultTemplateEnv
let templateEnv =
templateDefaults
& (#flashError ?~ mkError "Could not authenticate")
respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession
+checkTOTPIsValid
+ :: Text
+ -> User
+ -> FloraPage (Union CreateSessionResponses)
+checkTOTPIsValid userCode user = do
+ session <- getSession
+ validated <- liftIO $ TwoFactor.validateTOTP (fromJust user.totpKey) userCode
+ if validated
+ then do
+ Log.logInfo_ "[+] User connected!"
+ sessionId <- persistSession session.sessionId user.userId
+ let sessionCookie = craftSessionCookie sessionId True
+ respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie
+ else do
+ Log.logInfo_ "[+] Couldn't authenticate user's TOTP code"
+ templateDefaults <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateDefaults
+ & (#flashError ?~ mkError "Could not authenticate")
+ respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession
+
deleteSessionHandler :: PersistentSessionId -> FloraPage DeleteSessionResponse
deleteSessionHandler sessionId = do
Log.logInfo_ $ "[+] Logging-off session " <> display sessionId
diff --git a/src/web/FloraWeb/Pages/Server/Settings.hs b/src/web/FloraWeb/Pages/Server/Settings.hs
new file mode 100644
index 00000000..cd500eff
--- /dev/null
+++ b/src/web/FloraWeb/Pages/Server/Settings.hs
@@ -0,0 +1,130 @@
+module FloraWeb.Pages.Server.Settings
+ ( Routes
+ , server
+ ) where
+
+import Control.Monad.IO.Class
+import Data.ByteString.Base32 qualified as Base32
+import Data.Maybe (fromJust)
+import Data.Text.Encoding qualified as Text
+import Log qualified
+import Lucid
+import Optics.Core
+import Sel.HMAC.SHA256 qualified as HMAC
+import Servant
+
+import Flora.Environment
+import Flora.Model.User
+import Flora.Model.User.Update qualified as Update
+import Flora.QRCode qualified as QRCode
+import FloraWeb.Common.Auth.TwoFactor qualified as TwoFactor
+import FloraWeb.Common.Utils (redirect)
+import FloraWeb.Pages.Routes.Settings
+import FloraWeb.Pages.Templates (render, renderUVerb)
+import FloraWeb.Pages.Templates.Screens.Settings qualified as Settings
+import FloraWeb.Pages.Templates.Types
+import FloraWeb.Session
+
+server :: ServerT Routes FloraPage
+server =
+ Routes'
+ { index = userSettingsHandler
+ , getSecuritySettings = userSecuritySettingsHandler
+ , getTwoFactorSettings = getTwoFactorSettingsHandler
+ , postTwoFactorSetup = postTwoFactorSetupHandler
+ , deleteTwoFactorSetup = deleteTwoFactorSetupHandler
+ }
+
+userSettingsHandler :: FloraPage (Html ())
+userSettingsHandler = do
+ session <- getSession
+ templateEnv' <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateEnv'
+ & #title .~ "Account settings"
+ let user = fromJust session.mUser
+ render templateEnv $
+ Settings.dashboard user
+
+userSecuritySettingsHandler :: FloraPage (Html ())
+userSecuritySettingsHandler = do
+ session <- getSession
+ templateEnv' <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateEnv'
+ & #title .~ "Security settings"
+ render
+ templateEnv
+ Settings.securitySettings
+
+getTwoFactorSettingsHandler :: FloraPage (Html ())
+getTwoFactorSettingsHandler = do
+ FloraEnv{domain} <- getEnv
+ session <- getSession
+ templateEnv' <- fromSession session defaultTemplateEnv
+ let templateEnv =
+ templateEnv'
+ & #title .~ "Security settings"
+ let user = fromJust session.mUser
+ case user.totpKey of
+ Nothing -> do
+ userKey <- liftIO HMAC.newAuthenticationKey
+ Update.setupTOTP user.userId userKey
+ let uri = TwoFactor.uriFromKey domain user.email userKey
+ let qrCode =
+ QRCode.generateQRCode uri
+ & Text.decodeUtf8
+ render templateEnv $
+ Settings.twoFactorSettings
+ qrCode
+ (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey)
+ Just userKey -> do
+ if user.totpEnabled
+ then render templateEnv Settings.twoFactorSettingsRemove
+ else do
+ let uri = TwoFactor.uriFromKey domain user.email userKey
+ let qrCode =
+ QRCode.generateQRCode uri
+ & Text.decodeUtf8
+ render templateEnv $
+ Settings.twoFactorSettings
+ qrCode
+ (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey)
+
+postTwoFactorSetupHandler :: TwoFactorConfirmationForm -> FloraPage (Union TwoFactorSetupResponses)
+postTwoFactorSetupHandler TwoFactorConfirmationForm{code = userCode} = do
+ session <- getSession
+ templateEnv' <- fromSession session defaultTemplateEnv
+ let user = fromJust session.mUser
+ case user.totpKey of
+ Nothing -> respond $ WithStatus @301 (redirect "/settings/security/two-factor")
+ Just userKey -> do
+ validated <- liftIO $ TwoFactor.validateTOTP userKey userCode
+ if validated
+ then do
+ Update.confirmTOTP user.userId
+ Log.logInfo_ "Code validation succeeded"
+ respond $ WithStatus @301 (redirect "/settings/security/two-factor")
+ else do
+ Log.logAttention_ "Code validation failed"
+ let templateEnv =
+ templateEnv'
+ & #title .~ "Security settings"
+ & #flashError ?~ mkError "Code validation failed, please retry"
+ let uri = TwoFactor.uriFromKey "localhost" user.email userKey
+ let qrCode =
+ QRCode.generateQRCode uri
+ & Text.decodeUtf8
+ respond $
+ WithStatus @200 $
+ renderUVerb templateEnv $
+ Settings.twoFactorSettings
+ qrCode
+ (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey)
+
+deleteTwoFactorSetupHandler :: FloraPage DeleteTwoFactorSetupResponse
+deleteTwoFactorSetupHandler = do
+ session <- getSession
+ let user = fromJust session.mUser
+ Update.unSetTOTP user.userId
+ pure $ redirect "/settings/security"
diff --git a/src/web/FloraWeb/Pages/Templates.hs b/src/web/FloraWeb/Pages/Templates.hs
index df0b79cd..e8c61cdf 100644
--- a/src/web/FloraWeb/Pages/Templates.hs
+++ b/src/web/FloraWeb/Pages/Templates.hs
@@ -6,12 +6,15 @@ module FloraWeb.Pages.Templates
)
where
+import Control.Monad.Extra (whenJust)
import Control.Monad.Identity (runIdentity)
-import Control.Monad.Reader (runReaderT)
+import Control.Monad.Reader (ask, runReaderT)
import Data.ByteString.Lazy
+import Data.Text.Display
import Lucid
import Flora.Environment (DeploymentEnv (..))
+import FloraWeb.Components.Alert qualified as Alert
import FloraWeb.Components.Header (header)
import FloraWeb.Pages.Templates.Types as Types
@@ -30,7 +33,12 @@ mkErrorPage env template =
rendered :: DeploymentEnv -> FloraHTML -> FloraHTML
rendered _deploymentEnv target = do
+ TemplateEnv{flashInfo, flashError} <- ask
header
+ whenJust flashInfo $ \msg -> do
+ Alert.info (display msg)
+ whenJust flashError $ \msg -> do
+ Alert.exception (display msg)
main_ [] target
-- when (deploymentEnv == Development) $
diff --git a/src/web/FloraWeb/Pages/Templates/Admin.hs b/src/web/FloraWeb/Pages/Templates/Admin.hs
index 49422395..4a0357ec 100644
--- a/src/web/FloraWeb/Pages/Templates/Admin.hs
+++ b/src/web/FloraWeb/Pages/Templates/Admin.hs
@@ -34,20 +34,11 @@ dataReport adminReport = do
div_ [class_ "admin-card"] $ do
dt_
[class_ ""]
- "README, CHANGELOG, Upload time…"
+ "README, CHANGELOG, Upload time, Revision time, deprecation information"
dd_ [class_ ""] $
form_ [action_ "/admin/metadata", method_ "POST"] $ do
- button_ [class_ ""] "Fetch release metadata"
-
- div_ [class_ "admin-card"] $ do
- dt_
- [class_ ""]
- "Index import"
-
- dd_ [class_ ""] $
- form_ [action_ "/admin/index-import", method_ "POST"] $ do
- button_ [class_ ""] "Schedule"
+ button_ [class_ ""] "Fetch Hackage releases metadata"
a_ [href_ "/admin/odd-jobs"] $
div_ [class_ "admin-card"] $ do
diff --git a/src/web/FloraWeb/Pages/Templates/Error.hs b/src/web/FloraWeb/Pages/Templates/Error.hs
index 92af293e..8a4d9154 100644
--- a/src/web/FloraWeb/Pages/Templates/Error.hs
+++ b/src/web/FloraWeb/Pages/Templates/Error.hs
@@ -13,6 +13,7 @@ import Data.Kind (Type)
import Effectful
import Effectful.Error.Static (Error, throwError)
import Effectful.Reader.Static (Reader)
+import Flora.Environment (FeatureEnv)
import FloraWeb.Pages.Templates
import FloraWeb.Session
import Servant (Header, Headers, ServerError (..))
@@ -39,6 +40,7 @@ web404
:: ( Error ServerError :> es
, IOE :> es
, Reader (Headers '[Header "Set-Cookie" SetCookie] Session) :> es
+ , Reader FeatureEnv :> es
)
=> Eff es a
web404 = do
diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs
index 062d2fe3..1a13aeb5 100644
--- a/src/web/FloraWeb/Pages/Templates/Packages.hs
+++ b/src/web/FloraWeb/Pages/Templates/Packages.hs
@@ -1,10 +1,12 @@
-{-# LANGUAGE QuasiQuotes #-}
-
module FloraWeb.Pages.Templates.Packages where
import Control.Monad (when)
+import Control.Monad.Extra (whenJust)
+import Control.Monad.Reader (ask)
+import Data.Foldable (fold, forM_)
import Data.List qualified as List
import Data.Map.Strict qualified as Map
+import Data.Maybe (fromJust, isJust)
import Data.Positive
import Data.Text (Text)
import Data.Text qualified as Text
@@ -15,30 +17,29 @@ import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Data.Vector.Algorithms.Intro qualified as MVector
import Distribution.Orphans ()
+import Distribution.Pretty (pretty)
import Distribution.SPDX.License qualified as SPDX
import Distribution.Types.Flag (PackageFlag (..))
import Distribution.Types.Flag qualified as Flag
import Distribution.Types.Version (Version, mkVersion, versionNumbers)
import Lucid
import Lucid.Base
-import PyF
import Servant (ToHttpApiData (..))
import Text.PrettyPrint (Doc, hcat, render)
import Text.PrettyPrint qualified as PP
-import Data.Foldable (fold)
-import Data.Maybe (fromJust, fromMaybe)
-import Distribution.Pretty (pretty)
+import Flora.Environment (FeatureEnv (..))
import Flora.Model.Category.Types
import Flora.Model.Package
import Flora.Model.Release.Types
import Flora.Model.Requirement
import Flora.Search (SearchAction (..))
+import FloraWeb.Components.Icons
import FloraWeb.Components.PackageListItem (licenseIcon, packageListItem, requirementListItem)
import FloraWeb.Components.PaginationNav (paginationNav)
-import FloraWeb.Components.Utils (text)
+import FloraWeb.Components.Utils
import FloraWeb.Links qualified as Links
-import FloraWeb.Pages.Templates (FloraHTML)
+import FloraWeb.Pages.Templates (FloraHTML, TemplateEnv (..))
import FloraWeb.Pages.Templates.Haddock (renderHaddock)
data Target
@@ -55,84 +56,85 @@ instance Display Target where
presentationHeaderForSubpage
:: Namespace
-> PackageName
- -> Version
+ -> Release
-> Target
-> Word
-> FloraHTML
-presentationHeaderForSubpage namespace packageName version target numberOfPackages = do
- div_ [class_ "divider"] $ do
- div_ [class_ "page-title"] $ do
- h1_ [class_ ""] $ do
- span_ [class_ "headline"] $ do
- displayNamespace namespace
- chevronRightOutline
- linkToPackageWithVersion namespace packageName version
- chevronRightOutline
- toHtml (display target)
- p_ [class_ "synopsis"] $
- span_ [class_ "version"] $
- toHtml $
- display numberOfPackages <> " results"
+presentationHeaderForSubpage namespace packageName release target numberOfPackages = div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ h1_ [class_ ""] $ do
+ span_ [class_ "headline"] $ do
+ displayNamespace namespace
+ chevronRightOutline
+ linkToPackageWithVersion namespace packageName release.version
+ chevronRightOutline
+ toHtml (display target)
+ p_ [class_ "synopsis"] $
+ span_ [class_ "version"] $
+ toHtml $
+ display numberOfPackages
+ <> " results"
presentationHeaderForVersions
:: Namespace
-> PackageName
-> Word
-> FloraHTML
-presentationHeaderForVersions namespace packageName numberOfReleases = do
- div_ [class_ "divider"] $ do
- div_ [class_ "page-title"] $ do
- h1_ [class_ ""] $ do
- span_ [class_ "headline"] $ do
- displayNamespace namespace
- chevronRightOutline
- linkToPackage namespace packageName
- chevronRightOutline
- toHtml (display Versions)
- p_ [class_ "synopsis"] $
- span_ [class_ "version"] $
- toHtml $
- display numberOfReleases <> " results"
+presentationHeaderForVersions namespace packageName numberOfReleases = div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ h1_ [class_ ""] $ do
+ span_ [class_ "headline"] $ do
+ displayNamespace namespace
+ chevronRightOutline
+ linkToPackage namespace packageName
+ chevronRightOutline
+ toHtml (display Versions)
+ p_ [class_ "synopsis"] $
+ span_ [class_ "version"] $
+ toHtml $
+ display numberOfReleases
+ <> " results"
showDependents
:: Namespace
-> PackageName
- -> Version
+ -> Release
-> Word
-> Vector DependencyInfo
-> Positive Word
-> FloraHTML
-showDependents namespace packageName version count packagesInfo currentPage =
+showDependents namespace packageName release count packagesInfo currentPage =
div_ [class_ "container"] $ do
- presentationHeaderForSubpage namespace packageName version Dependents count
- div_ [class_ ""] $ do
- ul_ [class_ "package-list"] $
- Vector.forM_
- packagesInfo
- ( \dep ->
- packageListItem (dep.namespace, dep.name, dep.latestSynopsis, dep.latestVersion, dep.latestLicense)
- )
- when (count > 30) $
- paginationNav count currentPage (DependentsOf namespace packageName)
-
-showDependencies :: Namespace -> PackageName -> Version -> ComponentDependencies -> FloraHTML
-showDependencies namespace packageName version componentsInfo = do
+ presentationHeaderForSubpage namespace packageName release Dependents count
+ ul_ [class_ "package-list"] $ do
+ Vector.forM_
+ packagesInfo
+ ( \dep ->
+ packageListItem
+ ( dep.namespace
+ , dep.name
+ , dep.latestSynopsis
+ , dep.latestVersion
+ , dep.latestLicense
+ )
+ )
+ when (count > 30) $
+ paginationNav count currentPage (DependentsOf namespace packageName Nothing)
+
+showDependencies :: Namespace -> PackageName -> Release -> ComponentDependencies -> FloraHTML
+showDependencies namespace packageName release componentsInfo = do
let dependenciesCount = fromIntegral $ Map.foldr (\v acc -> Vector.length v + acc) 0 componentsInfo
div_ [class_ "container"] $ do
- presentationHeaderForSubpage namespace packageName version Dependencies dependenciesCount
+ presentationHeaderForSubpage namespace packageName release Dependencies dependenciesCount
div_ [class_ ""] $ requirementListing componentsInfo
listVersions :: Namespace -> PackageName -> Vector Release -> FloraHTML
listVersions namespace packageName releases =
div_ [class_ "container"] $ do
presentationHeaderForVersions namespace packageName (fromIntegral $ Vector.length releases)
- div_ [class_ ""] $
- ul_ [class_ "package-list"] $
- Vector.forM_
- releases
- ( \release -> do
- versionListItem namespace packageName release
- )
+ ul_ [class_ "package-list"] $
+ Vector.forM_
+ releases
+ ( versionListItem namespace packageName
+ )
versionListItem :: Namespace -> PackageName -> Release -> FloraHTML
versionListItem namespace packageName release = do
@@ -142,23 +144,36 @@ versionListItem namespace packageName release = do
Just ts ->
span_ [class_ "package-list-item__synopsis"] (toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts)
li_ [class_ "package-list-item"] $
- a_ [href, class_ ""] $ do
- h4_ [class_ "package-list-item__name"] $
- strong_ [class_ ""] . toHtml $
- "v" <> toHtml release.version
- uploadedAt
- div_ [class_ "package-list-item__metadata"] $ span_ [class_ "package-list-item__license"] $ do
- licenseIcon
- toHtml release.license
+ a_ [href, class_ ""] $
+ do
+ h4_ [class_ "package-list-item__name"]
+ $ strong_ [class_ ""]
+ . toHtml
+ $ "v"
+ <> toHtml release.version
+ uploadedAt
+ div_ [class_ "package-list-item__metadata"] $
+ span_ [class_ "package-list-item__license"] $
+ do
+ licenseIcon
+ toHtml release.license
-- | Render a list of package informations
-packageListing :: Vector PackageInfo -> FloraHTML
-packageListing packages =
- ul_ [class_ "package-list"] $
+packageListing
+ :: Maybe (Vector PackageInfo)
+ -- ^ Priority items that are highlighted,
+ -- like exact matches for a search
+ -> Vector PackageInfo
+ -> FloraHTML
+packageListing mExactMatchItems packages =
+ ul_ [class_ "package-list"] $ do
+ whenJust mExactMatchItems $ \exactMatchItems ->
+ forM_ exactMatchItems $ \em ->
+ div_ [class_ "exact-match"] $
+ packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license)
Vector.forM_
packages
- ( \PackageInfo{..} -> do
- packageListItem (namespace, name, synopsis, version, license)
+ ( \PackageInfo{..} -> packageListItem (namespace, name, synopsis, version, license)
)
requirementListing :: ComponentDependencies -> FloraHTML
@@ -166,18 +181,16 @@ requirementListing requirements =
ul_ [class_ "component-list"] $ requirementListItem requirements
showChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML
-showChangelog namespace packageName version mChangelog = do
- div_ [class_ "container"] $ do
- div_ [class_ "divider"] $ do
- div_ [class_ "page-title"] $
- h1_ [class_ ""] $ do
- span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName)
- toHtmlRaw @Text " "
- span_ [class_ "version"] $ toHtml $ display version
- section_ [class_ "release-changelog"] $ do
- case mChangelog of
- Nothing -> toHtml @Text "This release does not have a Changelog"
- Just (MkTextHtml changelogText) -> relaxHtmlT changelogText
+showChangelog namespace packageName version mChangelog = div_ [class_ "container"] $ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $
+ h1_ [class_ ""] $ do
+ span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName)
+ toHtmlRaw @Text " "
+ span_ [class_ "version"] $ toHtml $ display version
+ section_ [class_ "release-changelog"] $ do
+ case mChangelog of
+ Nothing -> toHtml @Text "This release does not have a Changelog"
+ Just (MkTextHtml changelogText) -> relaxHtmlT changelogText
displayReleaseVersion :: Version -> FloraHTML
displayReleaseVersion = toHtml
@@ -222,13 +235,14 @@ displayCategories categories =
div_ [class_ "license "] $ h3_ [class_ "package-body-section"] "Categories"
ul_ [class_ "categories"] $ foldMap renderCategory categories
-displayLinks :: Namespace -> PackageName -> Release -> FloraHTML
-displayLinks namespace packageName release =
+displayLinks :: Namespace -> PackageName -> Text -> Release -> FloraHTML
+displayLinks namespace packageName packageIndexURL release =
li_ [class_ ""] $ do
h3_ [class_ "package-body-section links"] "Links"
ul_ [class_ "links"] $ do
li_ [class_ "package-link"] $ a_ [href_ (getHomepage release)] "Homepage"
- li_ [class_ "package-link"] $ a_ [href_ ("https://hackage.haskell.org/package/" <> display packageName <> "-" <> display release.version)] "Documentation"
+ li_ [class_ "package-link"] $ a_ [href_ (packageIndexURL <> "/package/" <> display packageName <> "-" <> display release.version)] "Documentation"
+
li_ [class_ "package-link"] $ displaySourceRepos release.sourceRepos
li_ [class_ "package-link"] $ displayChangelog namespace packageName release.version release.changelog
@@ -260,16 +274,26 @@ displayVersions namespace packageName versions numberOfReleases =
displayVersion :: Release -> FloraHTML
displayVersion release =
li_ [class_ "release"] $ do
- let versionClass = "release-version" <> if fromMaybe False release.deprecated then " release-deprecated instruction-tooltip" else ""
- let dataText = ([dataText_ "This release is deprecated, pick another one" | fromMaybe False release.deprecated])
+ let versionClass = "release-version" <> if Just True == release.deprecated then " release-deprecated" else ""
+ let dataText = ([dataText_ "This release is deprecated, pick another one" | Just True == release.deprecated])
a_
- ([class_ versionClass, href_ ("/" <> toUrlPiece (Links.packageVersionLink namespace packageName (release.version)))] <> dataText)
- (toHtml $ display (release.version))
+ ([class_ versionClass, href_ ("/" <> toUrlPiece (Links.packageVersionLink namespace packageName release.version))] <> dataText)
+ (toHtml $ display release.version)
" "
case release.uploadedAt of
Nothing -> ""
Just ts ->
- span_ [] (toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts)
+ span_ [] $ do
+ toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts
+ case release.revisedAt of
+ Nothing -> span_ [] ""
+ Just revisionDate ->
+ span_
+ [ dataText_
+ ("Revised on " <> display (Time.formatTime defaultTimeLocale "%a, %_d %b %Y, %R %EZ" revisionDate))
+ , class_ "revised-date"
+ ]
+ pen
displayDependencies
:: (Namespace, PackageName, Version)
@@ -290,13 +314,13 @@ displayDependencies (namespace, packageName, version) numberOfDependencies depen
showAll :: Target -> Maybe Version -> Namespace -> PackageName -> FloraHTML
showAll target mVersion namespace packageName = do
let resource = case target of
- Dependents -> Links.packageDependents namespace packageName (PositiveUnsafe 1)
+ Dependents -> Links.packageDependents namespace packageName (PositiveUnsafe 1) Nothing
Dependencies -> Links.packageDependencies namespace packageName (fromJust mVersion)
Versions -> Links.packageVersions namespace packageName
a_ [class_ "dependency", href_ ("/" <> toUrlPiece resource)] "Show all…"
-displayInstructions :: PackageName -> Release -> FloraHTML
-displayInstructions packageName latestRelease =
+displayInstructions :: Namespace -> PackageName -> Release -> FloraHTML
+displayInstructions namespace packageName latestRelease =
li_ [class_ ""] $ do
h3_ [class_ "package-body-section"] "Installation"
div_ [class_ "items-top"] $ div_ [class_ ""] $ do
@@ -308,35 +332,43 @@ displayInstructions packageName latestRelease =
, value_ (formatInstallString packageName latestRelease)
, readonly_ "readonly"
]
+ TemplateEnv{features} <- ask
+ when (isJust features.blobStoreImpl) $ do
+ label_ [for_ "tarball", class_ "font-light"] "Download"
+ let v = display latestRelease.version
+ tarballName = display packageName <> "-" <> v <> ".tar.gz"
+ tarballLink = "/packages/" <> display namespace <> "/" <> display packageName <> "/" <> v <> "/" <> tarballName
+ div_ $ a_ [href_ tarballLink, download_ ""] $ toHtml tarballName
displayPackageDeprecation :: PackageAlternatives -> FloraHTML
displayPackageDeprecation (PackageAlternatives inFavourOf) =
li_ [class_ ""] $ do
- h3_ [class_ "package-body-section"] "Deprecated"
- div_ [class_ "items-top"] $ div_ [class_ ""] $ do
- if Vector.null inFavourOf
- then label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated"
- else do
- label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated in favour of"
- ul_ [class_ "package-alternatives"] $
- Vector.forM_ inFavourOf $ \PackageAlternative{namespace, package} ->
- li_ [] $
- a_
- [href_ ("/packages/" <> display namespace <> "/" <> display package)]
- (text $ display namespace <> "/" <> display package)
+ h3_ [class_ "package-body-section release-deprecated"] "Deprecated"
+ div_ [class_ "items-top"] $
+ div_ [class_ ""] $
+ if Vector.null inFavourOf
+ then label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated"
+ else do
+ label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated in favour of"
+ ul_ [class_ "package-alternatives"] $
+ Vector.forM_ inFavourOf $
+ \PackageAlternative{namespace, package} ->
+ li_ [] $
+ a_
+ [href_ ("/packages/" <> display namespace <> "/" <> display package)]
+ (text $ display namespace <> "/" <> display package)
displayReleaseDeprecation :: Maybe (Namespace, PackageName, Version) -> FloraHTML
displayReleaseDeprecation mLatestViableRelease =
li_ [class_ ""] $ do
- h3_ [class_ "package-body-section"] "Deprecated"
- div_ [class_ "items-top"] $ div_ [class_ ""] $ do
- case mLatestViableRelease of
- Nothing -> label_ [for_ "install-string", class_ "font-light"] "This release has been deprecated"
- Just (namespace, package, version) -> do
- label_ [for_ "install-string", class_ "font-light"] (text "This release has been deprecated in favour of: ")
- a_
- [href_ ("/packages/" <> display namespace <> "/" <> display package <> "/" <> display version)]
- (text $ display namespace <> "/" <> display package <> "-" <> display version)
+ h3_ [class_ "package-body-section release-deprecated"] "Deprecated"
+ div_ [class_ "items-top"] $ case mLatestViableRelease of
+ Nothing -> label_ [for_ "install-string", class_ "font-light"] "This release has been deprecated"
+ Just (namespace, package, version) -> do
+ label_ [for_ "install-string", class_ "font-light"] (text "This release has been deprecated in favour of: ")
+ a_
+ [href_ ("/packages/" <> display namespace <> "/" <> display package <> "/" <> display version)]
+ (text $ display namespace <> "/" <> display package <> "-" <> display version)
displayTestedWith :: Vector Version -> FloraHTML
displayTestedWith compilersVersions'
@@ -348,10 +380,7 @@ displayTestedWith compilersVersions'
ul_ [class_ "compiler-badges"] $
Vector.forM_
compilersVersions
- ( \version ->
- li_ [] $
- a_ [class_ "compiler-badge"] $
- toHtml @Text (display version)
+ ( li_ [] . a_ [class_ "compiler-badge"] . toHtml @Text . display
)
displayMaintainer :: Text -> FloraHTML
@@ -429,39 +458,17 @@ displayPackageFlag MkPackageFlag{flagName, flagDescription, flagDefault} = case
pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName))
toHtmlRaw @Text " "
defaultMarker flagDefault
- _ -> do
- details_ [] $ do
- summary_ [] $ do
- pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName))
- toHtmlRaw @Text " "
- defaultMarker flagDefault
- div_ [class_ "package-flag-description"] $ do
- renderHaddock $ Text.pack flagDescription
+ _ -> details_ [] $ do
+ summary_ [] $ do
+ pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName))
+ toHtmlRaw @Text " "
+ defaultMarker flagDefault
+ div_ [class_ "package-flag-description"] $ renderHaddock $ Text.pack flagDescription
defaultMarker :: Bool -> FloraHTML
defaultMarker True = em_ "(on by default)"
defaultMarker False = em_ "(off by default)"
----
-
-usageInstructionTooltip :: FloraHTML
-usageInstructionTooltip =
- toHtmlRaw @Text
- [str|
-
-|]
-
-chevronRightOutline :: FloraHTML
-chevronRightOutline =
- toHtmlRaw @Text
- [str|
-
-|]
-
-- | @datalist@ element
dataText_ :: Text -> Attribute
dataText_ = makeAttribute "data-text"
@@ -474,8 +481,9 @@ intercalateVec sep vector =
formatInstallString :: PackageName -> Release -> Text
formatInstallString packageName Release{version} =
- Text.pack . render $
- hcat [pretty packageName, PP.space, rangedVersion, ","]
+ Text.pack
+ . render
+ $ hcat [pretty packageName, PP.space, rangedVersion, ","]
where
rangedVersion :: Doc
rangedVersion = "^>=" <> majMin
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs b/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs
deleted file mode 100644
index aec412b3..00000000
--- a/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs
+++ /dev/null
@@ -1,8 +0,0 @@
-module FloraWeb.Pages.Templates.Pages.Categories
- ( index
- , showCategory
- )
-where
-
-import FloraWeb.Pages.Templates.Pages.Categories.Index (index)
-import FloraWeb.Pages.Templates.Pages.Categories.Show (showCategory)
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs b/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs
deleted file mode 100644
index 6f2cfeeb..00000000
--- a/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-module FloraWeb.Pages.Templates.Pages.Sessions where
-
-import FloraWeb.Pages.Templates.Types
-import Lucid
-
-newSession :: FloraHTML
-newSession = do
- let formClasses = "login-form"
- form_ [action_ "/sessions/new", method_ "POST", class_ formClasses] $ do
- h2_ [class_ ""] "Sign in"
- div_ $ do
- label_ [for_ "email", class_ "sr-only"] "Email address"
- input_
- [ id_ "email"
- , name_ "email"
- , type_ "email"
- , autocomplete_ "email"
- , required_ ""
- , placeholder_ "Email address"
- , class_ "form-input"
- ]
- div_ $ do
- label_ [for_ "password", class_ "sr-only"] "Email address"
- input_
- [ id_ "password"
- , name_ "password"
- , type_ "password"
- , autocomplete_ "current-password"
- , required_ ""
- , placeholder_ "Password"
- , class_ "form-input"
- ]
- -- div_ $ do
- -- label_ [for_ "remember", class_ "text-xl mr-3"] "Remember me"
- -- input_ [id_ "remember", name_ "remember", type_ "checkbox", class_ ""]
-
- div_ $
- button_ [type_ "submit", class_ "btn bg-brand-purple text-white w-full my-2"] "Sign in"
diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs
new file mode 100644
index 00000000..ace512ae
--- /dev/null
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs
@@ -0,0 +1,8 @@
+module FloraWeb.Pages.Templates.Screens.Categories
+ ( index
+ , showCategory
+ )
+where
+
+import FloraWeb.Pages.Templates.Screens.Categories.Index (index)
+import FloraWeb.Pages.Templates.Screens.Categories.Show (showCategory)
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs
similarity index 89%
rename from src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs
rename to src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs
index 3d6df3af..e7bde21c 100644
--- a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs
@@ -1,4 +1,4 @@
-module FloraWeb.Pages.Templates.Pages.Categories.Index where
+module FloraWeb.Pages.Templates.Screens.Categories.Index where
import Data.Vector (Vector)
import Data.Vector qualified as V
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs
similarity index 84%
rename from src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs
rename to src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs
index 3347bc6e..c04cdbdd 100644
--- a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs
@@ -1,4 +1,4 @@
-module FloraWeb.Pages.Templates.Pages.Categories.Show where
+module FloraWeb.Pages.Templates.Screens.Categories.Show where
import Data.Vector (Vector)
import Data.Vector qualified as V
@@ -14,4 +14,4 @@ showCategory :: Category -> Vector PackageInfo -> FloraHTML
showCategory Category{name, synopsis} packagesInfo = do
div_ [class_ "container"] $ do
presentationHeader name synopsis (fromIntegral $ V.length packagesInfo)
- packageListing packagesInfo
+ packageListing Nothing packagesInfo
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs b/src/web/FloraWeb/Pages/Templates/Screens/Home.hs
similarity index 72%
rename from src/web/FloraWeb/Pages/Templates/Pages/Home.hs
rename to src/web/FloraWeb/Pages/Templates/Screens/Home.hs
index d8390549..acbe2470 100644
--- a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Home.hs
@@ -1,31 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
-module FloraWeb.Pages.Templates.Pages.Home where
+module FloraWeb.Pages.Templates.Screens.Home where
import CMarkGFM
import Control.Monad.Reader
import Data.Text (Text)
import Lucid
-import Lucid.Svg
- ( d_
- , fill_
- , path_
- , stroke_
- , stroke_linecap_
- , stroke_linejoin_
- , stroke_width_
- , viewBox_
- )
import PyF
import Flora.Environment
+import FloraWeb.Components.MainSearchBar (mainSearchBar)
import FloraWeb.Pages.Templates.Types
show :: FloraHTML
show = do
banner
div_ [class_ "container-small"] $ do
- searchBar
+ mainSearchBar
buttons
banner :: FloraHTML
@@ -34,26 +25,6 @@ banner = do
h1_ [class_ "main-title"] $
span_ [class_ "main-title"] "Search Haskell packages on Flora"
-searchBar :: FloraHTML
-searchBar =
- form_ [action_ "/search", method_ "GET"] $ do
- div_ [class_ "main-search"] $ do
- label_ [for_ "search"] ""
- input_
- [ class_
- "search-bar"
- , type_ "search"
- , id_ "search"
- , name_ "q"
- , placeholder_ "Find a package"
- , value_ ""
- , tabindex_ "1"
- , autofocus_
- ]
- button_ [type_ "submit"] $
- svg_ [xmlns_ "http://www.w3.org/2000/svg", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $
- path_ [stroke_linecap_ "round", stroke_linejoin_ "round", stroke_width_ "2", d_ "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"]
-
buttons :: FloraHTML
buttons =
section_ [id_ "main-page-buttons"] $ do
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Screens/Packages.hs
similarity index 82%
rename from src/web/FloraWeb/Pages/Templates/Pages/Packages.hs
rename to src/web/FloraWeb/Pages/Templates/Screens/Packages.hs
index 2571f34b..cc1e85ad 100644
--- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Packages.hs
@@ -1,4 +1,4 @@
-module FloraWeb.Pages.Templates.Pages.Packages where
+module FloraWeb.Pages.Templates.Screens.Packages where
import Data.Function ((&))
import Data.Maybe (fromMaybe)
@@ -13,9 +13,9 @@ import Lucid.Orphans ()
import Flora.Model.Category.Types (Category (..))
import Flora.Model.Package.Types
import Flora.Model.Release.Types (Release (..))
+import FloraWeb.Components.Icons
import FloraWeb.Pages.Templates.Packages
- ( chevronRightOutline
- , displayCategories
+ ( displayCategories
, displayDependencies
, displayDependents
, displayInstructions
@@ -38,6 +38,7 @@ showPackage
-> Vector Release
-> Word
-> Package
+ -> Text
-> Vector Package
-> Word
-> Vector (Namespace, PackageName, Text)
@@ -49,15 +50,17 @@ showPackage
packageReleases
numberOfReleases
package@Package{namespace, name}
+ packageIndexURL
dependents
numberOfDependents
dependencies
numberOfDependencies
categories =
div_ [class_ "larger-container"] $ do
- presentationHeader latestRelease namespace name (latestRelease.synopsis)
+ presentationHeader latestRelease namespace name latestRelease.synopsis
packageBody
package
+ packageIndexURL
latestRelease
packageReleases
numberOfReleases
@@ -70,18 +73,20 @@ showPackage
presentationHeader :: Release -> Namespace -> PackageName -> Text -> FloraHTML
presentationHeader release namespace name synopsis =
div_ [class_ "divider"] $ do
- div_ [class_ "page-title"] $
+ div_ [class_ "page-title"] $ do
h1_ [class_ "package-title"] $ do
span_ [class_ "headline"] $ do
displayNamespace namespace
chevronRightOutline
toHtml name
- span_ [class_ "version"] $ displayReleaseVersion release.version
+ let versionClass = "version" <> if Just True == release.deprecated then " release-deprecated" else ""
+ span_ [class_ versionClass] $ displayReleaseVersion release.version
div_ [class_ "synopsis"] $
p_ [class_ ""] (toHtml synopsis)
packageBody
:: Package
+ -> Text
-> Release
-> Vector Release
-> Word
@@ -93,6 +98,7 @@ packageBody
-> FloraHTML
packageBody
Package{namespace, name = packageName, deprecationInfo}
+ packageIndexURL
latestRelease@Release{flags, deprecated, license, maintainer, version}
packageReleases
numberOfReleases
@@ -106,7 +112,7 @@ packageBody
displayCategories categories
displayLicense license
displayMaintainer maintainer
- displayLinks namespace packageName latestRelease
+ displayLinks namespace packageName packageIndexURL latestRelease
displayVersions namespace packageName packageReleases numberOfReleases
div_ [class_ "release-readme-column"] $ div_ [class_ "release-readme"] $ displayReadme latestRelease
div_ [class_ "package-right-column"] $ ul_ [class_ "package-right-rows"] $ do
@@ -115,13 +121,17 @@ packageBody
Nothing ->
if fromMaybe False deprecated
then displayReleaseDeprecation (getLatestViableRelease namespace packageName packageReleases)
- else displayInstructions packageName latestRelease
+ else displayInstructions namespace packageName latestRelease
displayTestedWith latestRelease.testedWith
displayDependencies (namespace, packageName, version) numberOfDependencies dependencies
displayDependents (namespace, packageName) numberOfDependents dependents
displayPackageFlags flags
-getLatestViableRelease :: Namespace -> PackageName -> Vector Release -> Maybe (Namespace, PackageName, Version)
+getLatestViableRelease
+ :: Namespace
+ -> PackageName
+ -> Vector Release
+ -> Maybe (Namespace, PackageName, Version)
getLatestViableRelease namespace packageName releases =
releases
& Vector.filter (\r -> not (fromMaybe False r.deprecated))
diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs
similarity index 53%
rename from src/web/FloraWeb/Pages/Templates/Pages/Search.hs
rename to src/web/FloraWeb/Pages/Templates/Screens/Search.hs
index 0be62482..85a0c49a 100644
--- a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs
@@ -1,17 +1,15 @@
-module FloraWeb.Pages.Templates.Pages.Search where
+module FloraWeb.Pages.Templates.Screens.Search where
import Control.Monad (when)
+import Data.Positive
import Data.Text (Text)
import Data.Text.Display (display)
import Data.Vector (Vector)
import Lucid
-import Data.Maybe (fromJust, isJust)
-import Data.Positive
import Flora.Model.Package (Namespace, PackageInfo (..))
import Flora.Search (SearchAction (..))
import FloraWeb.Components.PackageListHeader (presentationHeader)
-import FloraWeb.Components.PackageListItem
import FloraWeb.Components.PaginationNav (paginationNav)
import FloraWeb.Pages.Templates
import FloraWeb.Pages.Templates.Packages (packageListing)
@@ -20,24 +18,28 @@ showAllPackages :: Word -> Positive Word -> Vector PackageInfo -> FloraHTML
showAllPackages count currentPage packagesInfo = do
div_ [class_ "container"] $ do
presentationHeader "Packages" "" count
- div_ [class_ ""] $ packageListing packagesInfo
+ div_ [class_ ""] $ packageListing Nothing packagesInfo
paginationNav count currentPage ListAllPackages
-showAllPackagesInNamespace :: Namespace -> Word -> Positive Word -> Vector PackageInfo -> FloraHTML
-showAllPackagesInNamespace namespace count currentPage packagesInfo = do
+showAllPackagesInNamespace :: Namespace -> Text -> Word -> Positive Word -> Vector PackageInfo -> FloraHTML
+showAllPackagesInNamespace namespace description count currentPage packagesInfo = do
div_ [class_ "container"] $ do
- presentationHeader (display namespace) "" count
- div_ [class_ ""] $ packageListing packagesInfo
+ presentationHeader (display namespace) description count
+ div_ [class_ ""] $ packageListing Nothing packagesInfo
paginationNav count currentPage (ListAllPackagesInNamespace namespace)
-showResults :: Text -> Word -> Positive Word -> Maybe PackageInfo -> Vector PackageInfo -> FloraHTML
-showResults searchString count currentPage mExactMatch results = do
+showResults
+ :: Text
+ -> Word
+ -> Positive Word
+ -> Vector PackageInfo
+ -- ^ Exact matches
+ -> Vector PackageInfo
+ -- ^ Results
+ -> FloraHTML
+showResults searchString count currentPage exactMatches results = do
div_ [class_ "container"] $ do
presentationHeader searchString "" count
- when (isJust mExactMatch) $ do
- let em = fromJust mExactMatch
- div_ [class_ "exact-match"] $
- packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license)
- div_ [class_ ""] $ packageListing results
+ packageListing (Just exactMatches) results
when (count > 30) $
paginationNav count currentPage (SearchPackages searchString)
diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs
new file mode 100644
index 00000000..3c9217a5
--- /dev/null
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs
@@ -0,0 +1,48 @@
+module FloraWeb.Pages.Templates.Screens.Sessions where
+
+import FloraWeb.Pages.Templates.Types
+import Lucid
+
+newSession :: FloraHTML
+newSession = do
+ let formClasses = "login-form"
+ form_ [action_ "/sessions/new", method_ "POST", class_ formClasses] $ do
+ h2_ [class_ ""] "Sign in"
+ label_ [for_ "email", class_ "sr-only"] "Email address"
+ input_
+ [ id_ "email"
+ , name_ "email"
+ , type_ "email"
+ , autocomplete_ "email"
+ , required_ ""
+ , placeholder_ "Email address"
+ , class_ "form-input"
+ ]
+ label_ [for_ "password", class_ "sr-only"] "Email address"
+ input_
+ [ id_ "password"
+ , name_ "password"
+ , type_ "password"
+ , autocomplete_ "current-password"
+ , required_ ""
+ , placeholder_ "Password"
+ , class_ "form-input password"
+ ]
+ label_ [for_ "use_totp"] "Use two-factor authentication"
+ input_
+ [ id_ "use_totp"
+ , name_ "use_totp"
+ , type_ "checkbox"
+ ]
+ div_ [class_ "totp-zone"] $ do
+ label_ [for_ "totp"] "Two-factor code"
+ input_
+ [ id_ "totp"
+ , name_ "totp"
+ , type_ "text"
+ , pattern_ "0-9]+"
+ , autocomplete_ "off"
+ , class_ "form-input"
+ ]
+ div_ [class_ "login-button"] $
+ button_ [type_ "submit"] "Sign in"
diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs b/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs
new file mode 100644
index 00000000..6a935cf8
--- /dev/null
+++ b/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs
@@ -0,0 +1,78 @@
+module FloraWeb.Pages.Templates.Screens.Settings where
+
+import Lucid
+
+import Data.Text (Text)
+import Flora.Model.User
+import FloraWeb.Components.Button (button)
+import FloraWeb.Pages.Templates
+
+-- import FloraWeb.Components.Button
+
+dashboard :: User -> FloraHTML
+dashboard user = main_ $
+ div_ [class_ "container"] $ do
+ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ do
+ h1_ "Account settings"
+ section_ [class_ "settings_menu"] $ do
+ ul_ [] $ do
+ li_ $ a_ [href_ "/settings/profile"] "Profile"
+ li_ $ a_ [href_ "/settings/security"] "Security"
+
+profileSettings :: User -> FloraHTML
+profileSettings user = do
+ div_ [class_ "container"] $ do
+ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ do
+ h1_ "Profile settings"
+ section_ [] $ do
+ form_ [] $ do
+ h6_ [] "User information"
+ div_ $ do
+ label_ [for_ "email"] "Email address"
+ input_ [type_ "text", name_ "email", id_ "email", required_ "", class_ "form-input"]
+ div_ $
+ button_ [type_ "submit", class_ ""] "Update profile"
+ hr_ [class_ "settings_separator"]
+
+securitySettings :: FloraHTML
+securitySettings = do
+ div_ [class_ "container"] $ do
+ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ do
+ h1_ "Security settings"
+ section_ [] $ do
+ ul_ [] $ do
+ li_ [] $
+ a_ [href_ "/settings/security/two-factor"] "Two-factor authentication"
+
+twoFactorSettings :: Text -> Text -> FloraHTML
+twoFactorSettings qrCode base32Key = do
+ div_ [class_ "container"] $ do
+ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ do
+ h1_ "Two-Factor Authentication"
+ h2_ "Scan the QR Code"
+ img_ [src_ ("data:image/png;base64," <> qrCode), height_ "300", width_ "300"]
+ toHtml base32Key
+ form_ [action_ "/settings/security/two-factor/setup", method_ "POST"] $ do
+ label_ [for_ "code"] "Code from the authenticator app"
+ input_
+ [ id_ "code"
+ , name_ "code"
+ , type_ "text"
+ , required_ ""
+ , placeholder_ "XXXXXX"
+ , class_ "form-input"
+ ]
+ button "Save"
+
+twoFactorSettingsRemove :: FloraHTML
+twoFactorSettingsRemove = do
+ div_ [class_ "container"] $ do
+ div_ [class_ "divider"] $ do
+ div_ [class_ "page-title"] $ do
+ h1_ "Two-Factor Authentication"
+ form_ [action_ "/settings/security/two-factor/delete", method_ "POST"] $
+ button "Delete authenticator application"
diff --git a/src/web/FloraWeb/Pages/Templates/Types.hs b/src/web/FloraWeb/Pages/Templates/Types.hs
index 81ad4cb1..b1987aa7 100644
--- a/src/web/FloraWeb/Pages/Templates/Types.hs
+++ b/src/web/FloraWeb/Pages/Templates/Types.hs
@@ -13,13 +13,16 @@ module FloraWeb.Pages.Templates.Types
where
import Control.Monad.Identity
-import Control.Monad.Reader
+import Control.Monad.Reader (ReaderT)
import Data.Text (Text)
import Data.UUID qualified as UUID
import GHC.Generics
import Lucid
import Optics.Core
+import Data.Text.Display
+import Effectful
+import Effectful.Reader.Static (Reader, ask)
import Flora.Environment
import Flora.Environment.Config (Assets)
import Flora.Model.PersistentSession (PersistentSessionId (..))
@@ -30,13 +33,13 @@ import FloraWeb.Types
type FloraHTML = HtmlT (ReaderT TemplateEnv Identity) ()
newtype FlashInfo = FlashInfo {getFlashInfo :: Text}
- deriving (Show) via Text
+ deriving (Show, Display) via Text
mkInfo :: Text -> FlashInfo
mkInfo = FlashInfo
newtype FlashError = FlashError {getFlashInfo :: Text}
- deriving (Show) via Text
+ deriving (Show, Display) via Text
mkError :: Text -> FlashError
mkError = FlashError
@@ -51,9 +54,11 @@ data TemplateEnv = TemplateEnv
, mUser :: Maybe User
, sessionId :: PersistentSessionId
, environment :: DeploymentEnv
+ , features :: FeatureEnv
, activeElements :: ActiveElements
, assets :: Assets
, indexPage :: Bool
+ , navbarSearchContent :: Maybe Text
}
deriving stock (Show, Generic)
@@ -75,8 +80,10 @@ data TemplateDefaults = TemplateDefaults
, description :: Text
, mUser :: Maybe User
, environment :: DeploymentEnv
+ , features :: FeatureEnv
, activeElements :: ActiveElements
, indexPage :: Bool
+ , navbarSearchContent :: Maybe Text
}
deriving stock (Show, Generic)
@@ -100,8 +107,10 @@ defaultTemplateEnv =
, description = "Package index for the Haskell ecosystem"
, mUser = Nothing
, environment = Development
+ , features = FeatureEnv Nothing
, activeElements = defaultActiveElements
, indexPage = True
+ , navbarSearchContent = Nothing
}
-- | ⚠ DO NOT USE THIS FUNCTION IF YOU DON'T KNOW WHAT YOU'RE DOING
@@ -111,18 +120,20 @@ defaultsToEnv assets TemplateDefaults{..} =
in TemplateEnv{..}
fromSession
- :: MonadIO m
+ :: (Reader FeatureEnv :> es, IOE :> es)
=> Session
-> TemplateDefaults
- -> m TemplateEnv
+ -> Eff es TemplateEnv
fromSession session defaults = do
let sessionId = session.sessionId
let muser = session.mUser
let webEnvStore = session.webEnvStore
floraEnv <- liftIO $ fetchFloraEnv webEnvStore
+ featuresEnv <- ask @FeatureEnv
let assets = floraEnv.assets
let TemplateDefaults{..} =
defaults
& (#mUser .~ muser)
- & (#environment .~ (floraEnv.environment))
+ & (#environment .~ floraEnv.environment)
+ & (#features .~ featuresEnv)
pure TemplateEnv{..}
diff --git a/src/web/FloraWeb/Server.hs b/src/web/FloraWeb/Server.hs
index e774ba1e..071e0080 100644
--- a/src/web/FloraWeb/Server.hs
+++ b/src/web/FloraWeb/Server.hs
@@ -2,7 +2,6 @@ module FloraWeb.Server where
import Colourista.IO (blueMessage)
import Control.Exception (bracket)
-
import Control.Exception.Safe qualified as Safe
import Control.Monad (void, when)
import Data.Aeson qualified as Aeson
@@ -40,6 +39,7 @@ import Optics.Core
import Prometheus qualified
import Prometheus.Metric.GHC (ghcMetrics)
import Prometheus.Metric.Proc (procMetrics)
+import Sel
import Servant
( Application
, Context (..)
@@ -59,15 +59,16 @@ import Servant.API (getResponse)
import Servant.OpenApi
import Servant.Server.Generic (AsServerT, genericServeTWithContext)
-import Flora.Environment (DeploymentEnv, FloraEnv (..), LoggingEnv (..), getFloraEnv)
+import Flora.Environment (BlobStoreImpl (..), DeploymentEnv, FeatureEnv (..), FloraEnv (..), LoggingEnv (..), getFloraEnv)
import Flora.Environment.Config (Assets)
import Flora.Logging (runLog)
import Flora.Logging qualified as Logging
+import Flora.Model.BlobStore.API
import FloraJobs.Runner (runner)
import FloraJobs.Types (JobsRunnerEnv (..), makeConfig, makeUIConfig)
import FloraWeb.API.Routes qualified as API
import FloraWeb.API.Server qualified as API
-import FloraWeb.Common.Auth (FloraAuthContext, authHandler, requestID, runVisitorSession)
+import FloraWeb.Common.Auth (OptionalAuthContext, StrictAuthContext, optionalAuthHandler, requestID, runVisitorSession, strictAuthHandler)
import FloraWeb.Common.Metrics
import FloraWeb.Common.OpenSearch
import FloraWeb.Common.Tracing
@@ -82,29 +83,30 @@ import FloraWeb.Types
runFlora :: IO ()
runFlora =
- bracket
- (getFloraEnv & runFailIO & runEff)
- (runEff . shutdownFlora)
- ( \env ->
- runEff . runTime . runConcurrent $ do
- let baseURL = "http://localhost:" <> display (env.httpPort)
- liftIO $ blueMessage $ "🌺 Starting Flora server on " <> baseURL
- liftIO $ when (isJust $ env.logging.sentryDSN) (blueMessage "📋 Connected to Sentry endpoint")
- liftIO $ when env.logging.prometheusEnabled $ do
- blueMessage $ "📋 Service Prometheus metrics on " <> baseURL <> "/metrics"
- void $ Prometheus.register ghcMetrics
- void $ Prometheus.register procMetrics
- let withLogger = Logging.makeLogger (env.logging.logger)
- withLogger
- ( \appLogger ->
- runServer appLogger env
- )
- )
+ secureMain $
+ bracket
+ (getFloraEnv & runFailIO & runEff)
+ (runEff . shutdownFlora)
+ ( \env ->
+ runEff . runTime . runConcurrent $ do
+ let baseURL = "http://localhost:" <> display env.httpPort
+ liftIO $ blueMessage $ "🌺 Starting Flora server on " <> baseURL
+ liftIO $ when (isJust env.logging.sentryDSN) (blueMessage "📋 Connected to Sentry endpoint")
+ liftIO $ when env.logging.prometheusEnabled $ do
+ blueMessage $ "📋 Service Prometheus metrics on " <> baseURL <> "/metrics"
+ void $ Prometheus.register ghcMetrics
+ void $ Prometheus.register procMetrics
+ let withLogger = Logging.makeLogger env.logging.logger
+ withLogger
+ ( \appLogger ->
+ runServer appLogger env
+ )
+ )
shutdownFlora :: FloraEnv -> Eff '[IOE] ()
shutdownFlora env =
liftIO $
- Pool.destroyAllResources (env.pool)
+ Pool.destroyAllResources env.pool
logException
:: DeploymentEnv
@@ -121,33 +123,36 @@ runServer :: (Concurrent :> es, IOE :> es) => Logger -> FloraEnv -> Eff es ()
runServer appLogger floraEnv = do
httpManager <- liftIO $ HTTP.newManager tlsManagerSettings
let runnerEnv = JobsRunnerEnv httpManager
- let oddjobsUiCfg = makeUIConfig (floraEnv.config) appLogger (floraEnv.jobsPool)
+ let oddjobsUiCfg = makeUIConfig floraEnv.config appLogger floraEnv.jobsPool
oddJobsCfg =
makeConfig
runnerEnv
- (floraEnv.config)
+ floraEnv
appLogger
- (floraEnv.jobsPool)
+ floraEnv.jobsPool
runner
- void $ forkIO $ unsafeEff_ $ Safe.withException (startJobRunner oddJobsCfg) (logException (floraEnv.environment) appLogger)
- loggingMiddleware <- Logging.runLog (floraEnv.environment) appLogger WaiLog.mkLogMiddleware
+ void $
+ forkIO $
+ unsafeEff_ $
+ Safe.withException (startJobRunner oddJobsCfg) (logException floraEnv.environment appLogger)
+ loggingMiddleware <- Logging.runLog floraEnv.environment appLogger WaiLog.mkLogMiddleware
oddJobsEnv <- OddJobs.mkEnv oddjobsUiCfg ("/admin/odd-jobs/" <>)
let webEnv = WebEnv floraEnv
webEnvStore <- liftIO $ newWebEnvStore webEnv
let server = mkServer appLogger webEnvStore floraEnv oddjobsUiCfg oddJobsEnv
let warpSettings =
- setPort (fromIntegral $ floraEnv.httpPort) $
+ setPort (fromIntegral floraEnv.httpPort) $
setOnException
( onException
appLogger
- (floraEnv.environment)
- (floraEnv.logging)
+ floraEnv.environment
+ floraEnv.logging
)
defaultSettings
liftIO
$ runSettings warpSettings
- $ prometheusMiddleware (floraEnv.environment) (floraEnv.logging)
+ $ prometheusMiddleware floraEnv.environment floraEnv.logging
. heartbeatMiddleware
. loggingMiddleware
. const
@@ -160,10 +165,10 @@ mkServer
-> OddJobs.UIConfig
-> OddJobs.Env
-> Application
-mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = do
+mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv =
genericServeTWithContext
- (naturalTransform (floraEnv.environment) logger webEnvStore)
- (floraServer (floraEnv.pool) cfg jobsRunnerEnv)
+ (naturalTransform floraEnv.environment floraEnv.features logger webEnvStore)
+ (floraServer floraEnv.pool cfg jobsRunnerEnv)
(genAuthServerContext logger floraEnv)
-- What the fuck is happening here:
@@ -173,7 +178,7 @@ mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = do
-- [IsVisitor, DB, Time, Reader (Headers '[Header "Set-Cookie" SetCookie] Session), Log, Error ServerError, IOE]
-- api has effects:
-- [DB, Time, Reader (), Log, Error ServerError, IOE]
--- An the intermediate effect list of effects:
+-- And the intermediate effect list of effects:
-- [Reader WebEnvStore, Log, Error ServerError, IOE]
--
-- What must happen is that the list of effects of 'pages' and 'api' must correspond to the intermediate 'Flora'
@@ -192,7 +197,7 @@ floraServer pool cfg jobsRunnerEnv =
, pages = \sessionWithCookies ->
hoistServerWithContext
(Proxy @Pages.Routes)
- (Proxy @'[FloraAuthContext])
+ (Proxy @'[OptionalAuthContext])
( \floraPage ->
floraPage
& runVisitorSession
@@ -216,15 +221,24 @@ floraServer pool cfg jobsRunnerEnv =
, docs = serveDirectoryWith docsBundler
}
-naturalTransform :: DeploymentEnv -> Logger -> WebEnvStore -> Flora a -> Handler a
-naturalTransform deploymentEnv logger webEnvStore app =
+naturalTransform :: DeploymentEnv -> FeatureEnv -> Logger -> WebEnvStore -> Flora a -> Handler a
+naturalTransform deploymentEnv features logger webEnvStore app =
app
& runReader webEnvStore
+ & runReader features
+ & ( case features.blobStoreImpl of
+ Just (BlobStoreFS fp) -> runBlobStoreFS fp
+ _ -> runBlobStorePure
+ )
& runLog deploymentEnv logger
& effToHandler
-genAuthServerContext :: Logger -> FloraEnv -> Context '[FloraAuthContext, ErrorFormatters]
-genAuthServerContext logger floraEnv = authHandler logger floraEnv :. errorFormatters floraEnv.assets :. EmptyContext
+genAuthServerContext :: Logger -> FloraEnv -> Context '[OptionalAuthContext, StrictAuthContext, ErrorFormatters]
+genAuthServerContext logger floraEnv =
+ optionalAuthHandler logger floraEnv
+ :. strictAuthHandler logger floraEnv
+ :. errorFormatters floraEnv.assets
+ :. EmptyContext
errorFormatters :: Assets -> ErrorFormatters
errorFormatters assets =
@@ -232,7 +246,10 @@ errorFormatters assets =
notFoundPage :: Assets -> NotFoundErrorFormatter
notFoundPage assets _req =
- let result = runPureEff $ runErrorNoCallStack $ renderError (defaultsToEnv assets defaultTemplateEnv) notFound404
+ let result =
+ runPureEff $
+ runErrorNoCallStack $
+ renderError (defaultsToEnv assets defaultTemplateEnv) notFound404
in case result of
Left err -> err
Right _ -> err404
@@ -240,6 +257,6 @@ notFoundPage assets _req =
openApiHandler :: OpenApi
openApiHandler =
toOpenApi (Proxy @API.Routes)
- & (#info % #title .~ "Flora API")
- & (#info % #version .~ "v0")
- & (#info % #description ?~ "Flora API Documentation")
+ & #info % #title .~ "Flora API"
+ & #info % #version .~ "v0"
+ & #info % #description ?~ "Flora API Documentation"
diff --git a/src/web/FloraWeb/Types.hs b/src/web/FloraWeb/Types.hs
index 82a1814d..6a7b06e1 100644
--- a/src/web/FloraWeb/Types.hs
+++ b/src/web/FloraWeb/Types.hs
@@ -30,11 +30,14 @@ import Servant (FromHttpApiData (..), Handler, ServerError)
import Web.Cookie
import Flora.Environment
+import Flora.Model.BlobStore.API
type Flora :: Type -> Type
type Flora =
Eff
'[ Reader WebEnvStore
+ , Reader FeatureEnv
+ , BlobStoreAPI
, Log
, Error ServerError
, IOE
@@ -45,6 +48,8 @@ type FloraAPI =
'[ DB
, Time
, Reader ()
+ , Reader FeatureEnv
+ , BlobStoreAPI
, Log
, Error ServerError
, IOE
diff --git a/test/Flora/BlobSpec.hs b/test/Flora/BlobSpec.hs
new file mode 100644
index 00000000..6680fa9b
--- /dev/null
+++ b/test/Flora/BlobSpec.hs
@@ -0,0 +1,100 @@
+module Flora.BlobSpec where
+
+import Codec.Archive.Tar qualified as Tar
+import Codec.Archive.Tar.Entry qualified as Tar
+import Codec.Compression.GZip qualified as GZip
+import Control.Arrow
+import Control.Monad.IO.Class
+import Data.ByteString.Lazy (LazyByteString)
+import Data.ByteString.Lazy qualified as BL
+import Data.Function (on)
+import Data.List (sortBy)
+import Distribution.Version (mkVersion)
+import System.FilePath ((>))
+
+import Data.Foldable (traverse_)
+import Data.Maybe (fromJust)
+import Flora.Model.BlobIndex.Query qualified as Query
+import Flora.Model.BlobIndex.Types
+import Flora.Model.BlobIndex.Update qualified as Update
+import Flora.Model.Package
+import Flora.Model.Package.Query qualified as Query
+import Flora.Model.Package.Types ()
+import Flora.Model.Release.Query qualified as Query
+import Flora.Model.Release.Types
+import Flora.TestUtils
+
+spec :: TestEff TestTree
+spec =
+ testThese
+ "Blob store tests"
+ [ testThis "Import tarball" testImportTarball
+ , testThis "Import bad tarball" testBadTarball
+ , testThis "Import malformed tarball" testMalformedTarball
+ ]
+
+-- Util function to extract a list from Tar.Entries which is easier to compare
+toList :: Tar.Entries e -> Either e [Tar.Entry]
+toList = right reverse . left fst . Tar.foldlEntries (\acc x -> x : acc) []
+
+readTarball :: FilePath -> TestEff LazyByteString
+readTarball tarball = liftIO $ GZip.decompress <$> BL.readFile ("test/fixtures/tarballs" > tarball)
+
+testImportTarball :: TestEff ()
+testImportTarball = do
+ content <- readTarball "b-0.1.0.0.tar.gz"
+ let pname = PackageName "b"
+ version = mkVersion [0, 1, 0, 0]
+ res <- Update.insertTar pname version content
+ case res of
+ Left err -> assertFailure (show err)
+ Right hash -> do
+ content' <- Query.queryTar pname version hash
+ case toList . Tar.read <$> [content, content'] of
+ [Right tarEntries, Right tarEntries'] -> do
+ -- check we've not lost or gained any entries
+ assertEqual (length tarEntries) (length tarEntries')
+ -- Check the output order is sorted
+ checkAll Tar.entryPath (sortByPath tarEntries') tarEntries'
+ -- Check both paths and content are the same in input and output
+ checkAll Tar.entryPath (sortByPath tarEntries) tarEntries'
+ checkAll Tar.entryContent (sortByPath tarEntries) tarEntries'
+
+ -- check that we also archived the initial tarball along with the release
+ package <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "hackage") pname
+ release <- fromJust <$> Query.getReleaseByVersion package.packageId version
+ archivedContent <- fromJust <$> Query.getReleaseTarballArchive release.releaseId
+ assertEqual content archivedContent
+ [Left _, _] -> assertFailure "Input tar is corrupted"
+ [_, Left _] -> assertFailure "Generated corrupted tarball"
+ _ -> assertFailure "Something impossible happened!"
+ where
+ sortByPath = sortBy (compare `on` Tar.entryPath)
+ -- traverse the two lists asserting equality of the results of a
+ -- function on each element
+ checkAll f xs ys =
+ traverse_ (uncurry assertEqual . (f *** f)) $
+ zip xs ys
+
+testBadTarball :: TestEff ()
+testBadTarball = do
+ content <- readTarball "bad-tar-0.1.0.0.tar.gz"
+ let pname = PackageName "bad-tar"
+ version = mkVersion [0, 1, 0, 0]
+ res <- Update.insertTar pname version content
+ case res of
+ Right _ -> assertFailure "Imported bad tarball"
+ Left (BlobStoreTarError _ _ (TarUnsupportedEntry entry)) ->
+ assertEqual entry (Tar.SymbolicLink $ fromJust $ Tar.toLinkTarget "src/Lib.hs")
+ Left err -> assertFailure $ "Unexpected error " <> show err
+
+testMalformedTarball :: TestEff ()
+testMalformedTarball = do
+ content <- readTarball "malformed-tar-0.1.0.0.tar.gz"
+ let pname = PackageName "malformed-tar"
+ version = mkVersion [0, 1, 0, 0]
+ res <- Update.insertTar pname version content
+ case res of
+ Right _ -> assertFailure "Imported malformed tarball"
+ Left (BlobStoreTarError _ _ (TarUnexpectedLayout path)) -> assertEqual path "b-0.1.0.0"
+ Left _ -> assertFailure "Unexpected error"
diff --git a/test/Flora/ImportSpec.hs b/test/Flora/ImportSpec.hs
index 728d8942..68429e56 100644
--- a/test/Flora/ImportSpec.hs
+++ b/test/Flora/ImportSpec.hs
@@ -2,14 +2,17 @@ module Flora.ImportSpec where
import Data.Foldable (traverse_)
import Data.Maybe (catMaybes)
-import Data.Time.Format.ISO8601
+import Data.Set qualified as Set
+import Data.Text (Text)
import Log.Backend.StandardOutput (withStdOutLogger)
import Optics.Core
+import Flora.Import.Package (chooseNamespace)
import Flora.Import.Package.Bulk
import Flora.Model.Package.Query qualified as Query
import Flora.Model.Package.Types
-import Flora.Model.PackageIndex
+import Flora.Model.PackageIndex.Query qualified as Query
+import Flora.Model.PackageIndex.Update qualified as Update
import Flora.Model.Release.Query qualified as Query
import Flora.Model.Release.Types
import Flora.Model.User
@@ -18,28 +21,45 @@ import Flora.TestUtils
spec :: Fixtures -> TestEff TestTree
spec fixtures =
testThese
- "import tests"
+ "Import tests"
[ testThis "Import index" $ testImportIndex fixtures
+ , testThis "Namespace choosser" testNamespaceChooser
]
+testIndex :: FilePath
+testIndex = "./test/fixtures/tarballs/test-index.tar.gz"
+
+defaultRepo :: Text
+defaultRepo = "test-namespace"
+
+defaultRepoURL :: Text
+defaultRepoURL = "localhost"
+
+defaultDescription :: Text
+defaultDescription = "test-description"
+
testImportIndex :: Fixtures -> TestEff ()
testImportIndex fixture = withStdOutLogger $
\logger -> do
- let testIndex = "./test/fixtures/test-index.tar.gz"
- defaultRepo = "hackage.haskell.org"
+ mIndex <- Query.getPackageIndexByName defaultRepo
+ case mIndex of
+ Nothing -> Update.createPackageIndex defaultRepo defaultRepoURL defaultDescription Nothing
+ Just _ -> pure ()
importFromIndex
logger
- (fixture ^. #hackageUser % #userId)
- (Just defaultRepo)
+ (fixture.hackageUser.userId)
+ (defaultRepo, defaultRepoURL)
testIndex
True
- -- Check the expected timestamp
- timestamp <- getPackageIndexTimestamp defaultRepo
- expectedTimestamp <- iso8601ParseM "2010-01-01T00:00:00Z"
- assertEqual (Just expectedTimestamp) timestamp
-- check the packages have been imported
- tars <- traverse (Query.getPackageByNamespaceAndName (Namespace "hackage") . PackageName) ["tar-a", "tar-b"]
+ tars <- traverse (Query.getPackageByNamespaceAndName (Namespace defaultRepo) . PackageName) ["tar-a", "tar-b"]
releases <- fmap mconcat . traverse (\x -> Query.getReleases (x ^. #packageId)) $ catMaybes tars
- assertEqual (length tars) 2
- assertEqual (length releases) 2
- traverse_ (\x -> assertEqual (x ^. #repository) (Just "hackage.haskell.org")) releases
+ assertEqual 2 (length tars)
+ assertEqual 2 (length releases)
+ traverse_ (\x -> assertEqual (x ^. #repository) (Just defaultRepo)) releases
+
+testNamespaceChooser :: TestEff ()
+testNamespaceChooser = do
+ assertEqual
+ (chooseNamespace (PackageName "tar-a") defaultRepo (Set.fromList [PackageName "tar-a", PackageName "tar-b"]))
+ (Namespace defaultRepo)
diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs
index 50c76b20..029f0606 100644
--- a/test/Flora/PackageSpec.hs
+++ b/test/Flora/PackageSpec.hs
@@ -52,7 +52,7 @@ spec _fixtures =
testCabalDeps :: TestEff ()
testCabalDeps = do
dependencies <- do
- Just cabalPackage <- Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "Cabal")
+ cabalPackage <- assertJust =<< Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "Cabal")
releases <- Query.getReleases (cabalPackage ^. #packageId)
let latestRelease = maximumBy (compare `on` (.version)) releases
Query.getAllRequirements (latestRelease ^. #releaseId)
@@ -135,9 +135,9 @@ testCorrectNumberInHaskellNamespace = do
testBytestringDependents :: TestEff ()
testBytestringDependents = do
- results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30)
+ results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30) Nothing
assertEqual
- 22
+ 23
(Vector.length results)
testNoSelfDependent :: TestEff ()
@@ -147,6 +147,7 @@ testNoSelfDependent = do
assertEqual
( Set.fromList
[ PackageName "Cabal"
+ , PackageName "co-log"
, PackageName "flora"
, PackageName "hashable"
, PackageName "jose"
@@ -155,6 +156,7 @@ testNoSelfDependent = do
, PackageName "relude"
, PackageName "saturn"
, PackageName "semigroups"
+ , PackageName "servant-server"
, PackageName "text-display"
, PackageName "xml"
]
@@ -209,7 +211,7 @@ testPackagesDeprecation = do
[ DeprecatedPackage (PackageName "integer-gmp") alternative1
, DeprecatedPackage (PackageName "mtl") alternative2
]
- integerGmp <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "integer-gmp")
+ integerGmp <- assertJust =<< Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "integer-gmp")
assertEqual (Just alternative1) integerGmp.deprecationInfo
testGetNonDeprecatedPackages :: TestEff ()
@@ -222,13 +224,13 @@ testGetNonDeprecatedPackages = do
testReleaseDeprecation :: TestEff ()
testReleaseDeprecation = do
- result <- Query.getPackagesWithoutReleaseDeprecationInformation
- assertEqual 64 (length result)
+ result <- Query.getHackagePackagesWithoutReleaseDeprecationInformation
+ assertEqual 68 (length result)
binary <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "binary")
- Just deprecatedBinaryVersion' <- Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0])
+ deprecatedBinaryVersion' <- assertJust =<< Query.getReleaseByVersion binary.packageId (mkVersion [0, 10, 0, 0])
Update.setReleasesDeprecationMarker (Vector.singleton (True, deprecatedBinaryVersion'.releaseId))
- Just deprecatedBinaryVersion <- Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0])
+ deprecatedBinaryVersion <- assertJust =<< Query.getReleaseByVersion binary.packageId (mkVersion [0, 10, 0, 0])
assertEqual deprecatedBinaryVersion.deprecated (Just True)
---
diff --git a/test/Flora/SearchSpec.hs b/test/Flora/SearchSpec.hs
new file mode 100644
index 00000000..9463df0d
--- /dev/null
+++ b/test/Flora/SearchSpec.hs
@@ -0,0 +1,45 @@
+module Flora.SearchSpec where
+
+import Test.Tasty
+
+import Flora.Model.Package.Types
+import Flora.Search
+import Flora.TestUtils
+
+spec :: TestEff TestTree
+spec =
+ testThese
+ "Search bar mdifiers"
+ [ testThis "Parsing of \"depends:<@namespace>/\" search modifier" testParsingDependsSearchModifier
+ , testThis "Parsing of \"in:<@namespace> \" modifier" testParsingNamespacePackageModifier
+ , testThis "Parsing of \"in:<@namespace>\" modifier" testParsingNamespaceModifier
+ , testThis "Parsing of a query containing a modifier" testParsingQueryContainingModifier
+ ]
+
+testParsingDependsSearchModifier :: TestEff ()
+testParsingDependsSearchModifier = do
+ let result = parseSearchQuery "depends:@haskell/base"
+ assertEqual
+ (Just $ DependentsOf (Namespace "@haskell") (PackageName "base") Nothing)
+ result
+
+testParsingNamespacePackageModifier :: TestEff ()
+testParsingNamespacePackageModifier = do
+ let result = parseSearchQuery "in:@haskell base"
+ assertEqual
+ (Just $ SearchInNamespace (Namespace "@haskell") (PackageName "base"))
+ result
+
+testParsingNamespaceModifier :: TestEff ()
+testParsingNamespaceModifier = do
+ let result = parseSearchQuery "in:@haskell"
+ assertEqual
+ (Just $ ListAllPackagesInNamespace (Namespace "@haskell"))
+ result
+
+testParsingQueryContainingModifier :: TestEff ()
+testParsingQueryContainingModifier = do
+ let result = parseSearchQuery "bah blah blah depends:@haskell/base"
+ assertEqual
+ (Just (SearchPackages "bah blah blah depends:@haskell/base"))
+ result
diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs
index d864ad3e..8bcf60f8 100644
--- a/test/Flora/TestUtils.hs
+++ b/test/Flora/TestUtils.hs
@@ -11,6 +11,7 @@ module Flora.TestUtils
, assertBool
, assertEqual
, assertFailure
+ , assertJust
, assertRight
, assertRight'
, assertClientRight
@@ -99,6 +100,7 @@ import Flora.Environment.Config (LoggingDestination (..), PoolConfig (..))
import Flora.Import.Categories (importCategories)
import Flora.Import.Package.Bulk (importAllFilesInRelativeDirectory, importFromIndex)
import Flora.Logging qualified as Logging
+import Flora.Model.BlobStore.API
import Flora.Model.User
import Flora.Model.User.Query qualified as Query
import Flora.Model.User.Update
@@ -106,7 +108,7 @@ import Flora.Model.User.Update qualified as Update
import Flora.Publish
import FloraWeb.Client
-type TestEff = Eff '[Fail, Reader PoolConfig, DB, Log, Time, IOE]
+type TestEff = Eff '[Fail, BlobStoreAPI, Reader PoolConfig, DB, Log, Time, IOE]
data Fixtures = Fixtures
{ hackageUser :: User
@@ -123,7 +125,7 @@ importAllPackages fixtures = Log.withStdOutLogger $ \appLogger -> do
importAllFilesInRelativeDirectory
appLogger
(fixtures ^. #hackageUser % #userId)
- Nothing
+ ("hackage", "https://hackage.haskell.org")
"./test/fixtures/Cabal/"
True
@@ -135,6 +137,7 @@ runTestEff comp pool poolCfg = runEff $
. Log.runLog "flora-test" stdOutLogger LogAttention
. runDB pool
. runReader poolCfg
+ . runBlobStorePure
. runFailIO
$ comp
@@ -165,6 +168,10 @@ assertEqual expected actual = liftIO $ Test.assertEqual "" expected actual
assertFailure :: MonadIO m => String -> m ()
assertFailure = liftIO . Test.assertFailure
+assertJust :: HasCallStack => Maybe a -> TestEff a
+assertJust (Just a) = pure a
+assertJust Nothing = liftIO $ Test.assertFailure "Test return Nothing instead of Just"
+
assertRight :: HasCallStack => Either a b -> TestEff b
assertRight (Left _a) = liftIO $ Test.assertFailure "Test return Left instead of Right"
assertRight (Right b) = pure b
@@ -283,6 +290,8 @@ genUser = do
userFlags <- genUserFlags
createdAt <- genUTCTime
updatedAt <- genUTCTime
+ let totpKey = Nothing
+ let totpEnabled = False
pure User{..}
data RandomUserTemplate m = RandomUserTemplate
@@ -330,4 +339,6 @@ randomUser
userFlags <- generateUserFlags
createdAt <- generateCreatedAt
updatedAt <- generateUpdatedAt
+ let totpKey = Nothing
+ let totpEnabled = False
pure User{..}
diff --git a/test/Main.hs b/test/Main.hs
index 173bf599..c921283b 100644
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,22 +1,30 @@
module Main where
+import Data.Password.Types
import Effectful
import Effectful.Fail (runFailIO)
import Effectful.Log qualified as Log
import Effectful.PostgreSQL.Transact.Effect
import Effectful.Reader.Static (runReader)
import Effectful.Time
-import Flora.CabalSpec qualified as CabalSpec
-import Flora.CategorySpec qualified as CategorySpec
-import Flora.Environment
import Log.Backend.StandardOutput qualified as Log
import Log.Data
import System.IO
+import System.Process.Typed qualified as Process
import Test.Tasty (defaultMain, testGroup)
+import Flora.BlobSpec qualified as BlobSpec
+import Flora.CabalSpec qualified as CabalSpec
+import Flora.CategorySpec qualified as CategorySpec
+import Flora.Environment
import Flora.ImportSpec qualified as ImportSpec
+import Flora.Model.BlobStore.API
+import Flora.Model.PackageIndex.Update qualified as Update
+import Flora.Model.User (UserCreationForm (..), hashPassword, mkUser)
+import Flora.Model.User.Update qualified as Update
import Flora.OddJobSpec qualified as OddJobSpec
import Flora.PackageSpec qualified as PackageSpec
+import Flora.SearchSpec qualified as SearchSpec
import Flora.TemplateSpec qualified as TemplateSpec
import Flora.TestUtils
import Flora.UserSpec qualified as UserSpec
@@ -24,14 +32,21 @@ import Flora.UserSpec qualified as UserSpec
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
+ Process.runProcess "make db-drop"
+ Process.runProcess "make db-setup"
env <- runEff getFloraTestEnv
fixtures <- runEff $ Log.withStdOutLogger $ \stdOutLogger -> do
runTime
. Log.runLog "flora-test" stdOutLogger LogInfo
. runDB env.pool
. runReader env.dbConfig
+ . runBlobStorePure
. runFailIO
$ do
+ Update.createPackageIndex "hackage" "" "" Nothing
+ password <- hashPassword $ mkPassword "foobar2000"
+ templateUser <- mkUser $ UserCreationForm "hackage-user" "tech@flora.pm" password
+ Update.insertUser templateUser
testMigrations
f' <- getFixtures
importAllPackages f'
@@ -47,4 +62,6 @@ specs fixtures =
, TemplateSpec.spec
, CabalSpec.spec
, ImportSpec.spec fixtures
+ , BlobSpec.spec
+ , SearchSpec.spec
]
diff --git a/test/fixtures/Cabal/b.cabal b/test/fixtures/Cabal/b.cabal
index 9490c463..155ee41b 100644
--- a/test/fixtures/Cabal/b.cabal
+++ b/test/fixtures/Cabal/b.cabal
@@ -19,7 +19,7 @@ library
-- other-modules:
-- other-extensions:
build-depends: base >=4 && <5
- --, sublib
+ , sublib
-- hs-source-dirs:
default-language: Haskell2010
diff --git a/test/fixtures/Cabal/bad-tar.cabal b/test/fixtures/Cabal/bad-tar.cabal
new file mode 100644
index 00000000..92559d38
--- /dev/null
+++ b/test/fixtures/Cabal/bad-tar.cabal
@@ -0,0 +1,17 @@
+cabal-version: 3.0
+-- Initial a.cabal generated by cabal init. For further documentation, see
+-- http://haskell.org/cabal/users-guide/
+
+name: bad-tar
+version: 0.1.0.0
+-- synopsis:
+-- description:
+-- license:
+author: Raoul Hidalgo Charman
+build-type: Simple
+extra-source-files: ChangeLog.md
+
+executable e
+ Main-is: A.hs
+ default-language: Haskell2010
+ build-depends: base >=4 && <5
diff --git a/test/fixtures/Cabal/binary-0.2.cabal b/test/fixtures/Cabal/binary.cabal
similarity index 100%
rename from test/fixtures/Cabal/binary-0.2.cabal
rename to test/fixtures/Cabal/binary.cabal
diff --git a/test/fixtures/Cabal/co-log.cabal b/test/fixtures/Cabal/co-log.cabal
new file mode 100644
index 00000000..79c49ac8
--- /dev/null
+++ b/test/fixtures/Cabal/co-log.cabal
@@ -0,0 +1,189 @@
+cabal-version: 2.4
+name: co-log
+version: 0.6.0.2
+synopsis: Composable Contravariant Comonadic Logging Library
+description:
+ The default implementation of logging based on [co-log-core](http://hackage.haskell.org/package/co-log-core).
+ .
+ The ideas behind this package are described in the following blog post:
+ .
+ * [co-log: Composable Contravariant Combinatorial Comonadic Configurable Convenient Logging](https://kowainik.github.io/posts/2018-09-25-co-log)
+
+homepage: https://github.com/co-log/co-log
+bug-reports: https://github.com/co-log/co-log/issues
+license: MPL-2.0
+license-file: LICENSE
+author: Dmitrii Kovanikov
+maintainer: Kowainik
+copyright: 2018-2022 Kowainik, 2023 Co-Log
+category: Logging, Contravariant, Comonad
+build-type: Simple
+stability: provisional
+extra-doc-files: CHANGELOG.md
+ README.md
+tested-with: GHC == 8.10.7
+ GHC == 9.0.2
+ GHC == 9.2.8
+ GHC == 9.4.7
+ GHC == 9.6.2
+
+flag tutorial
+ description: Controls if tutorials get build (mainly to avoid building them on hackage).
+ default: False
+
+source-repository head
+ type: git
+ location: https://github.com/co-log/co-log.git
+
+common common-options
+ build-depends: base >= 4.14 && < 4.19
+
+ ghc-options: -Wall
+ -Wcompat
+ -Widentities
+ -Wincomplete-uni-patterns
+ -Wincomplete-record-updates
+ -Wredundant-constraints
+ if impl(ghc >= 8.2)
+ ghc-options: -fhide-source-paths
+ if impl(ghc >= 8.4)
+ ghc-options: -Wmissing-export-lists
+ -Wpartial-fields
+ if impl(ghc >= 8.8)
+ ghc-options: -Wmissing-deriving-strategies
+ if impl(ghc >= 8.10)
+ ghc-options: -Wunused-packages
+ if impl(ghc >= 9.0)
+ ghc-options: -Winvalid-haddock
+ if impl(ghc >= 9.2)
+ ghc-options: -Wredundant-bang-patterns
+ -Woperator-whitespace
+
+ default-language: Haskell2010
+ default-extensions: ConstraintKinds
+ DerivingStrategies
+ DeriveGeneric
+ GeneralizedNewtypeDeriving
+ LambdaCase
+ OverloadedStrings
+ RecordWildCards
+ ScopedTypeVariables
+ StandaloneDeriving
+ TupleSections
+ TypeApplications
+ ViewPatterns
+
+common tutorial-options
+ import: common-options
+ if os(windows) || !flag(tutorial)
+ buildable: False
+ build-depends: co-log-core
+ , text
+
+ build-tool-depends: markdown-unlit:markdown-unlit >= 0.5.0 && < 0.7
+ ghc-options: -pgmL markdown-unlit
+
+common tutorial-depends
+ import: tutorial-options
+ build-depends: co-log
+ , mtl
+
+
+library
+ import: common-options
+ hs-source-dirs: src
+ exposed-modules: Colog
+ Colog.Actions
+ Colog.Contra
+ Colog.Message
+ Colog.Monad
+ Colog.Pure
+ Colog.Rotation
+
+ build-depends: ansi-terminal >= 1.0 && < 1.1
+ , bytestring >= 0.10.8 && < 0.13
+ , co-log-core ^>= 0.3
+ , containers >= 0.5.7 && < 0.7
+ , contravariant ^>= 1.5
+ , directory ^>= 1.3.0
+ , exceptions >= 0.8.3 && < 0.11
+ , filepath ^>= 1.4.1
+ , mtl >= 2.2.2 && < 2.4
+ , text >= 1.2.3 && < 2.2
+ , chronos ^>= 1.1 && < 1.2
+ , transformers >= 0.5 && < 0.7
+ , dependent-sum >= 0.7 && < 0.8
+ , dependent-map >= 0.4 && < 0.5
+ , unliftio-core ^>= 0.2
+ , vector >= 0.12.0.3 && < 0.14
+ if impl(ghc < 9.4.5)
+ build-depends: run-st <= 0.1.3.0
+
+executable play-colog
+ import: common-options
+ hs-source-dirs: tutorials
+ main-is: Main.hs
+
+ build-depends: co-log
+ , mtl
+ , dependent-map
+
+ ghc-options: -threaded
+ -rtsopts
+ -with-rtsopts=-N
+
+executable concurrent-playground
+ import: common-options
+ hs-source-dirs: tutorials
+ main-is: Concurrent.hs
+ build-depends: bytestring
+ , co-log
+ ghc-options: -threaded
+ -rtsopts
+ -with-rtsopts=-N
+
+executable readme
+ import: tutorial-options
+ main-is: README.lhs
+ build-depends: co-log
+ , text
+
+test-suite test-co-log
+ import: common-options
+ build-depends: co-log
+ , co-log-core
+ , hedgehog >= 1.0 && < 1.5
+ hs-source-dirs: test
+ main-is: Property.hs
+ type: exitcode-stdio-1.0
+
+test-suite co-log-doctest
+ import: common-options
+ -- Disable `doctest` on windows since it couldn't handle qualified imports reliable (which leads to errors like "Not in scope: `C.timeToOffsetDatetime'").
+ if os(windows)
+ buildable: False
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: Doctest.hs
+
+ build-depends: doctest >= 0.16.0 && < 0.23
+ , Glob ^>= 0.10.0
+ ghc-options: -threaded
+
+
+executable tutorial-intro
+ import: tutorial-options
+ main-is: tutorials/1-intro/Intro.lhs
+
+executable tutorial-loggert-simple
+ import: tutorial-depends
+ main-is: tutorials/2-loggert/loggert.lhs
+
+executable tutorial-loggert
+ import: tutorial-depends
+ main-is: tutorials/3-loggert-with-message/loggert.lhs
+
+executable tutorial-custom
+ import: tutorial-depends
+ main-is: tutorials/custom/Custom.lhs
+
diff --git a/test/fixtures/Cabal/malformed-tar.cabal b/test/fixtures/Cabal/malformed-tar.cabal
new file mode 100644
index 00000000..5f3c4e31
--- /dev/null
+++ b/test/fixtures/Cabal/malformed-tar.cabal
@@ -0,0 +1,17 @@
+cabal-version: 3.0
+-- Initial a.cabal generated by cabal init. For further documentation, see
+-- http://haskell.org/cabal/users-guide/
+
+name: malformed-tar
+version: 0.1.0.0
+-- synopsis:
+-- description:
+-- license:
+author: Raoul Hidalgo Charman
+build-type: Simple
+extra-source-files: ChangeLog.md
+
+executable e
+ Main-is: A.hs
+ default-language: Haskell2010
+ build-depends: base >=4 && <5
diff --git a/test/fixtures/Cabal/servant-server.cabal b/test/fixtures/Cabal/servant-server.cabal
new file mode 100644
index 00000000..16a642ce
--- /dev/null
+++ b/test/fixtures/Cabal/servant-server.cabal
@@ -0,0 +1,171 @@
+cabal-version: 2.2
+name: servant-server
+version: 0.20
+x-revision: 2
+
+synopsis: A family of combinators for defining webservices APIs and serving them
+category: Servant, Web
+description:
+ A family of combinators for defining webservices APIs and serving them
+ .
+ You can learn about the basics in the .
+ .
+
+ is a runnable example, with comments, that defines a dummy API and implements
+ a webserver that serves this API, using this package.
+ .
+
+
+homepage: http://docs.servant.dev/
+bug-reports: http://github.com/haskell-servant/servant/issues
+license: BSD-3-Clause
+license-file: LICENSE
+author: Servant Contributors
+maintainer: haskell-servant-maintainers@googlegroups.com
+copyright: 2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors
+build-type: Simple
+tested-with: GHC==8.6.5, GHC==8.8.4, GHC ==8.10.7, GHC ==9.0.2, GHC ==9.2.7, GHC ==9.4.4
+
+extra-source-files:
+ CHANGELOG.md
+ README.md
+
+source-repository head
+ type: git
+ location: http://github.com/haskell-servant/servant.git
+
+library
+ exposed-modules:
+ Servant
+ Servant.Server
+ Servant.Server.Experimental.Auth
+ Servant.Server.Generic
+ Servant.Server.Internal
+ Servant.Server.Internal.BasicAuth
+ Servant.Server.Internal.Context
+ Servant.Server.Internal.Delayed
+ Servant.Server.Internal.DelayedIO
+ Servant.Server.Internal.ErrorFormatter
+ Servant.Server.Internal.Handler
+ Servant.Server.Internal.RouteResult
+ Servant.Server.Internal.Router
+ Servant.Server.Internal.RoutingApplication
+ Servant.Server.Internal.ServerError
+ Servant.Server.StaticFiles
+ Servant.Server.UVerb
+
+ -- deprecated
+ exposed-modules:
+ Servant.Utils.StaticFiles
+
+ -- Bundled with GHC: Lower bound to not force re-installs
+ -- text and mtl are bundled starting with GHC-8.4
+ build-depends:
+ base >= 4.9 && < 4.19
+ , bytestring >= 0.10.8.1 && < 0.12
+ , constraints >= 0.2 && < 0.14
+ , containers >= 0.5.7.1 && < 0.7
+ , mtl ^>= 2.2.2 || ^>= 2.3.1
+ , text >= 1.2.3.0 && < 2.1
+ , transformers >= 0.5.2.0 && < 0.7
+ , filepath >= 1.4.1.1 && < 1.5
+
+ -- Servant dependencies
+ -- strict dependency as we re-export 'servant' things.
+ build-depends:
+ servant >= 0.20 && < 0.21
+ , http-api-data >= 0.4.1 && < 0.7
+
+ -- Other dependencies: Lower bound around what is in the latest Stackage LTS.
+ -- Here can be exceptions if we really need features from the newer versions.
+ build-depends:
+ base-compat >= 0.10.5 && < 0.14
+ , base64-bytestring >= 1.0.0.1 && < 1.3
+ , exceptions >= 0.10.0 && < 0.11
+ , http-media >= 0.7.1.3 && < 0.9
+ , http-types >= 0.12.2 && < 0.13
+ , network-uri >= 2.6.1.0 && < 2.8
+ , monad-control >= 1.0.2.3 && < 1.1
+ , network >= 2.8 && < 3.2
+ , sop-core >= 0.4.0.0 && < 0.6
+ , string-conversions >= 0.4.0.1 && < 0.5
+ , resourcet >= 1.2.2 && < 1.4
+ , tagged >= 0.8.6 && < 0.9
+ , transformers-base >= 0.4.5.2 && < 0.5
+ , wai >= 3.2.2.1 && < 3.3
+ , wai-app-static >= 3.1.6.2 && < 3.2
+ , word8 >= 0.1.3 && < 0.2
+
+ hs-source-dirs: src
+ default-language: Haskell2010
+
+ ghc-options: -Wall -Wno-redundant-constraints
+
+executable greet
+ main-is: greet.hs
+ hs-source-dirs: example
+ ghc-options: -Wall
+ default-language: Haskell2010
+ build-depends:
+ base
+ , base-compat
+ , servant
+ , servant-server
+ , wai
+ , text
+
+ build-depends:
+ aeson >= 1.4.1.0 && < 3
+ , warp >= 3.2.25 && < 3.4
+
+test-suite spec
+ type: exitcode-stdio-1.0
+ ghc-options: -Wall
+ default-language: Haskell2010
+ hs-source-dirs: test
+ main-is: Spec.hs
+ other-modules:
+ Servant.ArbitraryMonadServerSpec
+ Servant.Server.ErrorSpec
+ Servant.Server.Internal.ContextSpec
+ Servant.Server.Internal.RoutingApplicationSpec
+ Servant.Server.RouterSpec
+ Servant.Server.StaticFilesSpec
+ Servant.Server.StreamingSpec
+ Servant.Server.UsingContextSpec
+ Servant.Server.UsingContextSpec.TestCombinators
+ Servant.HoistSpec
+ Servant.ServerSpec
+
+ -- Dependencies inherited from the library. No need to specify bounds.
+ build-depends:
+ base
+ , base-compat
+ , base64-bytestring
+ , bytestring
+ , http-types
+ , mtl
+ , resourcet
+ , safe
+ , servant
+ , servant-server
+ , sop-core
+ , string-conversions
+ , text
+ , transformers
+ , transformers-compat
+ , wai
+
+ -- Additional dependencies
+ build-depends:
+ aeson >= 1.4.1.0 && < 3
+ , directory >= 1.3.0.0 && < 1.4
+ , hspec >= 2.6.0 && < 2.12
+ , hspec-wai >= 0.10.1 && < 0.12
+ , QuickCheck >= 2.12.6.1 && < 2.15
+ , should-not-typecheck >= 2.1.0 && < 2.2
+ , temporary >= 1.3 && < 1.4
+ , wai-extra >= 3.0.24.3 && < 3.2
+
+ build-tool-depends:
+ hspec-discover:hspec-discover >= 2.6.0 && <2.12
diff --git a/test/fixtures/Cabal/text-display-0.0.5.0.cabal b/test/fixtures/Cabal/text-display-0.0.5.0.cabal
new file mode 100644
index 00000000..1c522c8a
--- /dev/null
+++ b/test/fixtures/Cabal/text-display-0.0.5.0.cabal
@@ -0,0 +1,105 @@
+cabal-version: 3.0
+name: text-display
+version: 0.0.5.0
+x-revision: 1
+category: Text
+synopsis: A typeclass for user-facing output
+description:
+ The 'Display' typeclass provides a solution for user-facing output that does not have to abide by the rules of the Show typeclass.
+
+homepage:
+ https://hackage.haskell.org/package/text-display-0.0.5.0/docs/doc/book/Introduction.html
+
+bug-reports: https://github.com/haskell-text/text-display/issues
+author: Hécate Moonlight
+maintainer: Hécate Moonlight
+license: MIT
+build-type: Simple
+tested-with:
+ GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.7 || ==9.4.5 || ==9.6.1
+
+extra-source-files:
+ LICENSE
+ README.md
+
+extra-doc-files:
+ ./doc/book/*.css
+ ./doc/book/*.html
+ ./doc/book/*.js
+ ./doc/book/css/*.css
+ ./doc/book/favicon.png
+ ./doc/book/favicon.svg
+ ./doc/book/FontAwesome/css/font-awesome.css
+ ./doc/book/FontAwesome/fonts/fontawesome-webfont.woff2
+ ./doc/book/fonts/*.css
+ ./doc/book/fonts/*.woff2
+ ./doc/book/searchindex.json
+ CHANGELOG.md
+
+flag book
+ description: Enable the generation of the book
+ default: False
+ manual: True
+
+source-repository head
+ type: git
+ location: https://github.com/haskell-text/text-display
+
+common common-extensions
+ default-language: Haskell2010
+
+common common-ghc-options
+ ghc-options:
+ -Wall -Wcompat -Widentities -Wincomplete-record-updates
+ -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints
+ -fhide-source-paths -Wno-unused-do-bind -funbox-strict-fields
+ -Wunused-packages
+
+common common-rts-options
+ ghc-options: -rtsopts -threaded -with-rtsopts=-N
+
+library
+ import: common-extensions
+ import: common-ghc-options
+ hs-source-dirs: src
+ exposed-modules:
+ Data.Text.Display
+ Data.Text.Display.Core
+ Data.Text.Display.Generic
+ build-depends:
+ , base >=4.12 && <5.0
+ , bytestring >=0.10 && <0.12
+ , text >=2.0
+
+executable book
+ import: common-extensions
+ import: common-ghc-options
+ import: common-rts-options
+ main-is: Main.hs
+ hs-source-dirs: doc/src
+
+ if !flag(book)
+ buildable: False
+
+ build-depends:
+ , base
+ , directory
+ , literatex
+ , shake
+
+test-suite text-display-test
+ import: common-extensions
+ import: common-ghc-options
+ import: common-rts-options
+ type: exitcode-stdio-1.0
+ main-is: Main.hs
+ hs-source-dirs: test
+ build-depends:
+ , base
+ , deepseq
+ , quickcheck-text
+ , tasty
+ , tasty-hunit
+ , tasty-quickcheck
+ , text
+ , text-display
diff --git a/test/fixtures/Tarball/tar-a.cabal b/test/fixtures/Tarball/tar-a.cabal
deleted file mode 100644
index 3b141c4e..00000000
--- a/test/fixtures/Tarball/tar-a.cabal
+++ /dev/null
@@ -1,35 +0,0 @@
-cabal-version: 3.0
--- Initial a.cabal generated by cabal init. For further documentation, see
--- http://haskell.org/cabal/users-guide/
-
-name: tar-a
-version: 0.1.0.0
--- synopsis:
--- description:
--- license:
-author: Francesco Gazzetta
---maintainer:
--- copyright:
--- category:
-build-type: Simple
-extra-source-files: ChangeLog.md
-
-executable e
- Main-is: A.hs
- default-language: Haskell2010
- build-depends: base >=4 && <5
- , b:{ sublib
- , anothersublib
- -- You can include sublibraries from hackage, like
- --, cabal-plan:{topograph}
- }
-
-library
- exposed-modules: A1
- -- other-modules:
- -- other-extensions:
- build-depends: base >=4 && <5
- , b:{ sublib
- , anothersublib }
- -- hs-source-dirs:
- default-language: Haskell2010
diff --git a/test/fixtures/Tarball/tar-b.cabal b/test/fixtures/Tarball/tar-b.cabal
deleted file mode 100644
index 484f4da5..00000000
--- a/test/fixtures/Tarball/tar-b.cabal
+++ /dev/null
@@ -1,40 +0,0 @@
-cabal-version: 3.0
--- Initial b.cabal generated by cabal init. For further documentation, see
--- http://haskell.org/cabal/users-guide/
-
-name: tar-b
-version: 0.1.0.0
--- synopsis:
--- description:
--- license:
-author: Francesco Gazzetta
---maintainer:
--- copyright:
--- category:
-build-type: Simple
-extra-source-files: ChangeLog.md
-
-library
- exposed-modules: BTop
- -- other-modules:
- -- other-extensions:
- build-depends: base >=4 && <5
- --, sublib
- -- hs-source-dirs:
- default-language: Haskell2010
-
-library sublib
- visibility: public
- exposed-modules: BSub
- -- other-modules:
- -- other-extensions:
- build-depends: base >=4 && <5
- -- hs-source-dirs:
- default-language: Haskell2010
-library anothersublib
- visibility: public
- -- other-modules:
- -- other-extensions:
- build-depends: base >=4 && <5
- -- hs-source-dirs:
- default-language: Haskell2010
diff --git a/test/fixtures/tarballs/b-0.1.0.0.tar.gz b/test/fixtures/tarballs/b-0.1.0.0.tar.gz
new file mode 100644
index 00000000..847e82ed
Binary files /dev/null and b/test/fixtures/tarballs/b-0.1.0.0.tar.gz differ
diff --git a/test/fixtures/tarballs/bad-tar-0.1.0.0.tar.gz b/test/fixtures/tarballs/bad-tar-0.1.0.0.tar.gz
new file mode 100644
index 00000000..af12d7be
Binary files /dev/null and b/test/fixtures/tarballs/bad-tar-0.1.0.0.tar.gz differ
diff --git a/test/fixtures/tarballs/malformed-tar-0.1.0.0.tar.gz b/test/fixtures/tarballs/malformed-tar-0.1.0.0.tar.gz
new file mode 100644
index 00000000..b67a3ce4
Binary files /dev/null and b/test/fixtures/tarballs/malformed-tar-0.1.0.0.tar.gz differ
diff --git a/test/fixtures/tarballs/test-index.tar.gz b/test/fixtures/tarballs/test-index.tar.gz
new file mode 100644
index 00000000..d65c69a7
Binary files /dev/null and b/test/fixtures/tarballs/test-index.tar.gz differ
diff --git a/test/fixtures/test-index.tar.gz b/test/fixtures/test-index.tar.gz
deleted file mode 100644
index 606a8fe0..00000000
Binary files a/test/fixtures/test-index.tar.gz and /dev/null differ